Everything
BIN
public/icon-black.png
Normal file
After Width: | Height: | Size: 110 KiB |
@ -2,9 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<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="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">
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||||
<title>Meteorite — Smarter GitHub notifications</title>
|
<title>Meteorite — Smarter GitHub notifications</title>
|
||||||
</head>
|
</head>
|
||||||
|
@ -7,19 +7,23 @@ import back from './svg/back.svg';
|
|||||||
import bolt from './svg/bolt.svg';
|
import bolt from './svg/bolt.svg';
|
||||||
import boltWhite from './svg/bolt-white.svg';
|
import boltWhite from './svg/bolt-white.svg';
|
||||||
import bookmarkAlt from './svg/bookmark-alt.svg';
|
import bookmarkAlt from './svg/bookmark-alt.svg';
|
||||||
|
import bookmarkAltWhite from './svg/bookmark-alt-white.svg';
|
||||||
import bookmark from './svg/bookmark.svg';
|
import bookmark from './svg/bookmark.svg';
|
||||||
import bookmarks from './svg/bookmarks.svg';
|
import bookmarks from './svg/bookmarks.svg';
|
||||||
import check from './svg/check.svg';
|
import check from './svg/check.svg';
|
||||||
|
import convo from './svg/convo.svg';
|
||||||
import doneAll from './svg/done-all.svg';
|
import doneAll from './svg/done-all.svg';
|
||||||
import done from './svg/done.svg';
|
import done from './svg/done.svg';
|
||||||
import hot from './svg/hot.svg';
|
import hot from './svg/hot.svg';
|
||||||
import inbox from './svg/inbox.svg';
|
import inbox from './svg/inbox.svg';
|
||||||
import inboxWhite from './svg/inbox-white.svg';
|
import inboxWhite from './svg/inbox-white.svg';
|
||||||
import locked from './svg/locked.svg';
|
import locked from './svg/locked.svg';
|
||||||
|
import lowPriority from './svg/low_priority.svg';
|
||||||
import menu from './svg/menu.svg';
|
import menu from './svg/menu.svg';
|
||||||
import next from './svg/next.svg';
|
import next from './svg/next.svg';
|
||||||
import people from './svg/people.svg';
|
import people from './svg/people.svg';
|
||||||
import peopleWhite from './svg/people-white.svg';
|
import peopleWhite from './svg/people-white.svg';
|
||||||
|
import prev from './svg/prev.svg';
|
||||||
import refresh from './svg/refresh.svg';
|
import refresh from './svg/refresh.svg';
|
||||||
import search from './svg/search.svg';
|
import search from './svg/search.svg';
|
||||||
import settings from './svg/settings.svg';
|
import settings from './svg/settings.svg';
|
||||||
@ -62,19 +66,23 @@ Icon.Back = createIcon(back);
|
|||||||
Icon.Bolt = createIcon(bolt);
|
Icon.Bolt = createIcon(bolt);
|
||||||
Icon.BoltWhite = createIcon(boltWhite);
|
Icon.BoltWhite = createIcon(boltWhite);
|
||||||
Icon.BookmarkAlt = createIcon(bookmarkAlt);
|
Icon.BookmarkAlt = createIcon(bookmarkAlt);
|
||||||
|
Icon.BookmarkAltWhite = createIcon(bookmarkAltWhite);
|
||||||
Icon.Bookmark = createIcon(bookmark);
|
Icon.Bookmark = createIcon(bookmark);
|
||||||
Icon.Bookmarks = createIcon(bookmarks);
|
Icon.Bookmarks = createIcon(bookmarks);
|
||||||
Icon.Check = createIcon(check);
|
Icon.Check = createIcon(check);
|
||||||
|
Icon.Convo = createIcon(convo);
|
||||||
Icon.DoneAll = createIcon(doneAll);
|
Icon.DoneAll = createIcon(doneAll);
|
||||||
Icon.Done = createIcon(done);
|
Icon.Done = createIcon(done);
|
||||||
Icon.Hot = createIcon(hot);
|
Icon.Hot = createIcon(hot);
|
||||||
Icon.Inbox = createIcon(inbox);
|
Icon.Inbox = createIcon(inbox);
|
||||||
Icon.InboxWhite = createIcon(inboxWhite);
|
Icon.InboxWhite = createIcon(inboxWhite);
|
||||||
Icon.Locked = createIcon(locked);
|
Icon.Locked = createIcon(locked);
|
||||||
|
Icon.LowPriority = createIcon(lowPriority);
|
||||||
Icon.Menu = createIcon(menu);
|
Icon.Menu = createIcon(menu);
|
||||||
Icon.Next = createIcon(next);
|
Icon.Next = createIcon(next);
|
||||||
Icon.People = createIcon(people);
|
Icon.People = createIcon(people);
|
||||||
Icon.PeopleWhite = createIcon(peopleWhite);
|
Icon.PeopleWhite = createIcon(peopleWhite);
|
||||||
|
Icon.Prev = createIcon(prev);
|
||||||
Icon.Refresh = createIcon(refresh);
|
Icon.Refresh = createIcon(refresh);
|
||||||
Icon.Search = createIcon(search);
|
Icon.Search = createIcon(search);
|
||||||
Icon.Settings = createIcon(settings);
|
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 React from 'react';
|
||||||
import loader from './loader.svg';
|
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 (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
background: `url(${loader}) center center no-repeat`,
|
background: `url(${(alt ? loaderAlt : loader)}) center center no-repeat`,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
height: size || 100,
|
height: size || 100,
|
||||||
width: 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 fill="none" fill-rule="evenodd">
|
||||||
<g transform="translate(1 1)" stroke-width="2.6">
|
<g transform="translate(1 1)" stroke-width="2.6">
|
||||||
<circle stroke-opacity=".5" cx="9" cy="9" r="9"/>
|
<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
|
<animateTransform
|
||||||
attributeName="transform"
|
attributeName="transform"
|
||||||
type="rotate"
|
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',
|
REVIEW_REQUESTED: 'review_requested',
|
||||||
PARTICIPATING: 'participating',
|
PARTICIPATING: 'participating',
|
||||||
SUBSCRIBED: 'subscribed',
|
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>
|
</div>
|
||||||
<BottomLinkContainer>
|
<BottomLinkContainer>
|
||||||
<SmallLink target="_blank" href="https://github.com/nickzuber/meteorite">View and contribute on GitHub</SmallLink>
|
<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>
|
</BottomLinkContainer>
|
||||||
</LandingMessage>
|
</LandingMessage>
|
||||||
<Curve />
|
<Curve />
|
||||||
|
@ -56,7 +56,7 @@ export default function Scene ({ loading, error, loggedOut, ...props }) {
|
|||||||
</ButtonsContainer>
|
</ButtonsContainer>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
) : loading ? (
|
) : loading ? (
|
||||||
<LoadingIcon />
|
<LoadingIcon style={{marginTop: 50}} />
|
||||||
) : loggedOut ? (
|
) : loggedOut ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<p>Log in with GitHub and we'll start organizing and sorting all of your notifications.</p>
|
<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
|
* - MENTION -> 8
|
||||||
* - ASSIGN -> 14
|
* - ASSIGN -> 14
|
||||||
* - REVIEW_REQUESTED -> 20
|
* - REVIEW_REQUESTED -> 20
|
||||||
* - SUBSCRIBED -> 6
|
* - SUBSCRIBED -> 3
|
||||||
* - AUTHOR -> 8
|
* - AUTHOR -> 8
|
||||||
* - OTHER -> 2
|
* - OTHER -> 2
|
||||||
*
|
*
|
||||||
* There are some rules that go to giving out these scores, primarily being the
|
* 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
|
* 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 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, MENTION, MENTION -> 0, 8, 3
|
||||||
* - null, ASSIGN, ASSIGN, REVIEW_REQUESTED, -> 0, 14, 2, 20
|
* - null, ASSIGN, ASSIGN, REVIEW_REQUESTED, -> 0, 14, 5, 20
|
||||||
* - null, SUBSCRIBED, SUBSCRIBED, SUBSCRIBED -> 0, 6, 2, 2
|
* - 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) {
|
function scoreOf (notification) {
|
||||||
return notification.reasons.length
|
return notification.reasons.length
|
||||||
@ -113,7 +114,7 @@ const SidebarLink = styled('a')({}, ({active, color}) => ({
|
|||||||
transition: 'background 0.12s ease-in-out',
|
transition: 'background 0.12s ease-in-out',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
background: active ? color : 'none',
|
background: active ? color : 'none',
|
||||||
color: active ? '#fff' : '#1a1a1a',
|
color: active ? '#fff' : '#202124',
|
||||||
':hover': {
|
':hover': {
|
||||||
background: active ? color: 'rgba(200, 200, 200, .25)'
|
background: active ? color: 'rgba(200, 200, 200, .25)'
|
||||||
},
|
},
|
||||||
@ -248,7 +249,7 @@ const NotificationTitle = styled('span')({
|
|||||||
const Repository = styled('span')({
|
const Repository = styled('span')({
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
marginLeft: 10,
|
marginLeft: 10,
|
||||||
fontSize: 15
|
fontSize: 14
|
||||||
});
|
});
|
||||||
|
|
||||||
const PRIssue = styled(Repository)({
|
const PRIssue = styled(Repository)({
|
||||||
@ -279,7 +280,7 @@ const TableItem = styled('td')({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
function getPRIssueIcon (type, reasons) {
|
function getPRIssueIcon (type, reasons) {
|
||||||
const grow = 1.2;
|
const grow = 1.0;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'PullRequest':
|
case 'PullRequest':
|
||||||
@ -448,12 +449,6 @@ export default function Scene ({
|
|||||||
<TableHeader>Queued</TableHeader>
|
<TableHeader>Queued</TableHeader>
|
||||||
{notificationsQueued.map(n => (
|
{notificationsQueued.map(n => (
|
||||||
<NotificationRow key={n.id}>
|
<NotificationRow key={n.id}>
|
||||||
<TableItem width={100}>
|
|
||||||
<InlineBlockContainer>
|
|
||||||
<Icon.Hot shrink={0.9} />
|
|
||||||
<Icon.Alarm shrink={0.9} />
|
|
||||||
</InlineBlockContainer>
|
|
||||||
</TableItem>
|
|
||||||
<TableItem>
|
<TableItem>
|
||||||
<div style={{ float: 'left', marginTop: 2 }}>
|
<div style={{ float: 'left', marginTop: 2 }}>
|
||||||
{getPRIssueIcon(n.type, n.reasons)}
|
{getPRIssueIcon(n.type, n.reasons)}
|
||||||
@ -464,11 +459,17 @@ export default function Scene ({
|
|||||||
onStageThread(n.id)
|
onStageThread(n.id)
|
||||||
}}>
|
}}>
|
||||||
<NotificationTitle>
|
<NotificationTitle>
|
||||||
<PRIssue alt="njksjnksdknjgf">{n.name}</PRIssue>
|
<PRIssue>{n.name}</PRIssue>
|
||||||
</NotificationTitle>
|
</NotificationTitle>
|
||||||
</TableItem>
|
</TableItem>
|
||||||
<TableItem width={200}>
|
{/* <TableItem width={200}>
|
||||||
<Repository>{n.reasons.map(r => r.reason).join(', ')}</Repository>
|
<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>
|
||||||
<TableItem width={150}>
|
<TableItem width={150}>
|
||||||
<Repository>{n.repository}</Repository>
|
<Repository>{n.repository}</Repository>
|
||||||
@ -494,12 +495,6 @@ export default function Scene ({
|
|||||||
<TableHeader style={{marginTop: 50}}>Staged</TableHeader>
|
<TableHeader style={{marginTop: 50}}>Staged</TableHeader>
|
||||||
{notificationsStaged.map(n => (
|
{notificationsStaged.map(n => (
|
||||||
<NotificationRow key={n.id}>
|
<NotificationRow key={n.id}>
|
||||||
<TableItem width={100}>
|
|
||||||
<InlineBlockContainer>
|
|
||||||
<Icon.Hot shrink={0.9} />
|
|
||||||
<Icon.Alarm shrink={0.9} />
|
|
||||||
</InlineBlockContainer>
|
|
||||||
</TableItem>
|
|
||||||
<TableItem>
|
<TableItem>
|
||||||
<div style={{ float: 'left', marginTop: 2 }}>
|
<div style={{ float: 'left', marginTop: 2 }}>
|
||||||
{getPRIssueIcon(n.type, n.reasons)}
|
{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 { OAUTH_TOKEN_COOKIE } from '../../constants/cookies';
|
||||||
import { routes } from '../../constants';
|
import { routes } from '../../constants';
|
||||||
import { Filters } from '../../constants/filters';
|
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 {
|
class NotificationsPage extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
isSearching: false,
|
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 => {
|
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 = () => {
|
onLogout = () => {
|
||||||
@ -36,9 +147,11 @@ class NotificationsPage extends React.Component {
|
|||||||
|
|
||||||
this.setState({ isSearching: true });
|
this.setState({ isSearching: true });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.warn(`searched for '${text}'`);
|
this.setState({
|
||||||
this.setState({ isSearching: false });
|
query: text,
|
||||||
}, 2000);
|
isSearching: false
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
@ -56,11 +169,84 @@ class NotificationsPage extends React.Component {
|
|||||||
error: fetchingNotificationsError,
|
error: fetchingNotificationsError,
|
||||||
} = this.props.notificationsApi;
|
} = 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 (
|
return (
|
||||||
<Scene
|
<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}
|
onLogout={this.onLogout}
|
||||||
|
onSetActiveStatus={this.onSetActiveStatus}
|
||||||
onSearch={this.onSearch}
|
onSearch={this.onSearch}
|
||||||
|
onClearQuery={this.onClearQuery}
|
||||||
onFetchNotifications={fetchNotifications}
|
onFetchNotifications={fetchNotifications}
|
||||||
onMarkAsRead={markAsRead}
|
onMarkAsRead={markAsRead}
|
||||||
onClearCache={clearCache}
|
onClearCache={clearCache}
|
||||||
@ -70,7 +256,6 @@ class NotificationsPage extends React.Component {
|
|||||||
isFetchingNotifications={isFetchingNotifications}
|
isFetchingNotifications={isFetchingNotifications}
|
||||||
fetchingNotificationsError={fetchingNotificationsError}
|
fetchingNotificationsError={fetchingNotificationsError}
|
||||||
onSetActiveFilter={this.onSetActiveFilter}
|
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';
|
const BASE_GITHUB_API_URL = 'https://api.github.com';
|
||||||
|
|
||||||
function subjectUrlToIssue (url) {
|
function cleanResponseUrl (url) {
|
||||||
return url
|
return url
|
||||||
.replace('api.github.com', 'github.com')
|
.replace('api.github.com', 'github.com')
|
||||||
.replace('/repos/', '/')
|
.replace('/repos/', '/')
|
||||||
@ -64,6 +64,17 @@ class NotificationsProvider extends React.Component {
|
|||||||
error: null
|
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) => {
|
requestPage = (page = 1, optimizePolling = true) => {
|
||||||
const headers = {
|
const headers = {
|
||||||
'Authorization': `token ${this.props.token}`,
|
'Authorization': `token ${this.props.token}`,
|
||||||
@ -102,13 +113,7 @@ class NotificationsProvider extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchNotifications = (page = 1, optimizePolling = true) => {
|
requestFetchNotifications = (page = 1, optimizePolling = true) => {
|
||||||
if (!this.props.token) {
|
|
||||||
console.error('Unauthenitcated, aborting request.')
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ loading: true });
|
|
||||||
return this.requestPage(page, optimizePolling)
|
return this.requestPage(page, optimizePolling)
|
||||||
.then(({headers, json}) => {
|
.then(({headers, json}) => {
|
||||||
if (json === null) return;
|
if (json === null) return;
|
||||||
@ -118,46 +123,59 @@ class NotificationsProvider extends React.Component {
|
|||||||
nextPage = links.next.page;
|
nextPage = links.next.page;
|
||||||
}
|
}
|
||||||
return this.processNotificationsChunk(nextPage, json);
|
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 }));
|
.finally(() => this.setState({ loading: false }));
|
||||||
}
|
}
|
||||||
|
|
||||||
processNotificationsChunk = (nextPage, notificationsChunk) => {
|
processNotificationsChunk = (nextPage, notificationsChunk) => {
|
||||||
console.log('chunk', notificationsChunk)
|
return new Promise((resolve, reject) => {
|
||||||
let everythingUpdated = true;
|
console.log('chunk', notificationsChunk)
|
||||||
|
let everythingUpdated = true;
|
||||||
|
|
||||||
if (notificationsChunk.length === 0) {
|
if (notificationsChunk.length === 0) {
|
||||||
// Apparently this means that a user has no notifications (makes sense).
|
// 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
|
// 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?
|
// of us having stale cache. How can we detect that a notifcation was seen?
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationsChunk.forEach(n => {
|
notificationsChunk.forEach(n => {
|
||||||
const cached_n = this.props.getItemFromStorage(n.id);
|
const cached_n = this.props.getItemFromStorage(n.id);
|
||||||
// If we've seen this notification before.
|
// If we've seen this notification before.
|
||||||
if (cached_n) {
|
if (cached_n) {
|
||||||
// Something's changed, we want to push
|
// Something's changed, we want to push
|
||||||
if (cached_n.updated_at !== n.updated_at) {
|
if (cached_n.updated_at !== n.updated_at) {
|
||||||
this.updateNotification(n, cached_n.reasons);
|
this.updateNotification(n, cached_n.reasons);
|
||||||
return;
|
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 {
|
||||||
// Else, update the cache.
|
// All done fetching updates, let's trigger a sync.
|
||||||
this.updateNotification(n);
|
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) => {
|
updateNotification = (n, prevReason = null) => {
|
||||||
@ -181,8 +199,10 @@ class NotificationsProvider extends React.Component {
|
|||||||
reasons: reasons,
|
reasons: reasons,
|
||||||
type: n.subject.type,
|
type: n.subject.type,
|
||||||
name: n.subject.title,
|
name: n.subject.title,
|
||||||
url: subjectUrlToIssue(n.subject.url),
|
url: cleanResponseUrl(n.subject.url),
|
||||||
repository: n.repository.name,
|
repository: n.repository.full_name,
|
||||||
|
number: n.subject.url.split('/').pop(),
|
||||||
|
repositoryUrl: cleanResponseUrl(n.repository.url)
|
||||||
};
|
};
|
||||||
this.props.setItemInStorage(n.id, value);
|
this.props.setItemInStorage(n.id, value);
|
||||||
}
|
}
|
||||||
@ -269,6 +289,7 @@ class NotificationsProvider extends React.Component {
|
|||||||
...this.state,
|
...this.state,
|
||||||
notifications: this.props.notifications,
|
notifications: this.props.notifications,
|
||||||
fetchNotifications: this.fetchNotifications,
|
fetchNotifications: this.fetchNotifications,
|
||||||
|
fetchNotificationsSync: this.requestFetchNotifications,
|
||||||
markAsRead: this.markAsRead,
|
markAsRead: this.markAsRead,
|
||||||
clearCache: this.clearCache,
|
clearCache: this.clearCache,
|
||||||
stageThread: this.stageThread
|
stageThread: this.stageThread
|
||||||
|
@ -1,13 +1,38 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import moment from 'moment';
|
||||||
import {Status} from '../constants/status';
|
import {Status} from '../constants/status';
|
||||||
|
import {Reasons} from '../constants/reasons';
|
||||||
|
|
||||||
const LOCAL_STORAGE_PREFIX = '__meteorite_noti_cache__';
|
const LOCAL_STORAGE_PREFIX = '__meteorite_noti_cache__';
|
||||||
|
|
||||||
class StorageProvider extends React.Component {
|
const getMockReasons = n => {
|
||||||
constructor (props) {
|
const reasons = Object.values(Reasons);
|
||||||
super(props);
|
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 = {
|
state = {
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
@ -28,6 +53,7 @@ class StorageProvider extends React.Component {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.setState({ notifications });
|
this.setState({ notifications });
|
||||||
|
// this.setState({ notifications: mockNotifications });
|
||||||
}
|
}
|
||||||
|
|
||||||
// val value : Object
|
// val value : Object
|
||||||
@ -42,11 +68,11 @@ class StorageProvider extends React.Component {
|
|||||||
//
|
//
|
||||||
// window.localStorage.removeItem(`${LOCAL_STORAGE_PREFIX}${id}`);
|
// window.localStorage.removeItem(`${LOCAL_STORAGE_PREFIX}${id}`);
|
||||||
const cached_n = this.getItem(id);
|
const cached_n = this.getItem(id);
|
||||||
cached_n = {
|
const closed_cached_n = {
|
||||||
...cached_n,
|
...cached_n,
|
||||||
status: Status.CLOSED
|
status: Status.CLOSED
|
||||||
};
|
};
|
||||||
this.setItem(id, cached_n);
|
this.setItem(id, closed_cached_n);
|
||||||
}
|
}
|
||||||
|
|
||||||
getItem = id => {
|
getItem = id => {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
|
||||||
.container-gradient {
|
.container-gradient {
|
||||||
/* background: radial-gradient(farthest-corner at -0% 100%, #7247ff 30%, #00ffbe 95%); */
|
/* 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%;
|
background-size: 400% 400%;
|
||||||
|
|
||||||
-webkit-animation: gradientTransition 20s ease infinite;
|
-webkit-animation: gradientTransition 20s ease infinite;
|
||||||
@ -13,8 +14,10 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08);
|
box-shadow: 0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
user-select: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #6772e5;
|
/* color: #6772e5; */
|
||||||
|
color: #24292e;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
-webkit-align-items: center;
|
-webkit-align-items: center;
|
||||||
@ -47,10 +50,12 @@
|
|||||||
.button-container-alt a {
|
.button-container-alt a {
|
||||||
position: relative;
|
position: relative;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
box-shadow: 0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08);
|
box-shadow: 0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #6772e5;
|
/* color: #6772e5; */
|
||||||
|
color: #24292e;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
-webkit-align-items: center;
|
-webkit-align-items: center;
|
||||||
|
@ -18,14 +18,14 @@
|
|||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: #6772e5;
|
background: #24292e;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body, * {
|
html, body, * {
|
||||||
font-family: 'Inter UI', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
font-family: 'Inter UI', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||||
sans-serif;
|
sans-serif;
|
||||||
color: #1a1a1a;
|
color: #202124;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|