mirror of
https://github.com/nickzuber/meteorite.git
synced 2024-12-01 16:32:48 +03:00
New homepage, working on more native notification support
This commit is contained in:
parent
35bc45c3a3
commit
d85dcb639e
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/icon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<!-- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> -->
|
||||
<meta name="theme-color" content="#24292e">
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<title>Meteorite — Smarter GitHub notifications</title>
|
||||
|
@ -54,6 +54,9 @@ import tagWhite from './svg/tag-white.svg';
|
||||
import sync from './svg/sync.svg';
|
||||
import noPhone from './svg/nophone.svg';
|
||||
import noMusic from './svg/nomusic.svg';
|
||||
import leftArrow from './svg/left-arrow.svg';
|
||||
import notificationsOn from './svg/notifications-on.svg';
|
||||
import notificationsOff from './svg/notifications-off.svg';
|
||||
|
||||
import issue_closed from './svg/github/issue-closed.svg';
|
||||
import issue_open from './svg/github/issue-open.svg';
|
||||
@ -135,6 +138,9 @@ Icon.TagWhite = createIcon(tagWhite);
|
||||
Icon.Sync = createIcon(sync);
|
||||
Icon.NoPhone = createIcon(noPhone);
|
||||
Icon.NoMusic = createIcon(noMusic);
|
||||
Icon.LeftArrow = createIcon(leftArrow);
|
||||
Icon.NotificationsOn = createIcon(notificationsOn);
|
||||
Icon.NotificationsOff = createIcon(notificationsOff);
|
||||
|
||||
Icon.IssueClosed = createIcon(issue_closed);
|
||||
Icon.IssueOpen = createIcon(issue_open);
|
||||
|
1
src/components/Icon/svg/left-arrow.svg
Normal file
1
src/components/Icon/svg/left-arrow.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 11H6.83l2.88-2.88c.39-.39.39-1.02 0-1.41-.39-.39-1.02-.39-1.41 0L3.71 11.3c-.39.39-.39 1.02 0 1.41L8.3 17.3c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L6.83 13H20c.55 0 1-.45 1-1s-.45-1-1-1z"/></svg>
|
After Width: | Height: | Size: 296 B |
1
src/components/Icon/svg/notifications-off.svg
Normal file
1
src/components/Icon/svg/notifications-off.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm0-15.5c2.49 0 4 2.02 4 4.5v.1l2 2V11c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68c-.24.06-.47.15-.69.23l1.64 1.64c.18-.02.36-.05.55-.05zM5.41 3.35L4 4.76l2.81 2.81C6.29 8.57 6 9.74 6 11v5l-2 2v1h14.24l1.74 1.74 1.41-1.41L5.41 3.35zM16 17H8v-6c0-.68.12-1.32.34-1.9L16 16.76V17z"/></svg>
|
After Width: | Height: | Size: 481 B |
1
src/components/Icon/svg/notifications-on.svg
Normal file
1
src/components/Icon/svg/notifications-on.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2zm-2 1H8v-6c0-2.48 1.51-4.5 4-4.5s4 2.02 4 4.5v6zM7.58 4.08L6.15 2.65C3.75 4.48 2.17 7.3 2.03 10.5h2c.15-2.65 1.51-4.97 3.55-6.42zm12.39 6.42h2c-.15-3.2-1.73-6.02-4.12-7.85l-1.42 1.43c2.02 1.45 3.39 3.77 3.54 6.42z"/></svg>
|
After Width: | Height: | Size: 512 B |
1
src/components/Icon/svg/right-arrow.svg
Normal file
1
src/components/Icon/svg/right-arrow.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"/></svg>
|
After Width: | Height: | Size: 195 B |
@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
const CLIENT_ID = '9478c90e57ef3d546ef0';
|
||||
const REDIRECT_URI = 'https://meteorite.surge.sh/login';
|
||||
const SCOPES = 'notifications';
|
||||
|
||||
const AuthenticationButton = props => (
|
||||
<a
|
||||
href={`https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&scope=${SCOPES}&redirect_uri=${REDIRECT_URI}`}
|
||||
href={`https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&scope=${SCOPES}`}
|
||||
{...props}
|
||||
>
|
||||
Authorize with GitHub
|
||||
|
@ -6,6 +6,8 @@ export const Reasons = {
|
||||
AUTHOR: 'author',
|
||||
OTHER: 'other',
|
||||
COMMENT: 'comment',
|
||||
STATE_CHANGE: 'state_change',
|
||||
TEAM_MENTION: 'team_mention'
|
||||
};
|
||||
|
||||
export const Badges = {
|
||||
|
BIN
src/images/icon.png
Normal file
BIN
src/images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
BIN
src/images/screenshot.png
Normal file
BIN
src/images/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 401 KiB |
@ -5,6 +5,7 @@ import { routes } from '../../constants';
|
||||
import Curve from '../../components/Curve';
|
||||
import Icon from '../../components/Icon';
|
||||
import Logo from '../../components/Logo';
|
||||
import screenshot from '../../images/screenshot.png';
|
||||
import '../../styles/gradient.css';
|
||||
|
||||
function createImagePlaceholder (highlight) {
|
||||
@ -326,13 +327,42 @@ function createImagePlaceholder (highlight) {
|
||||
);
|
||||
}
|
||||
|
||||
const ImageContainer = styled('div')({
|
||||
position: 'absolute',
|
||||
height: 390,
|
||||
width: 685,
|
||||
top: 155,
|
||||
left: '50%',
|
||||
background: `url(${screenshot}) center center no-repeat`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundColor: '#fff',
|
||||
boxShadow: '0 2px 8px rgba(179, 179, 179, 0.25)',
|
||||
marginLeft: 100,
|
||||
borderRadius: 8,
|
||||
display: 'block',
|
||||
'@media (max-width: 1000px)': {
|
||||
display: 'none'
|
||||
}
|
||||
});
|
||||
|
||||
const WidthContainer = styled('div')({
|
||||
margin: '0 auto',
|
||||
maxWidth: 1500,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
'@media (max-width: 1400px)': {
|
||||
flexDirection: 'column'
|
||||
}
|
||||
});
|
||||
|
||||
const Section = styled('div')({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
minHeight: 300,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
flexDirection: 'column',
|
||||
margin: '28px auto 0',
|
||||
padding: '60px 0'
|
||||
}, ({alt}) => alt && ({
|
||||
@ -341,10 +371,12 @@ const Section = styled('div')({
|
||||
color: '#fff'
|
||||
},
|
||||
'h2': {
|
||||
color: '#fff'
|
||||
},
|
||||
'@media (max-width: 1000px)': {
|
||||
flexDirection: 'column'
|
||||
color: '#fff',
|
||||
marginTop: 0,
|
||||
marginLeft: 15,
|
||||
fontSize: 42,
|
||||
textAlign: 'left',
|
||||
fontWeight: 600
|
||||
}
|
||||
}));
|
||||
|
||||
@ -393,8 +425,9 @@ const ImagePlaceholder = styled('div')({
|
||||
const Header = styled('h1')({
|
||||
color: '#fff',
|
||||
padding: '0 20px',
|
||||
margin: '0 auto 48px',
|
||||
letterSpacing: '-1.0px'
|
||||
margin: '0 0 24px',
|
||||
letterSpacing: '-1.0px',
|
||||
width: '50%',
|
||||
});
|
||||
|
||||
const SubHeader = styled(Header)({
|
||||
@ -408,19 +441,32 @@ const SubHeader = styled(Header)({
|
||||
|
||||
const LandingHeader = styled('div')({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
margin: '54px 20px 78px',
|
||||
maxWidth: 1000,
|
||||
width: '90%',
|
||||
margin: '22px 20px 54px',
|
||||
maxWidth: 1500,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
|
||||
const LandingMessage = styled(LandingHeader)({
|
||||
marginLeft: '5%',
|
||||
flexDirection: 'column',
|
||||
textAlign: 'center',
|
||||
maxWidth: 1000,
|
||||
textAlign: 'left',
|
||||
maxWidth: 1500,
|
||||
'h1': {
|
||||
display: 'block'
|
||||
},
|
||||
'@media (max-width: 1000px)': {
|
||||
textAlign: 'center',
|
||||
'h1': {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
width: 500
|
||||
},
|
||||
'div': {
|
||||
marginLeft: 'auto !important',
|
||||
marginRight: 'auto !important',
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@ -443,16 +489,24 @@ const SmallText = styled('span')({
|
||||
const BottomLinkContainer = styled(LandingHeader)({
|
||||
maxWidth: 350,
|
||||
width: '100%',
|
||||
margin: '32px auto 0',
|
||||
margin: '32px 20px 0',
|
||||
});
|
||||
|
||||
const LinkButton = styled('a')({});
|
||||
const U = styled('span')({
|
||||
color: 'inherit',});
|
||||
// background: '#009cfb',
|
||||
// padding: '0 6px 2px',
|
||||
// borderRadius: 4,
|
||||
// }, ({color}) => ({
|
||||
// background: color
|
||||
// }));
|
||||
|
||||
const UnofficialReleaseTag = styled('span')({
|
||||
color: 'white',
|
||||
position: 'absolute',
|
||||
left: '44px',
|
||||
bottom: '7px',
|
||||
bottom: '9px',
|
||||
fontSize: '11px',
|
||||
background: '#f42839',
|
||||
fontWeight: '800',
|
||||
@ -471,27 +525,61 @@ export default function Scene ({loggedIn, onLogout, ...props}) {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden'
|
||||
overflow: 'hidden',
|
||||
paddingBottom: 50
|
||||
}}>
|
||||
<LandingHeader>
|
||||
<Logo size={75} />
|
||||
<UnofficialReleaseTag>beta</UnofficialReleaseTag>
|
||||
<LandingHeader style={{paddingLeft: '5%'}}>
|
||||
<Logo size={75}>
|
||||
<UnofficialReleaseTag>beta</UnofficialReleaseTag>
|
||||
</Logo>
|
||||
{loggedIn ? (
|
||||
<div className="button-container">
|
||||
<RouterLink style={{marginRight: 15}} to={routes.NOTIFICATIONS}>notifications</RouterLink>
|
||||
<LinkButton style={{marginRight: 15}} href="#" onClick={onLogout}>sign out</LinkButton>
|
||||
<div className="button-container-alt">
|
||||
<RouterLink
|
||||
style={{
|
||||
marginRight: 15,
|
||||
color: '#fff',
|
||||
background: 'none'
|
||||
}} to={routes.NOTIFICATIONS}>notifications</RouterLink>
|
||||
<LinkButton
|
||||
style={{
|
||||
marginRight: 15,
|
||||
color: '#fff',
|
||||
background: 'none'
|
||||
}} href="#" onClick={onLogout}>sign out</LinkButton>
|
||||
</div>
|
||||
) : (
|
||||
<div className="button-container">
|
||||
<RouterLink style={{marginRight: 15}} to={routes.LOGIN}>sign in</RouterLink>
|
||||
<div className="button-container-alt">
|
||||
<RouterLink
|
||||
style={{
|
||||
marginRight: 15,
|
||||
color: '#fff',
|
||||
background: 'none'
|
||||
}} to={routes.LOGIN}>sign in</RouterLink>
|
||||
</div>
|
||||
)}
|
||||
</LandingHeader>
|
||||
<LandingMessage>
|
||||
<Header>Control your GitHub notifications</Header>
|
||||
<SubHeader>Prioritize the tasks that keep you and your team most productive</SubHeader>
|
||||
<div className="button-container">
|
||||
<div className="button-container-alt" style={{marginLeft: 20}}>
|
||||
<RouterLink to={routes.LOGIN}>let's get started</RouterLink>
|
||||
<LinkButton
|
||||
onClick={() => {
|
||||
const section = document.querySelector('#learnMore');
|
||||
const y = section.getBoundingClientRect().top + window.scrollY;
|
||||
window.scroll({
|
||||
top: y,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
marginLeft: 20,
|
||||
color: '#fff',
|
||||
background: 'none'
|
||||
}}>
|
||||
learn more
|
||||
<Icon.LeftArrow shrink={0.6} style={{marginLeft: 5, filter: 'invert(1)', transform: 'rotateY(180deg)'}} />
|
||||
</LinkButton>
|
||||
</div>
|
||||
<BottomLinkContainer>
|
||||
<SmallLink target="_blank" href="https://github.com/nickzuber/meteorite">View and contribute on GitHub</SmallLink>
|
||||
@ -508,31 +596,34 @@ export default function Scene ({loggedIn, onLogout, ...props}) {
|
||||
</SmallText>
|
||||
</BottomLinkContainer>
|
||||
</LandingMessage>
|
||||
<ImageContainer />
|
||||
<Curve />
|
||||
</div>
|
||||
<Section id="section">
|
||||
<Item style={{flex: '0 0 2.5%', padding: 0}} />
|
||||
<Item>
|
||||
{createImagePlaceholder('badges')}
|
||||
</Item>
|
||||
<Item id="item-text">
|
||||
<h2>Surface the things that matter the most.</h2>
|
||||
<ItemText>
|
||||
<Icon.Ring />
|
||||
<p>The most important issues and pull requests that require your presence are called out and brought to your attention.</p>
|
||||
</ItemText>
|
||||
<ItemText>
|
||||
<Icon.Ear />
|
||||
<p>We listen for updates with your notifications and let you know <i>why</i> and <i>when</i> things change.</p>
|
||||
</ItemText>
|
||||
<ItemText>
|
||||
<Icon.Zap />
|
||||
<p>Super charge your day by focusing on getting things done, rather than sifting through notifications or emails.</p>
|
||||
</ItemText>
|
||||
</Item>
|
||||
<Item style={{flex: '0 0 2.5%', padding: 0}} />
|
||||
<Section className="section">
|
||||
<WidthContainer>
|
||||
<Item style={{flex: '0 0 2.5%', padding: 0}} />
|
||||
<Item>
|
||||
{createImagePlaceholder('badges')}
|
||||
</Item>
|
||||
<Item className="item-text">
|
||||
<h2>Surface the things that matter the most.</h2>
|
||||
<ItemText>
|
||||
<Icon.Ring />
|
||||
<p>The most important issues and pull requests that require your presence are called out and brought to your attention.</p>
|
||||
</ItemText>
|
||||
<ItemText>
|
||||
<Icon.Ear />
|
||||
<p>We listen for updates with your notifications and let you know <i>why</i> and <i>when</i> things change.</p>
|
||||
</ItemText>
|
||||
<ItemText>
|
||||
<Icon.Zap />
|
||||
<p>Super charge your day by focusing on getting things done, rather than sifting through notifications or emails.</p>
|
||||
</ItemText>
|
||||
</Item>
|
||||
<Item style={{flex: '0 0 2.5%', padding: 0}} />
|
||||
</WidthContainer>
|
||||
</Section>
|
||||
<Section id="section" alt={true} style={{paddingTop: 140, overflowX: 'hidden'}}>
|
||||
<Section className="section" alt={true} style={{paddingTop: 140, overflowX: 'hidden'}}>
|
||||
<Curve style={{
|
||||
bottom: 'auto',
|
||||
marginBottom: 0,
|
||||
@ -540,26 +631,63 @@ export default function Scene ({loggedIn, onLogout, ...props}) {
|
||||
top: 0,
|
||||
transform: 'translateX(-50%) rotate(180deg)'
|
||||
}} />
|
||||
<Item style={{flex: '0 0 2.5%', padding: 0}} />
|
||||
<Item id="item-text">
|
||||
<h2>Your time matters, so<br />we keep things simple.</h2>
|
||||
<ItemText>
|
||||
<Icon.CloudOffWhite />
|
||||
<p>All of the information we use to make your notifications more useful is kept offline and kept on your own computer.</p>
|
||||
</ItemText>
|
||||
<ItemText>
|
||||
<Icon.NoPhone />
|
||||
<p>Simply sign in and start working — no complicated or intrusive set up needed.</p>
|
||||
</ItemText>
|
||||
<ItemText>
|
||||
<Icon.NoMusic />
|
||||
<p>No distractions — we only show you updates on things that matter to you.</p>
|
||||
</ItemText>
|
||||
</Item>
|
||||
<Item>
|
||||
{createImagePlaceholder('reason')}
|
||||
</Item>
|
||||
<Item style={{flex: '0 0 2.5%', padding: 0}} />
|
||||
<WidthContainer>
|
||||
<Item style={{flex: '0 0 2.5%', padding: 0}} />
|
||||
<Item className="item-text">
|
||||
<h2>Your time matters, so<br />we keep things simple.</h2>
|
||||
<ItemText>
|
||||
<Icon.CloudOffWhite />
|
||||
<p>All of the information we use to make your notifications more useful is kept offline and kept on your own computer.</p>
|
||||
</ItemText>
|
||||
<ItemText>
|
||||
<Icon.NoPhone />
|
||||
<p>Simply sign in and start working — no complicated or intrusive set up needed.</p>
|
||||
</ItemText>
|
||||
<ItemText>
|
||||
<Icon.NoMusic />
|
||||
<p>No distractions — we only show you updates on things that matter to you.</p>
|
||||
</ItemText>
|
||||
</Item>
|
||||
<Item>
|
||||
{createImagePlaceholder('reason')}
|
||||
</Item>
|
||||
<Item style={{flex: '0 0 2.5%', padding: 0}} />
|
||||
</WidthContainer>
|
||||
</Section>
|
||||
<Section id="learnMore" className="section" alt={true} style={{marginTop: 0}}>
|
||||
<h2 style={{textAlign: 'center', maxWidth: 900, color: '#fff'}}>
|
||||
Meteorite is the assistant for your <br />GitHub notifications.
|
||||
</h2>
|
||||
<WidthContainer>
|
||||
<Item className="item-text">
|
||||
<ItemText>
|
||||
<Icon.CloudOffWhite />
|
||||
<p>All of the information we use to make your notifications more useful is kept offline and kept on your own computer.</p>
|
||||
</ItemText>
|
||||
<ItemText>
|
||||
<Icon.NoPhone />
|
||||
<p>Simply sign in and start working — no complicated or intrusive set up needed.</p>
|
||||
</ItemText>
|
||||
<ItemText>
|
||||
<Icon.NoMusic />
|
||||
<p>No distractions — we only show you updates on things that matter to you.</p>
|
||||
</ItemText>
|
||||
</Item>
|
||||
<Item className="item-text">
|
||||
<ItemText>
|
||||
<Icon.CloudOffWhite />
|
||||
<p>All of the information we use to make your notifications more useful is kept offline and kept on your own computer.</p>
|
||||
</ItemText>
|
||||
<ItemText>
|
||||
<Icon.NoPhone />
|
||||
<p>Simply sign in and start working — no complicated or intrusive set up needed.</p>
|
||||
</ItemText>
|
||||
<ItemText>
|
||||
<Icon.NoMusic />
|
||||
<p>No distractions — we only show you updates on things that matter to you.</p>
|
||||
</ItemText>
|
||||
</Item>
|
||||
</WidthContainer>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
|
@ -16,18 +16,29 @@ import '../../styles/gradient.css';
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
/* eslint-disable no-script-url */
|
||||
|
||||
function getMessageFromReasons (reasons) {
|
||||
function stringOfType (type) {
|
||||
switch (type) {
|
||||
case 'PullRequest':
|
||||
return 'pull request';
|
||||
case 'Issue':
|
||||
return 'issue';
|
||||
default:
|
||||
return 'task';
|
||||
}
|
||||
}
|
||||
|
||||
function getMessageFromReasons (reasons, type) {
|
||||
switch (reasons[reasons.length - 1].reason) {
|
||||
case Reasons.ASSIGN:
|
||||
return 'You were just assigned a task';
|
||||
return 'You were assigned this ' + stringOfType(type);
|
||||
case Reasons.AUTHOR:
|
||||
return 'There was activity on a thread you created';
|
||||
return 'There was activity on this thread you created';
|
||||
case Reasons.COMMENT:
|
||||
return 'Somebody just left a comment';
|
||||
return 'Somebody left a comment';
|
||||
case Reasons.MENTION:
|
||||
return 'You were just @mentioned';
|
||||
return 'You were @mentioned';
|
||||
case Reasons.REVIEW_REQUESTED:
|
||||
return 'Your review was just requested';
|
||||
return 'Your review was requested for this ' + stringOfType(type);
|
||||
case Reasons.SUBSCRIBED:
|
||||
return 'There was an update and you\'re subscribed';
|
||||
case Reasons.OTHER:
|
||||
@ -86,11 +97,9 @@ const InlineBlockContainer = styled('div')({
|
||||
|
||||
const NotificationsContainer = styled('div')({
|
||||
position: 'relative',
|
||||
background: '#fff',
|
||||
margin: '0 auto',
|
||||
padding: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
overflowX: 'hidden',
|
||||
@ -117,7 +126,6 @@ const GeneralOptionsContainer = styled(NavigationContainer)({
|
||||
minHeight: 60,
|
||||
width: '95%',
|
||||
margin: 0,
|
||||
background: '#fff',
|
||||
padding: '8px 16px',
|
||||
paddingTop: 18,
|
||||
flex: '0 0 50px',
|
||||
@ -350,7 +358,6 @@ const NotificationRow = styled('tr')({
|
||||
width: '100%',
|
||||
borderRadius: 4,
|
||||
margin: '0 auto',
|
||||
background: '#fff',
|
||||
padding: '8px 16px',
|
||||
transition: 'all 0.1s ease-in-out',
|
||||
boxSizing: 'border-box',
|
||||
@ -468,6 +475,9 @@ function getPRIssueIcon (type, reasons) {
|
||||
}
|
||||
|
||||
export default function Scene ({
|
||||
currentTime,
|
||||
isFirstTimeUser,
|
||||
notificationsPermission,
|
||||
queuedCount,
|
||||
stagedCount,
|
||||
closedCount,
|
||||
@ -496,12 +506,23 @@ export default function Scene ({
|
||||
fetchingNotificationsError,
|
||||
activeFilter,
|
||||
onSetActiveFilter,
|
||||
setNotificationsPermission
|
||||
}) {
|
||||
const loading = isSearching || isFetchingNotifications;
|
||||
const isFirstPage = page === 1;
|
||||
const isLastPage = page === lastPage;
|
||||
|
||||
const NotificationsIcon = notificationsPermission === 'granted'
|
||||
? Icon.NotificationsOn
|
||||
: Icon.NotificationsOff;
|
||||
|
||||
console.log('notifications', notifications)
|
||||
console.log('isFirstTimeUser', isFirstTimeUser)
|
||||
console.log('notificationsPermission', notificationsPermission)
|
||||
|
||||
if (isFirstTimeUser && notifications.length > 5) {
|
||||
// alert('hello, clear ur shit');
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{marginTop: 60}}>
|
||||
@ -578,14 +599,14 @@ export default function Scene ({
|
||||
marginRight: '5px',
|
||||
top: '-3px',
|
||||
}} />
|
||||
{moment().format('h:mma')}
|
||||
{currentTime.format('h:mma')}
|
||||
</h3>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
padding: '6px 0px',
|
||||
fontSize: 15,
|
||||
opacity: 0.7,
|
||||
}}>{moment().format('dddd, MMMM Do')}</span>
|
||||
}}>{currentTime.format('dddd, MMMM Do')}</span>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
padding: '6px 0 8px',
|
||||
@ -701,6 +722,23 @@ export default function Scene ({
|
||||
}) : undefined}
|
||||
/>
|
||||
</EnhancedTab>
|
||||
<EnhancedTab tooltip={!loading ? "Toggle web notifications" : null} disabled={loading}>
|
||||
<NotificationsIcon
|
||||
opacity={0.9}
|
||||
onClick={!loading ? (() => {
|
||||
switch(notificationsPermission) {
|
||||
case 'granted':
|
||||
return setNotificationsPermission('denied');
|
||||
case 'denied':
|
||||
case 'default':
|
||||
default:
|
||||
Notification.requestPermission().then(result => {
|
||||
return setNotificationsPermission(result);
|
||||
});
|
||||
}
|
||||
}) : undefined}
|
||||
/>
|
||||
</EnhancedTab>
|
||||
{query ? (
|
||||
<React.Fragment>
|
||||
<div style={{display: 'inline-block'}} className="button-container-alt">
|
||||
@ -856,7 +894,7 @@ export default function Scene ({
|
||||
}}
|
||||
shrink={.5}
|
||||
/>
|
||||
{getMessageFromReasons(n.reasons)}
|
||||
{getMessageFromReasons(n.reasons, n.type)}
|
||||
</ReasonMessage>
|
||||
</TableItem>
|
||||
<TableItem width={100}>
|
||||
|
@ -13,7 +13,7 @@ import { Status } from '../../constants/status';
|
||||
import { Reasons, Badges } from '../../constants/reasons';
|
||||
import Scene from './Scene';
|
||||
|
||||
const PER_PAGE = 15;
|
||||
const PER_PAGE = 10;
|
||||
|
||||
// @TODO Move these functions.
|
||||
|
||||
@ -64,7 +64,6 @@ function scoreOf (notification) {
|
||||
return score;
|
||||
};
|
||||
|
||||
// @TODO implement this
|
||||
function badgesOf (notification) {
|
||||
const badges = [];
|
||||
const len = notification.reasons.length;
|
||||
@ -96,13 +95,15 @@ function badgesOf (notification) {
|
||||
};
|
||||
|
||||
const scoreOfReason = {
|
||||
[Reasons.ASSIGN]: 18,
|
||||
[Reasons.AUTHOR]: 10,
|
||||
[Reasons.MENTION]: 12,
|
||||
[Reasons.OTHER]: 2,
|
||||
[Reasons.REVIEW_REQUESTED]: 30,
|
||||
[Reasons.ASSIGN]: 21,
|
||||
[Reasons.AUTHOR]: 11,
|
||||
[Reasons.MENTION]: 17,
|
||||
[Reasons.TEAM_MENTION]: 11,
|
||||
[Reasons.OTHER]: 4,
|
||||
[Reasons.REVIEW_REQUESTED]: 29,
|
||||
[Reasons.SUBSCRIBED]: 3,
|
||||
[Reasons.COMMENT]: 6,
|
||||
[Reasons.STATE_CHANGE]: 5,
|
||||
};
|
||||
|
||||
const decorateWithScore = notification => ({
|
||||
@ -113,6 +114,9 @@ const decorateWithScore = notification => ({
|
||||
|
||||
class NotificationsPage extends React.Component {
|
||||
state = {
|
||||
currentTime: moment(),
|
||||
prevNotifications: [],
|
||||
isFirstTimeUser: false,
|
||||
isSearching: false,
|
||||
query: null,
|
||||
activeFilter: Filters.PARTICIPATING,
|
||||
@ -121,10 +125,17 @@ class NotificationsPage extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.props.notificationsApi.fetchNotifications();
|
||||
const isFirstTimeUser = !this.props.storageApi.getUserItem('hasOnboarded');
|
||||
|
||||
if (isFirstTimeUser) {
|
||||
this.setState({isFirstTimeUser: true});
|
||||
// this.props.storageApi.setUserItem('hasOnboarded', true);
|
||||
}
|
||||
|
||||
this.props.notificationsApi.fetchNotifications();
|
||||
this.syncer = setInterval(() => {
|
||||
this.props.notificationsApi.fetchNotificationsSync();
|
||||
this.setState({currentTime: moment()});
|
||||
}, 8 * 1000);
|
||||
}
|
||||
|
||||
@ -132,6 +143,10 @@ class NotificationsPage extends React.Component {
|
||||
clearInterval(this.syncer);
|
||||
}
|
||||
|
||||
diffForWebNotification (nextProps, nextState) {
|
||||
this.sendWebNotification();
|
||||
}
|
||||
|
||||
onChangePage = page => {
|
||||
this.setState({ currentPage: page });
|
||||
}
|
||||
@ -181,6 +196,16 @@ class NotificationsPage extends React.Component {
|
||||
this.props.notificationsApi.restoreThread(thread_id);
|
||||
}
|
||||
|
||||
setNotificationsPermission = (...args) => {
|
||||
this.props.notificationsApi.setNotificationsPermission(...args);
|
||||
}
|
||||
|
||||
sendWebNotification = () => {
|
||||
var img = '../images/icon.png';
|
||||
var text = 'HEY! Your task "null" is now overdue.';
|
||||
var notification = new Notification('Meteorite', { body: text, icon: img });
|
||||
}
|
||||
|
||||
render () {
|
||||
if (!this.props.authApi.token) {
|
||||
return <Redirect noThrow to={routes.LOGIN} />
|
||||
@ -191,37 +216,40 @@ class NotificationsPage extends React.Component {
|
||||
markAsRead,
|
||||
markAllAsStaged,
|
||||
clearCache,
|
||||
notificationsPermission,
|
||||
notifications,
|
||||
loading: isFetchingNotifications,
|
||||
error: fetchingNotificationsError,
|
||||
} = this.props.notificationsApi;
|
||||
|
||||
// @TODO Move all this out of the render method.
|
||||
// nick, do this ^ so you can fire off a web noti when the filtered/final
|
||||
// notifications have a diff
|
||||
let filterMethod = () => true;
|
||||
switch (this.state.activeFilter) {
|
||||
case Filters.PARTICIPATING:
|
||||
filterMethod = n => (
|
||||
n.reasons.some(({ reason }) => (
|
||||
reason === 'review_requested' ||
|
||||
reason === 'assign' ||
|
||||
reason === 'mention' ||
|
||||
reason === 'author'
|
||||
reason === Reasons.REVIEW_REQUESTED ||
|
||||
reason === Reasons.ASSIGN ||
|
||||
reason === Reasons.MENTION ||
|
||||
reason === Reasons.AUTHOR
|
||||
))
|
||||
);
|
||||
break;
|
||||
case Filters.ASSIGNED:
|
||||
filterMethod = n => (
|
||||
n.reasons.some(({ reason }) => reason === 'assign')
|
||||
n.reasons.some(({ reason }) => reason === Reasons.ASSIGN)
|
||||
);
|
||||
break;
|
||||
case Filters.REVIEW_REQUESTED:
|
||||
filterMethod = n => (
|
||||
n.reasons.some(({ reason }) => reason === 'review_requested')
|
||||
n.reasons.some(({ reason }) => reason === Reasons.REVIEW_REQUESTED)
|
||||
);
|
||||
break;
|
||||
case Filters.COMMENT:
|
||||
filterMethod = n => (
|
||||
n.reasons.some(({ reason }) => reason === 'comment')
|
||||
n.reasons.some(({ reason }) => reason === Reasons.COMMENT)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
@ -279,6 +307,10 @@ class NotificationsPage extends React.Component {
|
||||
|
||||
return (
|
||||
<Scene
|
||||
currentTime={this.state.currentTime}
|
||||
isFirstTimeUser={this.state.isFirstTimeUser}
|
||||
setNotificationsPermission={this.setNotificationsPermission}
|
||||
notificationsPermission={notificationsPermission}
|
||||
queuedCount={notificationsQueued.length}
|
||||
stagedCount={notificationsStaged.length}
|
||||
closedCount={notificationsClosed.length}
|
||||
|
@ -59,8 +59,12 @@ class NotificationsProvider extends React.Component {
|
||||
}
|
||||
|
||||
state = {
|
||||
syncing: false,
|
||||
loading: false,
|
||||
error: null
|
||||
error: null,
|
||||
notificationsPermission:
|
||||
this.props.getUserItem('notificationsPermission') ||
|
||||
'default',
|
||||
}
|
||||
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
@ -78,6 +82,16 @@ class NotificationsProvider extends React.Component {
|
||||
return this.props.notifications !== nextProps.notifications;
|
||||
}
|
||||
|
||||
// The web notificaitons API doesn't let users revoke notifications permission
|
||||
// after they already grant it, for a reason I can only assume that was pure evil.
|
||||
// So, if a user wants to stop getting notifications we set that in their local
|
||||
// storage, leaning towards it being revoked.
|
||||
setNotificationsPermission = permission => {
|
||||
this.setState({notificationsPermission: permission});
|
||||
this.props.setUserItem('notificationsPermission', permission);
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
requestPage = (page = 1, optimizePolling = true) => {
|
||||
const headers = {
|
||||
'Authorization': `token ${this.props.token}`,
|
||||
@ -107,16 +121,23 @@ class NotificationsProvider extends React.Component {
|
||||
}
|
||||
|
||||
requestFetchNotifications = (page = 1, optimizePolling = true) => {
|
||||
if (this.state.syncing) {
|
||||
// Don't try to send off another request if we're already trying to get one.
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
this.setState({syncing: true});
|
||||
return this.requestPage(page, optimizePolling)
|
||||
.then(({headers, json}) => {
|
||||
if (json === null) return;
|
||||
if (json === null) return [];
|
||||
let nextPage = null;
|
||||
const links = headers['link'];
|
||||
if (links && links.next && links.next.page) {
|
||||
nextPage = links.next.page;
|
||||
}
|
||||
return this.processNotificationsChunk(nextPage, json);
|
||||
});
|
||||
})
|
||||
.finally(() => this.setState({syncing: false}));
|
||||
}
|
||||
|
||||
fetchNotifications = (page = 1, optimizePolling = true) => {
|
||||
@ -125,9 +146,15 @@ class NotificationsProvider extends React.Component {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.state.loading) {
|
||||
// Don't try to fetch if we're already fetching
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
this.setState({ loading: true });
|
||||
return this.requestFetchNotifications(page, optimizePolling)
|
||||
.catch(error => this.setState({ error }))
|
||||
.then(() => this.setState({error: null}))
|
||||
.catch(error =>this.setState({error}))
|
||||
.finally(() => this.setState({ loading: false }));
|
||||
}
|
||||
|
||||
@ -199,9 +226,15 @@ class NotificationsProvider extends React.Component {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.state.loading) {
|
||||
// Don't try to fetch if we're already fetching
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
this.setState({ loading: true });
|
||||
return this.requestMarkAsRead(thread_id)
|
||||
.catch(error => this.setState({ error }))
|
||||
.then(() => this.setState({error: null}))
|
||||
.catch(error => this.setState({error}))
|
||||
.finally(() => this.setState({ loading: false }));
|
||||
}
|
||||
|
||||
@ -217,7 +250,8 @@ class NotificationsProvider extends React.Component {
|
||||
clearCache = () => {
|
||||
this.setState({ loading: true });
|
||||
return this.requestClearCache()
|
||||
.catch(error => this.setState({ error }))
|
||||
.then(() => this.setState({error: null}))
|
||||
.catch(error => this.setState({error}))
|
||||
.finally(() => this.setState({ loading: false }));
|
||||
}
|
||||
|
||||
@ -278,21 +312,24 @@ class NotificationsProvider extends React.Component {
|
||||
markAllAsStaged = () => {
|
||||
this.setState({ loading: true });
|
||||
return this.requestStageAll()
|
||||
.catch(error => this.setState({ error }))
|
||||
.then(() => this.setState({error: null}))
|
||||
.catch(error => this.setState({error}))
|
||||
.finally(() => this.setState({ loading: false }));
|
||||
}
|
||||
|
||||
stageThread = thread_id => {
|
||||
this.setState({ loading: true });
|
||||
return this.requestStageThread(thread_id)
|
||||
.catch(error => this.setState({ error }))
|
||||
.then(() => this.setState({error: null}))
|
||||
.catch(error => this.setState({error}))
|
||||
.finally(() => this.setState({ loading: false }));
|
||||
}
|
||||
|
||||
restoreThread = thread_id => {
|
||||
this.setState({ loading: true });
|
||||
return this.requestRestoreThread(thread_id)
|
||||
.catch(error => this.setState({ error }))
|
||||
.then(() => this.setState({error: null}))
|
||||
.catch(error => this.setState({error}))
|
||||
.finally(() => this.setState({ loading: false }));
|
||||
}
|
||||
|
||||
@ -337,6 +374,7 @@ class NotificationsProvider extends React.Component {
|
||||
clearCache: this.clearCache,
|
||||
stageThread: this.stageThread,
|
||||
restoreThread: this.restoreThread,
|
||||
setNotificationsPermission: this.setNotificationsPermission,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -350,6 +388,8 @@ const withNotificationsProvider = WrappedComponent => props => (
|
||||
notifications,
|
||||
getItem,
|
||||
setItem,
|
||||
getUserItem,
|
||||
setUserItem,
|
||||
clearCache,
|
||||
removeItem
|
||||
}) => (
|
||||
@ -358,6 +398,8 @@ const withNotificationsProvider = WrappedComponent => props => (
|
||||
notifications={notifications}
|
||||
getItemFromStorage={getItem}
|
||||
setItemInStorage={setItem}
|
||||
getUserItem={getUserItem}
|
||||
setUserItem={setUserItem}
|
||||
clearStorageCache={clearCache}
|
||||
removeItemFromStorage={removeItem}
|
||||
token={token}
|
||||
|
@ -1,38 +1,14 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import {Status} from '../constants/status';
|
||||
import {Reasons} from '../constants/reasons';
|
||||
import {createMockNotifications} from '../utils/mocks';
|
||||
|
||||
const mockNotifications = createMockNotifications(100);
|
||||
|
||||
export const LOCAL_STORAGE_PREFIX = '__meteorite_noti_cache__';
|
||||
export const LOCAL_STORAGE_USER_PREFIX = '__meteorite_user_cache__';
|
||||
export const LOCAL_STORAGE_STATISTIC_PREFIX = '__meteorite_statistic_cache__';
|
||||
|
||||
const getMockReasons = n => {
|
||||
const reasons = Object.values(Reasons);
|
||||
const len = reasons.length;
|
||||
return new Array(n).fill(0).map(_ => ({
|
||||
reason: reasons[Math.floor(Math.random() * len)],
|
||||
time: moment().format()
|
||||
}));
|
||||
};
|
||||
|
||||
const getMockNotification = randomNumber => ({
|
||||
id: randomNumber,
|
||||
updated_at: moment().format(),
|
||||
status: (randomNumber > 0.8 ? Status.STAGED : Status.QUEUED),
|
||||
reasons: getMockReasons(Math.ceil(randomNumber * 10)),
|
||||
type: ['Issue', 'PullRequest'][Math.floor(randomNumber * 2)],
|
||||
name: 'Mock - Fake notification name',
|
||||
url: 'https://github.com/test/repo/pull',
|
||||
repository: 'test/mock',
|
||||
number: Math.ceil(randomNumber * 1000),
|
||||
repositoryUrl: 'https://github.com/test/repo',
|
||||
});
|
||||
|
||||
const mockNotifications = new Array(1000);
|
||||
for (let i = 0; i < mockNotifications.length; i++) {
|
||||
mockNotifications[i] = getMockNotification(Math.random());
|
||||
}
|
||||
|
||||
class StorageProvider extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
@ -142,10 +118,22 @@ class StorageProvider extends React.Component {
|
||||
try {
|
||||
return JSON.parse(window.localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${id}`));
|
||||
} catch (e) {
|
||||
return null;
|
||||
return window.localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
getUserItem = id => {
|
||||
try {
|
||||
return JSON.parse(window.localStorage.getItem(`${LOCAL_STORAGE_USER_PREFIX}${id}`));
|
||||
} catch (e) {
|
||||
return window.localStorage.getItem(`${LOCAL_STORAGE_USER_PREFIX}${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
setUserItem = (id, value) => {
|
||||
window.localStorage.setItem(`${LOCAL_STORAGE_USER_PREFIX}${id}`, JSON.stringify(value));
|
||||
}
|
||||
|
||||
removeItem = id => {
|
||||
// We never really want to purge anything from the cache if we can help it,
|
||||
// since there's always a chance that a read notification can be resurrected.
|
||||
@ -169,6 +157,8 @@ class StorageProvider extends React.Component {
|
||||
...this.state,
|
||||
setItem: this.setItem,
|
||||
getItem: this.getItem,
|
||||
getUserItem: this.getUserItem,
|
||||
setUserItem: this.setUserItem,
|
||||
removeItem: this.removeItem,
|
||||
clearCache: this.clearCache,
|
||||
refreshNotifications: this.refreshNotifications,
|
||||
|
@ -11,6 +11,7 @@
|
||||
}
|
||||
|
||||
.button-container a {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08);
|
||||
margin: 0 auto;
|
||||
@ -38,13 +39,25 @@
|
||||
display: -ms-inline-flexbox;
|
||||
display: inline-flex;
|
||||
}
|
||||
.button-container a:hover {
|
||||
background: #f9f9f9;
|
||||
box-shadow: 0 2px 6px #4a4a4a5c;
|
||||
|
||||
.button-container a:before {
|
||||
content: "";
|
||||
transition: all 150ms ease;
|
||||
background: rgba(190, 197, 208, 0.25);
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
.button-container a:active {
|
||||
background: #e4e4e4;
|
||||
box-shadow: 0 0 0 #4a4a4a5c;
|
||||
.button-container a:hover:before {
|
||||
transform: scale(1);
|
||||
}
|
||||
.button-container a:active:before {
|
||||
background: rgba(190, 197, 208, 0.5)
|
||||
}
|
||||
|
||||
.button-container-alt a {
|
||||
@ -114,10 +127,10 @@
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1400px) {
|
||||
#section {
|
||||
.section {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
#item-text {
|
||||
.item-text {
|
||||
width: 600px;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user