Everything
BIN
public/icon-black.png
Normal file
After Width: | Height: | Size: 110 KiB |
@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/icon-purple.png">
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/icon-black.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta name="theme-color" content="#24292e">
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<title>Meteorite — Smarter GitHub notifications</title>
|
||||
</head>
|
||||
|
@ -7,19 +7,23 @@ import back from './svg/back.svg';
|
||||
import bolt from './svg/bolt.svg';
|
||||
import boltWhite from './svg/bolt-white.svg';
|
||||
import bookmarkAlt from './svg/bookmark-alt.svg';
|
||||
import bookmarkAltWhite from './svg/bookmark-alt-white.svg';
|
||||
import bookmark from './svg/bookmark.svg';
|
||||
import bookmarks from './svg/bookmarks.svg';
|
||||
import check from './svg/check.svg';
|
||||
import convo from './svg/convo.svg';
|
||||
import doneAll from './svg/done-all.svg';
|
||||
import done from './svg/done.svg';
|
||||
import hot from './svg/hot.svg';
|
||||
import inbox from './svg/inbox.svg';
|
||||
import inboxWhite from './svg/inbox-white.svg';
|
||||
import locked from './svg/locked.svg';
|
||||
import lowPriority from './svg/low_priority.svg';
|
||||
import menu from './svg/menu.svg';
|
||||
import next from './svg/next.svg';
|
||||
import people from './svg/people.svg';
|
||||
import peopleWhite from './svg/people-white.svg';
|
||||
import prev from './svg/prev.svg';
|
||||
import refresh from './svg/refresh.svg';
|
||||
import search from './svg/search.svg';
|
||||
import settings from './svg/settings.svg';
|
||||
@ -62,19 +66,23 @@ Icon.Back = createIcon(back);
|
||||
Icon.Bolt = createIcon(bolt);
|
||||
Icon.BoltWhite = createIcon(boltWhite);
|
||||
Icon.BookmarkAlt = createIcon(bookmarkAlt);
|
||||
Icon.BookmarkAltWhite = createIcon(bookmarkAltWhite);
|
||||
Icon.Bookmark = createIcon(bookmark);
|
||||
Icon.Bookmarks = createIcon(bookmarks);
|
||||
Icon.Check = createIcon(check);
|
||||
Icon.Convo = createIcon(convo);
|
||||
Icon.DoneAll = createIcon(doneAll);
|
||||
Icon.Done = createIcon(done);
|
||||
Icon.Hot = createIcon(hot);
|
||||
Icon.Inbox = createIcon(inbox);
|
||||
Icon.InboxWhite = createIcon(inboxWhite);
|
||||
Icon.Locked = createIcon(locked);
|
||||
Icon.LowPriority = createIcon(lowPriority);
|
||||
Icon.Menu = createIcon(menu);
|
||||
Icon.Next = createIcon(next);
|
||||
Icon.People = createIcon(people);
|
||||
Icon.PeopleWhite = createIcon(peopleWhite);
|
||||
Icon.Prev = createIcon(prev);
|
||||
Icon.Refresh = createIcon(refresh);
|
||||
Icon.Search = createIcon(search);
|
||||
Icon.Settings = createIcon(settings);
|
||||
|
@ -1 +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 fill="#f68700" d="M22 5.72l-4.6-3.86-1.29 1.53 4.6 3.86L22 5.72zM7.88 3.39L6.6 1.86 2 5.71l1.29 1.53 4.59-3.85zM12.5 8H11v6l4.75 2.85.75-1.23-4-2.37V8zM12 4c-4.97 0-9 4.03-9 9s4.02 9 9 9c4.97 0 9-4.03 9-9s-4.03-9-9-9zm0 16c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path fill="#00cd94" d="M22 5.72l-4.6-3.86-1.29 1.53 4.6 3.86L22 5.72zM7.88 3.39L6.6 1.86 2 5.71l1.29 1.53 4.59-3.85zM12.5 8H11v6l4.75 2.85.75-1.23-4-2.37V8zM12 4c-4.97 0-9 4.03-9 9s4.02 9 9 9c4.97 0 9-4.03 9-9s-4.03-9-9-9zm0 16c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z"/></svg>
|
||||
|
Before Width: | Height: | Size: 411 B After Width: | Height: | Size: 411 B |
1
src/components/Icon/svg/bookmark-alt-white.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="#fff" d="M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3 7 3V5c0-1.1-.9-2-2-2zm0 15l-5-2.18L7 18V5h10v13z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
After Width: | Height: | Size: 237 B |
1
src/components/Icon/svg/convo.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 fill="#00A0F5" d="M21 6h-2v9H6v2c0 .55.45 1 1 1h11l4 4V7c0-.55-.45-1-1-1zm-4 6V3c0-.55-.45-1-1-1H3c-.55 0-1 .45-1 1v14l4-4h10c.55 0 1-.45 1-1z"/></svg>
|
After Width: | Height: | Size: 278 B |
1
src/components/Icon/svg/low_priority.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="M14 5h8v2h-8zm0 5.5h8v2h-8zm0 5.5h8v2h-8zM2 11.5C2 15.08 4.92 18 8.5 18H9v2l3-3-3-3v2h-.5C6.02 16 4 13.98 4 11.5S6.02 7 8.5 7H12V5H8.5C4.92 5 2 7.92 2 11.5z"/><path fill="none" d="M0 0h24v24H0z"/></svg>
|
After Width: | Height: | Size: 294 B |
@ -1 +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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/><path fill="none" d="M0 0h24v24H0V0z"/></svg>
|
Before Width: | Height: | Size: 195 B After Width: | Height: | Size: 194 B |
1
src/components/Icon/svg/prev.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="M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41z"/><path fill="none" d="M0 0h24v24H0V0z"/></svg>
|
After Width: | Height: | Size: 196 B |
@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import loader from './loader.svg';
|
||||
import loaderAlt from './loader-alt.svg';
|
||||
|
||||
export default function LoadingIcon ({ style, size, ...props }) {
|
||||
export default function LoadingIcon ({ style, size, alt, ...props }) {
|
||||
return (
|
||||
<div style={{
|
||||
background: `url(${loader}) center center no-repeat`,
|
||||
background: `url(${(alt ? loaderAlt : loader)}) center center no-repeat`,
|
||||
position: 'relative',
|
||||
height: size || 100,
|
||||
width: size || 100,
|
||||
|
17
src/components/LoadingIcon/loader-alt.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
|
||||
<svg width="22" height="22" viewBox="-1 -1 22 22" xmlns="http://www.w3.org/2000/svg" stroke="#e4e4e4">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(1 1)" stroke-width="2.6">
|
||||
<circle stroke="none" stroke-opacity=".5" cx="9" cy="9" r="9"/>
|
||||
<path stroke="#24292e" d="M18,9 C18,4.03 13.97,0 9,0">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 9 9"
|
||||
to="360 9 9"
|
||||
dur=".5s"
|
||||
repeatCount="indefinite"/>
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 727 B |
@ -3,7 +3,7 @@
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(1 1)" stroke-width="2.6">
|
||||
<circle stroke-opacity=".5" cx="9" cy="9" r="9"/>
|
||||
<path stroke="#6772e5" d="M18,9 C18,4.03 13.97,0 9,0">
|
||||
<path stroke="#24292e" d="M18,9 C18,4.03 13.97,0 9,0">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
|
Before Width: | Height: | Size: 713 B After Width: | Height: | Size: 713 B |
@ -3,5 +3,5 @@ export const Filters = {
|
||||
REVIEW_REQUESTED: 'review_requested',
|
||||
PARTICIPATING: 'participating',
|
||||
SUBSCRIBED: 'subscribed',
|
||||
HOT: 'hot',
|
||||
COMMENT: 'comment'
|
||||
};
|
||||
|
15
src/constants/reasons.js
Normal file
@ -0,0 +1,15 @@
|
||||
export const Reasons = {
|
||||
MENTION: 'mention',
|
||||
ASSIGN: 'assign',
|
||||
REVIEW_REQUESTED: 'review_requested',
|
||||
SUBSCRIBED: 'subscribed',
|
||||
AUTHOR: 'author',
|
||||
OTHER: 'other',
|
||||
COMMENT: 'comment',
|
||||
};
|
||||
|
||||
export const Badges = {
|
||||
HOT: 'hot',
|
||||
COMMENTS: 'comments',
|
||||
OLD: 'old'
|
||||
};
|
@ -95,7 +95,7 @@ export default function Scene ({loggedIn, onLogout, ...props}) {
|
||||
</div>
|
||||
<BottomLinkContainer>
|
||||
<SmallLink target="_blank" href="https://github.com/nickzuber/meteorite">View and contribute on GitHub</SmallLink>
|
||||
<SmallText href="">Already have an account?</SmallText>
|
||||
<SmallText>Totally free and open sourced</SmallText>
|
||||
</BottomLinkContainer>
|
||||
</LandingMessage>
|
||||
<Curve />
|
||||
|
@ -56,7 +56,7 @@ export default function Scene ({ loading, error, loggedOut, ...props }) {
|
||||
</ButtonsContainer>
|
||||
</React.Fragment>
|
||||
) : loading ? (
|
||||
<LoadingIcon />
|
||||
<LoadingIcon style={{marginTop: 50}} />
|
||||
) : loggedOut ? (
|
||||
<React.Fragment>
|
||||
<p>Log in with GitHub and we'll start organizing and sorting all of your notifications.</p>
|
||||
|
@ -23,20 +23,21 @@ import '../../styles/gradient.css';
|
||||
* - MENTION -> 8
|
||||
* - ASSIGN -> 14
|
||||
* - REVIEW_REQUESTED -> 20
|
||||
* - SUBSCRIBED -> 6
|
||||
* - SUBSCRIBED -> 3
|
||||
* - AUTHOR -> 8
|
||||
* - OTHER -> 2
|
||||
*
|
||||
* There are some rules that go to giving out these scores, primarily being the
|
||||
* first time we see one of these unique reasons, we award the notification with
|
||||
* the respective score, but a reason that transitions into itself will be awarded
|
||||
* the score of `OTHER`. For example:
|
||||
* a degraded score of min(ceil(n/3), 2). For example:
|
||||
*
|
||||
* - null, MENTION, MENTION -> 0, 8, 2
|
||||
* - null, ASSIGN, ASSIGN, REVIEW_REQUESTED, -> 0, 14, 2, 20
|
||||
* - null, SUBSCRIBED, SUBSCRIBED, SUBSCRIBED -> 0, 6, 2, 2
|
||||
* - null, MENTION, MENTION -> 0, 8, 3
|
||||
* - null, ASSIGN, ASSIGN, REVIEW_REQUESTED, -> 0, 14, 5, 20
|
||||
* - null, SUBSCRIBED, SUBSCRIBED, SUBSCRIBED -> 0, 3, 2, 2
|
||||
*
|
||||
* @param {Object} notification Some notification to sort.
|
||||
* @param {Object} notification Some notification to score.
|
||||
* @return {number} The score.
|
||||
*/
|
||||
function scoreOf (notification) {
|
||||
return notification.reasons.length
|
||||
@ -113,7 +114,7 @@ const SidebarLink = styled('a')({}, ({active, color}) => ({
|
||||
transition: 'background 0.12s ease-in-out',
|
||||
display: 'flex',
|
||||
background: active ? color : 'none',
|
||||
color: active ? '#fff' : '#1a1a1a',
|
||||
color: active ? '#fff' : '#202124',
|
||||
':hover': {
|
||||
background: active ? color: 'rgba(200, 200, 200, .25)'
|
||||
},
|
||||
@ -248,7 +249,7 @@ const NotificationTitle = styled('span')({
|
||||
const Repository = styled('span')({
|
||||
fontWeight: 500,
|
||||
marginLeft: 10,
|
||||
fontSize: 15
|
||||
fontSize: 14
|
||||
});
|
||||
|
||||
const PRIssue = styled(Repository)({
|
||||
@ -279,7 +280,7 @@ const TableItem = styled('td')({
|
||||
}));
|
||||
|
||||
function getPRIssueIcon (type, reasons) {
|
||||
const grow = 1.2;
|
||||
const grow = 1.0;
|
||||
|
||||
switch (type) {
|
||||
case 'PullRequest':
|
||||
@ -448,12 +449,6 @@ export default function Scene ({
|
||||
<TableHeader>Queued</TableHeader>
|
||||
{notificationsQueued.map(n => (
|
||||
<NotificationRow key={n.id}>
|
||||
<TableItem width={100}>
|
||||
<InlineBlockContainer>
|
||||
<Icon.Hot shrink={0.9} />
|
||||
<Icon.Alarm shrink={0.9} />
|
||||
</InlineBlockContainer>
|
||||
</TableItem>
|
||||
<TableItem>
|
||||
<div style={{ float: 'left', marginTop: 2 }}>
|
||||
{getPRIssueIcon(n.type, n.reasons)}
|
||||
@ -464,11 +459,17 @@ export default function Scene ({
|
||||
onStageThread(n.id)
|
||||
}}>
|
||||
<NotificationTitle>
|
||||
<PRIssue alt="njksjnksdknjgf">{n.name}</PRIssue>
|
||||
<PRIssue>{n.name}</PRIssue>
|
||||
</NotificationTitle>
|
||||
</TableItem>
|
||||
<TableItem width={200}>
|
||||
{/* <TableItem width={200}>
|
||||
<Repository>{n.reasons.map(r => r.reason).join(', ')}</Repository>
|
||||
</TableItem> */}
|
||||
<TableItem width={100}>
|
||||
<InlineBlockContainer>
|
||||
<Icon.Hot shrink={0.75} />
|
||||
<Icon.Alarm shrink={0.75} />
|
||||
</InlineBlockContainer>
|
||||
</TableItem>
|
||||
<TableItem width={150}>
|
||||
<Repository>{n.repository}</Repository>
|
||||
@ -494,12 +495,6 @@ export default function Scene ({
|
||||
<TableHeader style={{marginTop: 50}}>Staged</TableHeader>
|
||||
{notificationsStaged.map(n => (
|
||||
<NotificationRow key={n.id}>
|
||||
<TableItem width={100}>
|
||||
<InlineBlockContainer>
|
||||
<Icon.Hot shrink={0.9} />
|
||||
<Icon.Alarm shrink={0.9} />
|
||||
</InlineBlockContainer>
|
||||
</TableItem>
|
||||
<TableItem>
|
||||
<div style={{ float: 'left', marginTop: 2 }}>
|
||||
{getPRIssueIcon(n.type, n.reasons)}
|
||||
|
655
src/pages/Notifications/SceneAlt.js
Normal file
@ -0,0 +1,655 @@
|
||||
import React from 'react';
|
||||
import {Link} from "@reach/router";
|
||||
import styled from 'react-emotion';
|
||||
import Icon from '../../components/Icon';
|
||||
import Logo from '../../components/Logo';
|
||||
import LoadingIcon from '../../components/LoadingIcon';
|
||||
import {routes} from '../../constants';
|
||||
import {Filters} from '../../constants/filters';
|
||||
import {withOnEnter} from '../../enhance';
|
||||
import {Status} from '../../constants/status';
|
||||
import {Reasons, Badges} from '../../constants/reasons';
|
||||
import '../../styles/gradient.css';
|
||||
|
||||
const FixedContainer = styled('div')({
|
||||
position: 'fixed'
|
||||
});
|
||||
|
||||
const InlineBlockContainer = styled('div')({
|
||||
'div': {
|
||||
display: 'inline-block'
|
||||
}
|
||||
});
|
||||
|
||||
const NotificationsContainer = styled('div')({
|
||||
position: 'relative',
|
||||
background: '#fff',
|
||||
margin: '0 auto',
|
||||
padding: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
overflowX: 'hidden',
|
||||
boxSizing: 'border-box'
|
||||
});
|
||||
|
||||
const NavigationContainer = styled('div')({
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
boxSizing: 'border-box',
|
||||
margin: '0 auto',
|
||||
width: '100%',
|
||||
background: 'none',
|
||||
height: 60,
|
||||
backgroundColor: '#24292e',
|
||||
color: 'hsla(0,0%,100%,.75)',
|
||||
paddingBottom: '12px',
|
||||
paddingTop: '12px',
|
||||
zIndex: '100',
|
||||
});
|
||||
|
||||
const GeneralOptionsContainer = styled(NavigationContainer)({
|
||||
position: 'relative',
|
||||
zIndex: '1',
|
||||
height: 'initial',
|
||||
minHeight: 60,
|
||||
width: '100%',
|
||||
margin: 0,
|
||||
marginLeft: 230,
|
||||
maxWidth: 1000,
|
||||
background: '#fff',
|
||||
padding: '8px 16px',
|
||||
paddingTop: 18,
|
||||
flex: '0 0 50px',
|
||||
'button': {
|
||||
display: 'inline-flex',
|
||||
margin: 0
|
||||
}
|
||||
});
|
||||
|
||||
const Sidebar = styled('div')({
|
||||
flex: '0 0 200px',
|
||||
padding: '0 20px 20px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
|
||||
const SidebarLink = styled('a')({}, ({active, color}) => ({
|
||||
textAlign: 'left',
|
||||
userSelect: 'none',
|
||||
margin: '0 auto',
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
borderRadius: 4,
|
||||
alignItems: 'center',
|
||||
padding: '0 14px',
|
||||
height: 40,
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'capitalize',
|
||||
textDecoration: 'none',
|
||||
transition: 'background 0.12s ease-in-out',
|
||||
display: 'flex',
|
||||
background: active ? color : 'none',
|
||||
color: active ? '#fff' : '#202124',
|
||||
':before': {
|
||||
content: '""',
|
||||
transition: 'all 150ms ease',
|
||||
background: 'rgba(190, 197, 208, 0.25)',
|
||||
borderRadius: 4,
|
||||
display: 'block',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
transform: 'scale(0)'
|
||||
},
|
||||
':hover:before': {
|
||||
transform: active ? 'scale(0)' : 'scale(1)',
|
||||
},
|
||||
':active:before': {
|
||||
background: 'rgba(190, 197, 208, 0.5)'
|
||||
},
|
||||
'div': {
|
||||
marginRight: 5
|
||||
}
|
||||
}));
|
||||
|
||||
const Notifications = styled('div')({
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
const NavTab = styled('a')({
|
||||
position: 'relative',
|
||||
textTransform: 'capitalize',
|
||||
userSelect: 'none',
|
||||
borderRadius: 4,
|
||||
textDecoration: 'none',
|
||||
fontWeight: '500',
|
||||
fontSize: '14px',
|
||||
textAlign: 'left',
|
||||
opacity: 0.6,
|
||||
padding: '20px 32px',
|
||||
paddingLeft: '16px',
|
||||
width: '150px',
|
||||
display: 'inline-block',
|
||||
margin: 0,
|
||||
transition: 'all 150ms ease',
|
||||
':hover': {
|
||||
background: 'rgba(190, 197, 208, 0.25)',
|
||||
},
|
||||
}, ({ active, color }) => active && ({
|
||||
color,
|
||||
opacity: 1,
|
||||
':after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
background: color,
|
||||
height: '3px',
|
||||
width: '90%',
|
||||
bottom: '0',
|
||||
left: '5%',
|
||||
borderTopLeftRadius: '4px',
|
||||
borderTopRightRadius: '4px',
|
||||
}
|
||||
}));
|
||||
|
||||
const Tab = styled('button')({
|
||||
position: 'relative',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
border: 0,
|
||||
outline: 'none',
|
||||
background: 'none',
|
||||
height: 40,
|
||||
width: 40,
|
||||
borderRadius: '100%',
|
||||
margin: '0 auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
':before': {
|
||||
content: "''",
|
||||
transition: 'all 150ms ease',
|
||||
background: 'rgba(190, 197, 208, 0.25)',
|
||||
borderRadius: '100%',
|
||||
display: 'block',
|
||||
height: 40,
|
||||
width: 40,
|
||||
position: 'absolute',
|
||||
transform: 'scale(0)'
|
||||
},
|
||||
':hover:before': {
|
||||
transform: 'scale(1)',
|
||||
},
|
||||
':active:before': {
|
||||
background: 'rgba(190, 197, 208, 0.5)'
|
||||
}
|
||||
}, ({disabled}) => disabled && ({
|
||||
background: 'none !important',
|
||||
opacity: 0.35,
|
||||
cursor: 'default',
|
||||
':hover:before': {
|
||||
transform: 'scale(0) !important',
|
||||
},
|
||||
':active:before': {
|
||||
background: 'none !important'
|
||||
}
|
||||
}));
|
||||
|
||||
const SearchField = styled('div')({
|
||||
float: 'left',
|
||||
textAlign: 'left',
|
||||
width: '50%',
|
||||
boxShadow: '0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08)',
|
||||
margin: '0 auto',
|
||||
background: 'hsla(0,0%,100%,.125)',
|
||||
borderRadius: '4px',
|
||||
alignItems: 'center',
|
||||
padding: 0,
|
||||
height: '36px',
|
||||
fontSize: '13px',
|
||||
textDecoration: 'none',
|
||||
transition: 'all 0.06s ease-in-out',
|
||||
display: 'inline-flex',
|
||||
':focus-within': {
|
||||
background: '#fff'
|
||||
}
|
||||
});
|
||||
|
||||
const Message = styled('div')({
|
||||
display: 'block',
|
||||
textAlign: 'center',
|
||||
marginTop: 96,
|
||||
'p': {
|
||||
paddingTop: 24,
|
||||
userSelect: 'none',
|
||||
display: 'block',
|
||||
margin: 0
|
||||
}
|
||||
});
|
||||
|
||||
const LoaderContainer = styled('div')({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100%'
|
||||
});
|
||||
|
||||
const SearchInput = styled('input')({
|
||||
flex: 1,
|
||||
textAlign: 'left',
|
||||
margin: '0 auto',
|
||||
background: 'none',
|
||||
padding: 0,
|
||||
height: '36px',
|
||||
color: '#fff',
|
||||
fontSize: '13px',
|
||||
textDecoration: 'none',
|
||||
display: 'inline-flex',
|
||||
border: '0',
|
||||
outline: 'none',
|
||||
':focus': {
|
||||
color: '#202124'
|
||||
}
|
||||
});
|
||||
const EnhancedSearchInput = withOnEnter(SearchInput);
|
||||
|
||||
const NotificationRow = styled('tr')({
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
// borderBottom: '1px solid #f2f2f2',
|
||||
display: 'block',
|
||||
textAlign: 'left',
|
||||
width: '100%',
|
||||
margin: '0 auto',
|
||||
background: '#fff',
|
||||
padding: '8px 16px',
|
||||
transition: 'all 0.1s ease-in-out',
|
||||
boxSizing: 'border-box',
|
||||
':hover': {
|
||||
background: '#f9f9f9',
|
||||
// boxShadow: '0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08)',
|
||||
zIndex: 10
|
||||
}
|
||||
});
|
||||
|
||||
const NotificationTab = styled(Tab)({
|
||||
display: 'inline-flex',
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
const NotificationTitle = styled('span')({
|
||||
position: 'relative',
|
||||
}, ({img}) => img && ({
|
||||
paddingLeft: 20,
|
||||
'::before': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
display: 'block',
|
||||
background: `url(${img}) center center no-repeat`,
|
||||
backgroundSize: 'cover',
|
||||
left: 0,
|
||||
height: 20,
|
||||
width: 20,
|
||||
}
|
||||
}));
|
||||
|
||||
const Repository = styled('span')({
|
||||
fontWeight: 500,
|
||||
marginLeft: 10,
|
||||
fontSize: 14
|
||||
});
|
||||
|
||||
const PRIssue = styled(Repository)({
|
||||
fontWeight: 400,
|
||||
});
|
||||
|
||||
const Table = styled('table')({
|
||||
width: '100%',
|
||||
maxWidth: 970,
|
||||
minWidth: 970,
|
||||
display: 'block',
|
||||
'td': {
|
||||
display: 'inline-block'
|
||||
}
|
||||
});
|
||||
|
||||
const TableItem = styled('td')({
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}, ({width}) => ({
|
||||
width
|
||||
}));
|
||||
|
||||
function getPRIssueIcon (type, reasons) {
|
||||
const grow = 1.0;
|
||||
|
||||
switch (type) {
|
||||
case 'PullRequest':
|
||||
return (
|
||||
<Icon.PrMerged shrink={grow} />
|
||||
);
|
||||
case 'Issue':
|
||||
return (
|
||||
<Icon.IssueOpen shrink={grow} />
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default function Scene ({
|
||||
first,
|
||||
last,
|
||||
lastPage,
|
||||
page,
|
||||
notifications,
|
||||
query,
|
||||
activeStatus,
|
||||
allNotificationsCount,
|
||||
onChangePage,
|
||||
onSetActiveStatus,
|
||||
onClearQuery,
|
||||
onLogout,
|
||||
onSearch,
|
||||
onMarkAsRead,
|
||||
onFetchNotifications,
|
||||
onRefreshNotifications,
|
||||
onStageThread,
|
||||
isSearching,
|
||||
isFetchingNotifications,
|
||||
onClearCache,
|
||||
fetchingNotificationsError,
|
||||
activeFilter,
|
||||
onSetActiveFilter,
|
||||
}) {
|
||||
const isLoading = isSearching || isFetchingNotifications;
|
||||
const isFirstPage = page === 1;
|
||||
const isLastPage = page === lastPage;
|
||||
|
||||
console.warn('before render in scene', notifications)
|
||||
|
||||
if (query) {
|
||||
notifications = notifications.filter(n => (
|
||||
n.name.toLowerCase().indexOf(query.toLowerCase()) > -1)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{marginTop: 60}}>
|
||||
<NavigationContainer>
|
||||
<div style={{
|
||||
maxWidth: 1200,
|
||||
textAlign: 'right',
|
||||
margin: '0 auto',
|
||||
padding: '0 20px 0 40px',
|
||||
}}>
|
||||
<Logo
|
||||
size={36}
|
||||
style={{
|
||||
float: 'left',
|
||||
marginRight: 48,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => {
|
||||
onSetActiveStatus(Status.QUEUED);
|
||||
onSetActiveFilter(Filters.PARTICIPATING);
|
||||
}}
|
||||
/>
|
||||
<SearchField>
|
||||
<Icon.Search size={48} opacity={.45} />
|
||||
<EnhancedSearchInput
|
||||
disabled={isLoading}
|
||||
type="text"
|
||||
placeholder="Search for notifications"
|
||||
onEnter={onSearch}
|
||||
/>
|
||||
{isSearching && <LoadingIcon alt={true} size={48} />}
|
||||
</SearchField>
|
||||
<div style={{display: 'inline-block'}} className="button-container-alt">
|
||||
<Link style={{
|
||||
marginRight: 15,
|
||||
background: 'none',
|
||||
color: '#fff',
|
||||
height: 36,
|
||||
padding: '0 12px'
|
||||
}} to={routes.HOME}>home</Link>
|
||||
</div>
|
||||
<div style={{display: 'inline-block'}} className="button-container-alt">
|
||||
<a style={{
|
||||
marginRight: 15,
|
||||
background: 'none',
|
||||
color: '#fff',
|
||||
height: 36,
|
||||
padding: '0 12px'
|
||||
}} href="#" onClick={onLogout}>sign out</a>
|
||||
</div>
|
||||
</div>
|
||||
</NavigationContainer>
|
||||
<GeneralOptionsContainer>
|
||||
<Tab disabled={isLoading}>
|
||||
<Icon.Refresh
|
||||
opacity={0.9}
|
||||
onClick={!isLoading ? (() => onFetchNotifications()) : undefined}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab disabled={isLoading}>
|
||||
<Icon.Trash
|
||||
opacity={0.9}
|
||||
onClick={!isLoading ? (() => onClearCache()) : undefined}
|
||||
/>
|
||||
</Tab>
|
||||
{query ? (
|
||||
<React.Fragment>
|
||||
<div style={{display: 'inline-block'}} className="button-container-alt">
|
||||
<a style={{
|
||||
marginRight: 15,
|
||||
background: 'none',
|
||||
color: '#202124',
|
||||
textTransform: 'inherit',
|
||||
boxShadow: '0 0 0',
|
||||
fontWeight: 400,
|
||||
height: 36,
|
||||
padding: '0 12px',
|
||||
}}
|
||||
>
|
||||
Showing results for '{query}'
|
||||
</a>
|
||||
</div>
|
||||
<Tab disabled={isLoading}>
|
||||
<Icon.X
|
||||
opacity={0.9}
|
||||
onClick={!isLoading ? (() => onClearQuery()) : undefined}
|
||||
/>
|
||||
</Tab>
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
<div style={{float: 'right'}}>
|
||||
<div style={{display: 'inline-block'}} className="button-container-alt">
|
||||
<a style={{
|
||||
marginRight: 15,
|
||||
background: 'none',
|
||||
color: '#202124',
|
||||
textTransform: 'inherit',
|
||||
boxShadow: '0 0 0',
|
||||
fontWeight: 400,
|
||||
height: 36,
|
||||
padding: '0 12px',
|
||||
}}>
|
||||
{first}-{last} of about {allNotificationsCount}
|
||||
</a>
|
||||
</div>
|
||||
<Tab disabled={isLoading || isFirstPage}>
|
||||
<Icon.Prev
|
||||
opacity={0.9}
|
||||
onClick={!isLoading && !isFirstPage ? (() => onChangePage(page - 1)) : undefined}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab disabled={isLoading || isLastPage}>
|
||||
<Icon.Next
|
||||
opacity={0.9}
|
||||
onClick={!isLoading && !isLastPage ? (() => onChangePage(page + 1)) : undefined}
|
||||
/>
|
||||
</Tab>
|
||||
</div>
|
||||
</GeneralOptionsContainer>
|
||||
<GeneralOptionsContainer style={{paddingTop: 4}}>
|
||||
<NavTab
|
||||
color="#00d19a"
|
||||
active={activeStatus === Status.QUEUED}
|
||||
onClick={() => onSetActiveStatus(Status.QUEUED)}
|
||||
href="#">
|
||||
Queued
|
||||
</NavTab>
|
||||
<NavTab
|
||||
color="#009ef8"
|
||||
active={activeStatus === Status.STAGED}
|
||||
onClick={() => onSetActiveStatus(Status.STAGED)}
|
||||
href="#">
|
||||
Staged
|
||||
</NavTab>
|
||||
<NavTab
|
||||
color="#f12c3f"
|
||||
active={activeStatus === Status.CLOSED}
|
||||
onClick={() => onSetActiveStatus(Status.CLOSED)}
|
||||
href="#">
|
||||
Closed
|
||||
</NavTab>
|
||||
</GeneralOptionsContainer>
|
||||
<NotificationsContainer>
|
||||
<Sidebar>
|
||||
<FixedContainer>
|
||||
<SidebarLink
|
||||
active={activeFilter === Filters.ALL}
|
||||
color="#6772e5"
|
||||
onClick={() => onSetActiveFilter(Filters.ALL)}>
|
||||
{activeFilter === Filters.ALL ? (
|
||||
<Icon.InboxWhite shrink={.6} />
|
||||
) : (
|
||||
<Icon.Inbox shrink={.6} />
|
||||
)}
|
||||
all notifications
|
||||
</SidebarLink>
|
||||
<SidebarLink
|
||||
active={activeFilter === Filters.PARTICIPATING}
|
||||
color="#00d19a"
|
||||
onClick={() => onSetActiveFilter(Filters.PARTICIPATING)}>
|
||||
{activeFilter === Filters.PARTICIPATING ? (
|
||||
<Icon.PeopleWhite shrink={.6} />
|
||||
) : (
|
||||
<Icon.People shrink={.6} />
|
||||
)}
|
||||
participating
|
||||
</SidebarLink>
|
||||
<SidebarLink
|
||||
active={activeFilter === Filters.COMMENT}
|
||||
color="#00A0F5"
|
||||
onClick={() => onSetActiveFilter(Filters.COMMENT)}>
|
||||
{activeFilter === Filters.COMMENT ? (
|
||||
<Icon.BookmarkAltWhite shrink={.6} />
|
||||
) : (
|
||||
<Icon.BookmarkAlt shrink={.6} />
|
||||
)}
|
||||
commented
|
||||
</SidebarLink>
|
||||
</FixedContainer>
|
||||
</Sidebar>
|
||||
<Notifications>
|
||||
{isFetchingNotifications ? (
|
||||
<LoaderContainer>
|
||||
<LoadingIcon />
|
||||
</LoaderContainer>
|
||||
) : notifications.length <= 0 ? (
|
||||
<Message>
|
||||
<p style={{
|
||||
fontSize: 16,
|
||||
fontWeight: 400,
|
||||
}}>
|
||||
No {activeStatus.toLowerCase()} notifications</p>
|
||||
<p style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
color: '#5f6368'
|
||||
}}>
|
||||
🎉 You're all set here for the moment</p>
|
||||
</Message>
|
||||
) : (
|
||||
<Table>
|
||||
<tbody>
|
||||
{notifications.map(n => (
|
||||
<NotificationRow key={n.id}>
|
||||
<TableItem>
|
||||
<div style={{ float: 'left', marginTop: 2 }}>
|
||||
{getPRIssueIcon(n.type, n.reasons)}
|
||||
</div>
|
||||
</TableItem>
|
||||
<TableItem width={400} onClick={() => {
|
||||
window.open(n.url);
|
||||
onStageThread(n.id)
|
||||
}}>
|
||||
<NotificationTitle>
|
||||
<PRIssue>{n.name}</PRIssue>
|
||||
</NotificationTitle>
|
||||
</TableItem>
|
||||
{/* <TableItem width={200}>
|
||||
<Repository>{n.reasons.map(r => r.reason).join(', ')}</Repository>
|
||||
</TableItem> */}
|
||||
<TableItem width={100}>
|
||||
<InlineBlockContainer>
|
||||
{n.badges.map(badge => {
|
||||
switch (badge) {
|
||||
case Badges.HOT:
|
||||
// lots of `reasons` within short time frame
|
||||
return <Icon.Hot shrink={0.75} />
|
||||
break;
|
||||
case Badges.OLD:
|
||||
// old
|
||||
return <Icon.Alarm shrink={0.75} />
|
||||
break;
|
||||
case Badges.COMMENTS:
|
||||
// lots of `reasons`
|
||||
return <Icon.Convo shrink={0.75} />
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</InlineBlockContainer>
|
||||
</TableItem>
|
||||
<TableItem width={250}>
|
||||
<Repository>{n.repository}</Repository>
|
||||
</TableItem>
|
||||
<TableItem width={150} style={{textAlign: 'right'}}>
|
||||
<NotificationTab>
|
||||
{n.score}
|
||||
</NotificationTab>
|
||||
<NotificationTab>
|
||||
<Icon.Check
|
||||
opacity={0.9}
|
||||
onClick={!isLoading ? (() => onStageThread(n.id)) : undefined}
|
||||
/>
|
||||
</NotificationTab>
|
||||
<NotificationTab>
|
||||
<Icon.X
|
||||
opacity={0.9}
|
||||
onClick={!isLoading ? (() => onMarkAsRead(n.id)) : undefined}
|
||||
/>
|
||||
</NotificationTab>
|
||||
</TableItem>
|
||||
{/* <p>Last read at {n.last_read_at ? moment(n.last_read_at).format('dddd h:mma') : 'never'}</p>
|
||||
<p>Last updated at {moment(n.last_updated).format('dddd h:mma')}</p> */}
|
||||
</NotificationRow>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Notifications>
|
||||
</NotificationsContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -8,16 +8,127 @@ import { withStorageProvider } from '../../providers/Storage';
|
||||
import { OAUTH_TOKEN_COOKIE } from '../../constants/cookies';
|
||||
import { routes } from '../../constants';
|
||||
import { Filters } from '../../constants/filters';
|
||||
import Scene from './Scene';
|
||||
import { Status } from '../../constants/status';
|
||||
import { Reasons, Badges } from '../../constants/reasons';
|
||||
import Scene from './SceneAlt';
|
||||
|
||||
// @TODO Move these functions.
|
||||
|
||||
/**
|
||||
* Given a notification, give it a score based on its importance.
|
||||
*
|
||||
* There are some interesting workarounds that go into this algorithm to account
|
||||
* for GitHub's broken notifications API -- but we will get to that later. First,
|
||||
* let's start off with the basics of scoring.
|
||||
*
|
||||
* There are a few "reasons" that we can be getting a notification, each having
|
||||
* an initial weight of importance:
|
||||
*
|
||||
* - MENTION -> 8
|
||||
* - ASSIGN -> 14
|
||||
* - REVIEW_REQUESTED -> 20
|
||||
* - SUBSCRIBED -> 3
|
||||
* - COMMENT -> 3
|
||||
* - AUTHOR -> 6
|
||||
* - OTHER -> 2
|
||||
*
|
||||
* There are some rules that go to giving out these scores, primarily being the
|
||||
* first time we see one of these unique reasons, we award the notification with
|
||||
* the respective score, but a reason that transitions into itself will be awarded
|
||||
* a degraded score of min(ceil(n/3), 2). For example:
|
||||
*
|
||||
* - null, MENTION, MENTION -> 0, 8, 3
|
||||
* - null, ASSIGN, ASSIGN, REVIEW_REQUESTED, -> 0, 14, 5, 20
|
||||
* - null, SUBSCRIBED, SUBSCRIBED, SUBSCRIBED -> 0, 3, 2, 2
|
||||
*
|
||||
* @param {Object} notification Some notification to score.
|
||||
* @return {number} The score.
|
||||
*/
|
||||
function scoreOf (notification) {
|
||||
const {reasons} = notification;
|
||||
let score = 0;
|
||||
let prevReason = null;
|
||||
for (let i = 0; i < reasons.length; i++) {
|
||||
const reason = reasons[i].reason;
|
||||
console.log(reason)
|
||||
if (prevReason && reason === prevReason) {
|
||||
const degradedScore = Math.ceil(scoreOfReason[reason] / 3);
|
||||
score += Math.max(degradedScore, 2);
|
||||
} else {
|
||||
score += scoreOfReason[reason];
|
||||
}
|
||||
prevReason = reason;
|
||||
}
|
||||
return score;
|
||||
};
|
||||
|
||||
// @TODO implement this
|
||||
function badgesOf (notification) {
|
||||
const badges = [];
|
||||
if (notification.reasons.length > 7) {
|
||||
badges.push(Badges.HOT);
|
||||
}
|
||||
if (notification.reasons.length > 3) {
|
||||
badges.push(Badges.COMMENTS);
|
||||
}
|
||||
if (notification.reasons.length <= 2) {
|
||||
badges.push(Badges.OLD);
|
||||
}
|
||||
return badges;
|
||||
};
|
||||
|
||||
const scoreOfReason = {
|
||||
[Reasons.ASSIGN]: 14,
|
||||
[Reasons.AUTHOR]: 6,
|
||||
[Reasons.MENTION]: 8,
|
||||
[Reasons.OTHER]: 2,
|
||||
[Reasons.REVIEW_REQUESTED]: 20,
|
||||
[Reasons.SUBSCRIBED]: 3,
|
||||
[Reasons.COMMENT]: 3,
|
||||
};
|
||||
|
||||
const decorateWithScore = notification => ({
|
||||
...notification,
|
||||
score: scoreOf(notification),
|
||||
badges: badgesOf(notification)
|
||||
});
|
||||
|
||||
const PER_PAGE = 10;
|
||||
|
||||
class NotificationsPage extends React.Component {
|
||||
state = {
|
||||
isSearching: false,
|
||||
activeFilter: Filters.PARTICIPATING
|
||||
query: null,
|
||||
activeFilter: Filters.PARTICIPATING,
|
||||
activeStatus: Status.QUEUED,
|
||||
currentPage: 1
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.syncer = setInterval(() => {
|
||||
console.warn('sync');
|
||||
this.props.notificationsApi.fetchNotificationsSync();
|
||||
}, 15 * 1000);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearInterval(this.syncer);
|
||||
}
|
||||
|
||||
onChangePage = page => {
|
||||
this.setState({ currentPage: page });
|
||||
}
|
||||
|
||||
onSetActiveFilter = filter => {
|
||||
this.setState({ activeFilter: filter });
|
||||
this.setState({ activeFilter: filter, currentPage: 1 });
|
||||
}
|
||||
|
||||
onSetActiveStatus = status => {
|
||||
this.setState({ activeStatus: status, currentPage: 1 });
|
||||
}
|
||||
|
||||
onClearQuery = () => {
|
||||
this.setState({ query: null });
|
||||
}
|
||||
|
||||
onLogout = () => {
|
||||
@ -36,9 +147,11 @@ class NotificationsPage extends React.Component {
|
||||
|
||||
this.setState({ isSearching: true });
|
||||
setTimeout(() => {
|
||||
console.warn(`searched for '${text}'`);
|
||||
this.setState({ isSearching: false });
|
||||
}, 2000);
|
||||
this.setState({
|
||||
query: text,
|
||||
isSearching: false
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
render () {
|
||||
@ -56,11 +169,84 @@ class NotificationsPage extends React.Component {
|
||||
error: fetchingNotificationsError,
|
||||
} = this.props.notificationsApi;
|
||||
|
||||
// @TODO Move all this out of the render method.
|
||||
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'
|
||||
))
|
||||
);
|
||||
break;
|
||||
case Filters.COMMENT:
|
||||
filterMethod = n => (
|
||||
n.reasons.some(({ reason }) => reason === 'comment')
|
||||
);
|
||||
break;
|
||||
default:
|
||||
filterMethod = () => true;
|
||||
}
|
||||
|
||||
const filteredNotifications = notifications.filter(filterMethod);
|
||||
const allNotificationsCount = filteredNotifications.length;
|
||||
|
||||
const notificationsQueued = filteredNotifications.filter(n => n.status === Status.QUEUED);
|
||||
const notificationsStaged = filteredNotifications.filter(n => n.status === Status.STAGED);
|
||||
const notificationsClosed = filteredNotifications.filter(n => n.status === Status.CLOSED);
|
||||
|
||||
let notificationsToRender = [];
|
||||
switch (this.state.activeStatus) {
|
||||
case Status.CLOSED:
|
||||
notificationsToRender = notificationsClosed;
|
||||
break;
|
||||
case Status.STAGED:
|
||||
notificationsToRender = notificationsStaged;
|
||||
break;
|
||||
case Status.QUEUED:
|
||||
default:
|
||||
notificationsToRender = notificationsQueued;
|
||||
}
|
||||
|
||||
const scoredAndSortedNotifications = notificationsToRender
|
||||
.map(decorateWithScore)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
let firstIndex = (this.state.currentPage - 1) * PER_PAGE;
|
||||
let lastIndex = (this.state.currentPage * PER_PAGE);
|
||||
let notificationsOnPage = scoredAndSortedNotifications.slice(firstIndex, lastIndex);
|
||||
let lastPage = Math.ceil(scoredAndSortedNotifications.length / PER_PAGE);
|
||||
let firstNumbered = firstIndex + 1;
|
||||
let lastNumbered = Math.min(lastIndex, scoredAndSortedNotifications.length);
|
||||
|
||||
if (scoredAndSortedNotifications.length === 0) {
|
||||
firstIndex = 0;
|
||||
lastIndex = 0;
|
||||
notificationsOnPage = [];
|
||||
lastPage = 1;
|
||||
firstNumbered = 0;
|
||||
lastNumbered = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<Scene
|
||||
notifications={notifications}
|
||||
first={firstNumbered}
|
||||
last={lastNumbered}
|
||||
lastPage={lastPage}
|
||||
allNotificationsCount={allNotificationsCount}
|
||||
notifications={notificationsOnPage}
|
||||
query={this.state.query}
|
||||
page={this.state.currentPage}
|
||||
activeStatus={this.state.activeStatus}
|
||||
activeFilter={this.state.activeFilter}
|
||||
onChangePage={this.onChangePage}
|
||||
onLogout={this.onLogout}
|
||||
onSetActiveStatus={this.onSetActiveStatus}
|
||||
onSearch={this.onSearch}
|
||||
onClearQuery={this.onClearQuery}
|
||||
onFetchNotifications={fetchNotifications}
|
||||
onMarkAsRead={markAsRead}
|
||||
onClearCache={clearCache}
|
||||
@ -70,7 +256,6 @@ class NotificationsPage extends React.Component {
|
||||
isFetchingNotifications={isFetchingNotifications}
|
||||
fetchingNotificationsError={fetchingNotificationsError}
|
||||
onSetActiveFilter={this.onSetActiveFilter}
|
||||
activeFilter={this.state.activeFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import {Status} from '../constants/status';
|
||||
|
||||
const BASE_GITHUB_API_URL = 'https://api.github.com';
|
||||
|
||||
function subjectUrlToIssue (url) {
|
||||
function cleanResponseUrl (url) {
|
||||
return url
|
||||
.replace('api.github.com', 'github.com')
|
||||
.replace('/repos/', '/')
|
||||
@ -64,6 +64,17 @@ class NotificationsProvider extends React.Component {
|
||||
error: null
|
||||
}
|
||||
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
// Update if our state changes
|
||||
if ((this.state.loading !== nextState.loading) ||
|
||||
(this.state.error !== nextState.error)) {
|
||||
return true;
|
||||
}
|
||||
// Only update if our notifications prop changes.
|
||||
// All other props "changing" should NOT trigger a rerender.
|
||||
return this.props.notifications !== nextProps.notifications;
|
||||
}
|
||||
|
||||
requestPage = (page = 1, optimizePolling = true) => {
|
||||
const headers = {
|
||||
'Authorization': `token ${this.props.token}`,
|
||||
@ -102,13 +113,7 @@ class NotificationsProvider extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
fetchNotifications = (page = 1, optimizePolling = true) => {
|
||||
if (!this.props.token) {
|
||||
console.error('Unauthenitcated, aborting request.')
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setState({ loading: true });
|
||||
requestFetchNotifications = (page = 1, optimizePolling = true) => {
|
||||
return this.requestPage(page, optimizePolling)
|
||||
.then(({headers, json}) => {
|
||||
if (json === null) return;
|
||||
@ -118,46 +123,59 @@ class NotificationsProvider extends React.Component {
|
||||
nextPage = links.next.page;
|
||||
}
|
||||
return this.processNotificationsChunk(nextPage, json);
|
||||
})
|
||||
.catch(error => console.error(error) || this.setState({ error }))
|
||||
});
|
||||
}
|
||||
|
||||
fetchNotifications = (page = 1, optimizePolling = true) => {
|
||||
if (!this.props.token) {
|
||||
console.error('Unauthenitcated, aborting request.')
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setState({ loading: true });
|
||||
return this.requestFetchNotifications(page, optimizePolling)
|
||||
.catch(error => this.setState({ error }))
|
||||
.finally(() => this.setState({ loading: false }));
|
||||
}
|
||||
|
||||
processNotificationsChunk = (nextPage, notificationsChunk) => {
|
||||
console.log('chunk', notificationsChunk)
|
||||
let everythingUpdated = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('chunk', notificationsChunk)
|
||||
let everythingUpdated = true;
|
||||
|
||||
if (notificationsChunk.length === 0) {
|
||||
// Apparently this means that a user has no notifications (makes sense).
|
||||
// So I guess we should purge our cache? This brings up the great point
|
||||
// of us having stale cache. How can we detect that a notifcation was seen?
|
||||
}
|
||||
if (notificationsChunk.length === 0) {
|
||||
// Apparently this means that a user has no notifications (makes sense).
|
||||
// So I guess we should purge our cache? This brings up the great point
|
||||
// of us having stale cache. How can we detect that a notifcation was seen?
|
||||
}
|
||||
|
||||
notificationsChunk.forEach(n => {
|
||||
const cached_n = this.props.getItemFromStorage(n.id);
|
||||
// If we've seen this notification before.
|
||||
if (cached_n) {
|
||||
// Something's changed, we want to push
|
||||
if (cached_n.updated_at !== n.updated_at) {
|
||||
this.updateNotification(n, cached_n.reasons);
|
||||
return;
|
||||
notificationsChunk.forEach(n => {
|
||||
const cached_n = this.props.getItemFromStorage(n.id);
|
||||
// If we've seen this notification before.
|
||||
if (cached_n) {
|
||||
// Something's changed, we want to push
|
||||
if (cached_n.updated_at !== n.updated_at) {
|
||||
this.updateNotification(n, cached_n.reasons);
|
||||
return;
|
||||
}
|
||||
// This means that something didn't update, which means the page we're
|
||||
// currently processing has stale data so we don't need to fetch the next page.
|
||||
everythingUpdated = false;
|
||||
} else {
|
||||
// Else, update the cache.
|
||||
this.updateNotification(n);
|
||||
}
|
||||
// This means that something didn't update, which means the page we're
|
||||
// currently processing has stale data so we don't need to fetch the next page.
|
||||
everythingUpdated = false;
|
||||
});
|
||||
|
||||
if (nextPage && everythingUpdated) {
|
||||
// Still need to fetch more updates.
|
||||
this.fetchNotifications(nextPage, false);
|
||||
} else {
|
||||
// Else, update the cache.
|
||||
this.updateNotification(n);
|
||||
// All done fetching updates, let's trigger a sync.
|
||||
this.props.refreshNotifications();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
if (nextPage && everythingUpdated) {
|
||||
// Still need to fetch more updates.
|
||||
this.fetchNotifications(nextPage, false);
|
||||
} else {
|
||||
// All done fetching updates, let's trigger a sync.
|
||||
this.props.refreshNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
updateNotification = (n, prevReason = null) => {
|
||||
@ -181,8 +199,10 @@ class NotificationsProvider extends React.Component {
|
||||
reasons: reasons,
|
||||
type: n.subject.type,
|
||||
name: n.subject.title,
|
||||
url: subjectUrlToIssue(n.subject.url),
|
||||
repository: n.repository.name,
|
||||
url: cleanResponseUrl(n.subject.url),
|
||||
repository: n.repository.full_name,
|
||||
number: n.subject.url.split('/').pop(),
|
||||
repositoryUrl: cleanResponseUrl(n.repository.url)
|
||||
};
|
||||
this.props.setItemInStorage(n.id, value);
|
||||
}
|
||||
@ -269,6 +289,7 @@ class NotificationsProvider extends React.Component {
|
||||
...this.state,
|
||||
notifications: this.props.notifications,
|
||||
fetchNotifications: this.fetchNotifications,
|
||||
fetchNotificationsSync: this.requestFetchNotifications,
|
||||
markAsRead: this.markAsRead,
|
||||
clearCache: this.clearCache,
|
||||
stageThread: this.stageThread
|
||||
|
@ -1,13 +1,38 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import {Status} from '../constants/status';
|
||||
import {Reasons} from '../constants/reasons';
|
||||
|
||||
const LOCAL_STORAGE_PREFIX = '__meteorite_noti_cache__';
|
||||
|
||||
class StorageProvider extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
}
|
||||
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 {
|
||||
state = {
|
||||
loading: false,
|
||||
error: null,
|
||||
@ -28,6 +53,7 @@ class StorageProvider extends React.Component {
|
||||
}
|
||||
});
|
||||
this.setState({ notifications });
|
||||
// this.setState({ notifications: mockNotifications });
|
||||
}
|
||||
|
||||
// val value : Object
|
||||
@ -42,11 +68,11 @@ class StorageProvider extends React.Component {
|
||||
//
|
||||
// window.localStorage.removeItem(`${LOCAL_STORAGE_PREFIX}${id}`);
|
||||
const cached_n = this.getItem(id);
|
||||
cached_n = {
|
||||
const closed_cached_n = {
|
||||
...cached_n,
|
||||
status: Status.CLOSED
|
||||
};
|
||||
this.setItem(id, cached_n);
|
||||
this.setItem(id, closed_cached_n);
|
||||
}
|
||||
|
||||
getItem = id => {
|
||||
|
@ -1,7 +1,8 @@
|
||||
|
||||
.container-gradient {
|
||||
/* background: radial-gradient(farthest-corner at -0% 100%, #7247ff 30%, #00ffbe 95%); */
|
||||
background: radial-gradient(farthest-corner at -0% 100%, #6772e5 30%, #00cfff 95%);
|
||||
/* background: radial-gradient(farthest-corner at -0% 100%, #6772e5 30%, #00cfff 95%); */
|
||||
background: radial-gradient(farthest-corner at -0% 100%, #24292e 30%, #213a54 95%);
|
||||
background-size: 400% 400%;
|
||||
|
||||
-webkit-animation: gradientTransition 20s ease infinite;
|
||||
@ -13,8 +14,10 @@
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08);
|
||||
margin: 0 auto;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
color: #6772e5;
|
||||
/* color: #6772e5; */
|
||||
color: #24292e;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
-webkit-align-items: center;
|
||||
@ -47,10 +50,12 @@
|
||||
.button-container-alt a {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
box-shadow: 0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08);
|
||||
margin: 0 auto;
|
||||
cursor: pointer;
|
||||
color: #6772e5;
|
||||
/* color: #6772e5; */
|
||||
color: #24292e;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
-webkit-align-items: center;
|
||||
|
@ -18,14 +18,14 @@
|
||||
|
||||
::selection {
|
||||
color: #fff;
|
||||
background: #6772e5;
|
||||
background: #24292e;
|
||||
}
|
||||
|
||||
html, body, * {
|
||||
font-family: 'Inter UI', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
color: #1a1a1a;
|
||||
color: #202124;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|