This commit is contained in:
Nicholas Zuber 2018-11-02 18:08:41 -04:00
parent 698d9d44a2
commit 4be84327ce
12 changed files with 368 additions and 150 deletions

View File

@ -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="#9065ff" d="M18,9 C18,4.03 13.97,0 9,0">
<path stroke="#6772e5" 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

View File

@ -1,6 +1,6 @@
import React from 'react';
import meteoriteSvg from './meteorite.svg';
import meteoritePng from './meteorite-v2-2.png';
// import meteoritePng from './meteorite-v2-2.png';
export default function LoadingIcon ({ style, size, ...props }) {
return (

View File

@ -1,18 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 415.481 415.481" style="enable-background:new 0 0 415.481 415.481;" xml:space="preserve">
<g>
<path d="M263.768,332.171c14.135,14.135,32.696,21.202,51.263,21.2c18.563-0.001,37.131-7.068,51.263-21.2 c13.693-13.692,21.234-31.898,21.234-51.263s-7.541-37.57-21.234-51.263c-28.266-28.266-74.258-28.267-102.526,0 C235.502,257.911,235.502,303.904,263.768,332.171z M315.031,221.439c15.235,0,30.471,5.799,42.07,17.398 c11.237,11.237,17.426,26.178,17.426,42.07s-6.189,30.833-17.426,42.07c-23.198,23.199-60.942,23.198-84.141,0 c-23.198-23.198-23.198-60.943,0-84.141C284.56,227.238,299.796,221.439,315.031,221.439z" style="fill: #9065ff"></path>
<path d="M386.093,209.846l-80.602-80.602c-2.538-2.538-6.654-2.538-9.192,0c-2.539,2.539-2.539,6.654,0,9.192l80.602,80.602 c34.115,34.115,34.115,89.624,0,123.739c-34.115,34.115-89.624,34.115-123.739,0L106.798,196.414c-2.538-2.538-6.654-2.538-9.192,0 c-2.539,2.539-2.539,6.654,0,9.192L243.969,351.97c19.592,19.592,45.327,29.388,71.062,29.388c25.735,0,51.47-9.796,71.062-29.388 C425.277,312.786,425.277,249.029,386.093,209.846z" style="fill: #9065ff"></path>
<path d="M271.905,114.043c1.269,1.269,2.933,1.904,4.596,1.904s3.327-0.635,4.596-1.904c2.539-2.539,2.539-6.654,0-9.192 L239.73,63.482c-2.538-2.538-6.654-2.538-9.192,0c-2.539,2.539-2.539,6.654,0,9.192L271.905,114.043z" style="fill: #9065ff"></path>
<path d="M11.096,47.326c-2.538-2.538-6.654-2.538-9.192,0c-2.539,2.539-2.539,6.654,0,9.192l39.2,39.2 c1.269,1.269,2.933,1.904,4.596,1.904s3.327-0.635,4.596-1.904c2.539-2.539,2.539-6.654,0-9.192L11.096,47.326z" style="fill: #9065ff"></path>
<path d="M215.869,267.791c1.664,0,3.327-0.635,4.596-1.904c2.539-2.539,2.539-6.654,0-9.192l-155.37-155.37 c-2.538-2.538-6.654-2.538-9.192,0c-2.539,2.539-2.539,6.654,0,9.192l155.37,155.37 C212.542,267.156,214.206,267.791,215.869,267.791z" style="fill: #9065ff"></path>
<path d="M203.319,178.388c1.269,1.269,2.933,1.904,4.596,1.904s3.327-0.635,4.596-1.904c2.539-2.539,2.539-6.654,0-9.192 l-82.728-82.728c-2.538-2.538-6.654-2.538-9.192,0c-2.539,2.539-2.539,6.654,0,9.192L203.319,178.388z" style="fill: #9065ff"></path>
<path d="M241.5,216.569c1.269,1.269,2.933,1.904,4.596,1.904c1.664,0,3.327-0.635,4.596-1.904c2.539-2.539,2.539-6.654,0-9.192 l-18.03-18.03c-2.538-2.538-6.654-2.538-9.192,0c-2.539,2.539-2.539,6.654,0,9.192L241.5,216.569z" style="fill: #9065ff"></path>
<path d="M210.213,105.736c1.269,1.269,2.933,1.904,4.596,1.904s3.327-0.635,4.596-1.904c2.539-2.539,2.539-6.654,0-9.192 l-60.517-60.517c-2.538-2.538-6.654-2.538-9.192,0c-2.539,2.539-2.539,6.654,0,9.192L210.213,105.736z" style="fill: #9065ff"></path>
<path d="M240.617,117.756c-2.538-2.538-6.654-2.538-9.192,0c-2.539,2.539-2.539,6.654,0,9.192l59.394,59.394 c1.269,1.269,2.933,1.904,4.596,1.904s3.327-0.635,4.596-1.904c2.539-2.539,2.539-6.654,0-9.192L240.617,117.756z" style="fill: #9065ff"></path>
<path d="M291.916,282.231c10.614,0,19.25-8.635,19.25-19.249c0-10.614-8.635-19.25-19.25-19.25 c-10.614,0-19.249,8.635-19.249,19.25C272.667,273.595,281.302,282.231,291.916,282.231z M291.916,256.732 c3.446,0,6.25,2.804,6.25,6.25s-2.804,6.249-6.25,6.249s-6.249-2.803-6.249-6.249S288.47,256.732,291.916,256.732z" style="fill: #9065ff"></path>
<path d="M343.67,320.479c14.288,0,25.912-11.624,25.912-25.912c0-14.288-11.624-25.912-25.912-25.912 c-14.288,0-25.912,11.624-25.912,25.912C317.758,308.855,329.382,320.479,343.67,320.479z M343.67,281.655 c7.12,0,12.912,5.792,12.912,12.912s-5.792,12.912-12.912,12.912s-12.912-5.792-12.912-12.912S336.55,281.655,343.67,281.655z" style="fill: #9065ff"></path>
<path d="M294.915,303.991c-1.905-4.641-8.646-5.429-11.4-1.12c-2.745,4.001,0.019,9.587,4.765,10.079 C293.023,313.44,296.841,308.413,294.915,303.991C294.755,303.601,295.085,304.381,294.915,303.991z" style="fill: #9065ff"></path>
<path d="M329.415,250.471c1.902,4.647,8.646,5.429,11.4,1.12c1.765-2.572,1.324-6.174-0.92-8.305 c-2.156-2.047-5.615-2.402-8.09-0.705C329.243,244.216,328.236,247.696,329.415,250.471 C329.575,250.861,329.245,250.071,329.415,250.471z" style="fill: #9065ff"></path>
<path d="M304.915,321.481c0,3.536,2.962,6.5,6.5,6.5c3.536,0,6.5-2.966,6.5-6.5c0-3.544-2.956-6.5-6.5-6.5 C307.871,314.981,304.915,317.937,304.915,321.481z" style="fill: #9065ff"></path>
<path d="M263.768,332.171c14.135,14.135,32.696,21.202,51.263,21.2c18.563-0.001,37.131-7.068,51.263-21.2 c13.693-13.692,21.234-31.898,21.234-51.263s-7.541-37.57-21.234-51.263c-28.266-28.266-74.258-28.267-102.526,0 C235.502,257.911,235.502,303.904,263.768,332.171z M315.031,221.439c15.235,0,30.471,5.799,42.07,17.398 c11.237,11.237,17.426,26.178,17.426,42.07s-6.189,30.833-17.426,42.07c-23.198,23.199-60.942,23.198-84.141,0 c-23.198-23.198-23.198-60.943,0-84.141C284.56,227.238,299.796,221.439,315.031,221.439z" style="fill: #6772e5"></path>
<path d="M386.093,209.846l-80.602-80.602c-2.538-2.538-6.654-2.538-9.192,0c-2.539,2.539-2.539,6.654,0,9.192l80.602,80.602 c34.115,34.115,34.115,89.624,0,123.739c-34.115,34.115-89.624,34.115-123.739,0L106.798,196.414c-2.538-2.538-6.654-2.538-9.192,0 c-2.539,2.539-2.539,6.654,0,9.192L243.969,351.97c19.592,19.592,45.327,29.388,71.062,29.388c25.735,0,51.47-9.796,71.062-29.388 C425.277,312.786,425.277,249.029,386.093,209.846z" style="fill: #6772e5"></path>
<path d="M271.905,114.043c1.269,1.269,2.933,1.904,4.596,1.904s3.327-0.635,4.596-1.904c2.539-2.539,2.539-6.654,0-9.192 L239.73,63.482c-2.538-2.538-6.654-2.538-9.192,0c-2.539,2.539-2.539,6.654,0,9.192L271.905,114.043z" style="fill: #6772e5"></path>
<path d="M11.096,47.326c-2.538-2.538-6.654-2.538-9.192,0c-2.539,2.539-2.539,6.654,0,9.192l39.2,39.2 c1.269,1.269,2.933,1.904,4.596,1.904s3.327-0.635,4.596-1.904c2.539-2.539,2.539-6.654,0-9.192L11.096,47.326z" style="fill: #6772e5"></path>
<path d="M215.869,267.791c1.664,0,3.327-0.635,4.596-1.904c2.539-2.539,2.539-6.654,0-9.192l-155.37-155.37 c-2.538-2.538-6.654-2.538-9.192,0c-2.539,2.539-2.539,6.654,0,9.192l155.37,155.37 C212.542,267.156,214.206,267.791,215.869,267.791z" style="fill: #6772e5"></path>
<path d="M203.319,178.388c1.269,1.269,2.933,1.904,4.596,1.904s3.327-0.635,4.596-1.904c2.539-2.539,2.539-6.654,0-9.192 l-82.728-82.728c-2.538-2.538-6.654-2.538-9.192,0c-2.539,2.539-2.539,6.654,0,9.192L203.319,178.388z" style="fill: #6772e5"></path>
<path d="M241.5,216.569c1.269,1.269,2.933,1.904,4.596,1.904c1.664,0,3.327-0.635,4.596-1.904c2.539-2.539,2.539-6.654,0-9.192 l-18.03-18.03c-2.538-2.538-6.654-2.538-9.192,0c-2.539,2.539-2.539,6.654,0,9.192L241.5,216.569z" style="fill: #6772e5"></path>
<path d="M210.213,105.736c1.269,1.269,2.933,1.904,4.596,1.904s3.327-0.635,4.596-1.904c2.539-2.539,2.539-6.654,0-9.192 l-60.517-60.517c-2.538-2.538-6.654-2.538-9.192,0c-2.539,2.539-2.539,6.654,0,9.192L210.213,105.736z" style="fill: #6772e5"></path>
<path d="M240.617,117.756c-2.538-2.538-6.654-2.538-9.192,0c-2.539,2.539-2.539,6.654,0,9.192l59.394,59.394 c1.269,1.269,2.933,1.904,4.596,1.904s3.327-0.635,4.596-1.904c2.539-2.539,2.539-6.654,0-9.192L240.617,117.756z" style="fill: #6772e5"></path>
<path d="M291.916,282.231c10.614,0,19.25-8.635,19.25-19.249c0-10.614-8.635-19.25-19.25-19.25 c-10.614,0-19.249,8.635-19.249,19.25C272.667,273.595,281.302,282.231,291.916,282.231z M291.916,256.732 c3.446,0,6.25,2.804,6.25,6.25s-2.804,6.249-6.25,6.249s-6.249-2.803-6.249-6.249S288.47,256.732,291.916,256.732z" style="fill: #6772e5"></path>
<path d="M343.67,320.479c14.288,0,25.912-11.624,25.912-25.912c0-14.288-11.624-25.912-25.912-25.912 c-14.288,0-25.912,11.624-25.912,25.912C317.758,308.855,329.382,320.479,343.67,320.479z M343.67,281.655 c7.12,0,12.912,5.792,12.912,12.912s-5.792,12.912-12.912,12.912s-12.912-5.792-12.912-12.912S336.55,281.655,343.67,281.655z" style="fill: #6772e5"></path>
<path d="M294.915,303.991c-1.905-4.641-8.646-5.429-11.4-1.12c-2.745,4.001,0.019,9.587,4.765,10.079 C293.023,313.44,296.841,308.413,294.915,303.991C294.755,303.601,295.085,304.381,294.915,303.991z" style="fill: #6772e5"></path>
<path d="M329.415,250.471c1.902,4.647,8.646,5.429,11.4,1.12c1.765-2.572,1.324-6.174-0.92-8.305 c-2.156-2.047-5.615-2.402-8.09-0.705C329.243,244.216,328.236,247.696,329.415,250.471 C329.575,250.861,329.245,250.071,329.415,250.471z" style="fill: #6772e5"></path>
<path d="M304.915,321.481c0,3.536,2.962,6.5,6.5,6.5c3.536,0,6.5-2.966,6.5-6.5c0-3.544-2.956-6.5-6.5-6.5 C307.871,314.981,304.915,317.937,304.915,321.481z" style="fill: #6772e5"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -13,7 +13,8 @@ const Container = styled('div')({
borderRadius: 4,
margin: '0 auto',
padding: '24px 48px 76px',
width: 300
width: 300,
boxShadow: '0 50px 100px rgba(50,50,93,.1), 0 15px 35px rgba(50,50,93,.15), 0 5px 15px rgba(0,0,0,.1)'
});
const ButtonsContainer = styled('div')({

View File

@ -1,6 +1,5 @@
import React from 'react';
import { Link } from "@reach/router";
import moment from 'moment';
import styled from 'react-emotion';
import Icon from '../../components/Icon';
import Logo from '../../components/Logo';
@ -10,9 +9,12 @@ import { Filters } from '../../constants/filters';
import { withOnEnter } from '../../enhance';
import '../../styles/gradient.css';
const FixedContainer = styled('div')({
position: 'fixed'
});
const NotificationsContainer = styled('div')({
position: 'relative',
boxSizing: 'border-box',
background: '#fff',
margin: '0 auto',
padding: 0,
@ -36,41 +38,43 @@ const NavigationContainer = styled('div')({
const GeneralOptionsContainer = styled(NavigationContainer)({
background: '#fff',
padding: '8px 80px',
flex: '0 0 75px',
padding: '8px 16px',
paddingLeft: 260,
flex: '0 0 50px',
'button': {
display: 'inline-flex',
margin: 0,
marginTop: 12
margin: 0
}
});
const Sidebar = styled('div')({
flex: '0 0 180px',
flex: '0 0 200px',
padding: '0 20px 20px',
marginTop: 15,
'a': {
textAlign: 'left',
margin: '0 auto',
cursor: 'pointer',
borderRadius: '8px',
alignItems: 'center',
padding: '0 16px',
height: '48px',
fontSize: '12px',
fontWeight: '700',
textTransform: 'uppercase',
textDecoration: 'none',
transition: 'background 0.12s ease-in-out',
display: 'flex'
}
marginTop: 15
});
const SidebarLink = styled('a')({}, ({active, color}) => ({
textAlign: 'left',
margin: '0 auto',
cursor: 'pointer',
borderRadius: '8px',
alignItems: 'center',
padding: '0 14px',
height: 40,
fontSize: '12px',
fontWeight: 600,
letterSpacing: 0.5,
textTransform: 'uppercase',
textDecoration: 'none',
transition: 'background 0.12s ease-in-out',
display: 'flex',
background: active ? color : 'none',
color: active ? '#fff' : '#1a1a1a',
':hover': {
background: active ? color: 'rgba(200, 200, 200, .25)'
},
'div': {
marginRight: 5
}
}));
@ -83,7 +87,6 @@ const Tab = styled('button')({
border: 0,
outline: 'none',
background: 'none',
display: 'block',
height: 40,
width: 40,
borderRadius: '100%',
@ -108,7 +111,7 @@ const SearchField = styled('div')({
float: 'left',
textAlign: 'left',
width: '50%',
boxShadow: '0 1px 3px #4a4a4a5c',
boxShadow: '0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08)',
margin: '0 auto',
background: '#fff',
borderRadius: '4px',
@ -147,23 +150,30 @@ const SearchInput = styled('input')({
});
const EnhancedSearchInput = withOnEnter(SearchInput);
const NotificationRow = styled('div')({
const NotificationRow = styled('tr')({
position: 'relative',
cursor: 'pointer',
borderBottom: '1px solid #f2f2f2',
display: 'block',
textAlign: 'left',
width: '100%',
margin: '0 auto',
background: '#fff',
padding: '16px 24px',
padding: '8px 16px',
transition: 'all 0.12s ease-in-out',
borderRadius: 8,
boxSizing: 'border-box',
':hover': {
background: '#f9f9f9'
// background: '#f9f9f9',
boxShadow: '0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08)',
zIndex: 10
}
});
const NotificationTitle = styled('div')({
const NotificationTab = styled(Tab)({
margin: 0,
});
const NotificationTitle = styled('span')({
position: 'relative',
}, ({img}) => img && ({
paddingLeft: 20,
@ -180,15 +190,30 @@ const NotificationTitle = styled('div')({
}));
const Repository = styled('span')({
fontWeight: 600,
fontWeight: 500,
marginLeft: 10,
fontSize: 16
fontSize: 14
});
const PRIssue = styled(Repository)({
fontWeight: 400,
});
const Table = styled('table')({
display: 'block',
'td': {
display: 'inline-block'
}
});
const TableItem = styled('td')({
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}, ({width}) => ({
width
}));
function getPRIssueIcon (type, reason) {
const grow = 1.2;
@ -231,14 +256,17 @@ export default function Scene ({
n.reason !== 'manual' &&
n.reason !== 'invitation'
);
break;
break;
case Filters.SUBSCRIBED:
filterMethod = n => n.reason === 'subscribed';
break;
case Filters.ALL:
default:
filterMethod = () => true;
}
notifications = notifications
.sort((a, b) => b.repository.name.localeCompare(a.repository.name))
.sort((a, b) => a.repository.localeCompare(b.repository))
.filter(filterMethod);
return (
@ -276,13 +304,13 @@ export default function Scene ({
<Tab disabled={isLoading}>
<Icon.Refresh
opacity={0.9}
onClick={!isLoading && onFetchNotifications}
onClick={!isLoading ? (() => onFetchNotifications()) : undefined}
/>
</Tab>
<Tab disabled={isLoading}>
<Icon.DoneAll
opacity={0.9}
onClick={!isLoading && (() => onMarkAsRead('402658026'))}
onClick={!isLoading ? (() => onMarkAsRead('402658026')) : undefined}
/>
</Tab>
<Tab>
@ -301,58 +329,60 @@ export default function Scene ({
</GeneralOptionsContainer>
<NotificationsContainer>
<Sidebar>
<SidebarLink
active={activeFilter === Filters.ALL}
color="#9065ff"
onClick={() => onSetActiveFilter(Filters.ALL)}
>
{activeFilter === Filters.ALL ? (
<Icon.InboxWhite shrink={.6} />
) : (
<Icon.Inbox shrink={.6} />
)}
all notifications
</SidebarLink>
<SidebarLink
active={activeFilter === Filters.REVIEW_REQUESTED}
color="#2cbf73"
onClick={() => onSetActiveFilter(Filters.REVIEW_REQUESTED)}
>
{activeFilter === Filters.REVIEW_REQUESTED ? (
<Icon.BoltWhite shrink={.6} />
) : (
<Icon.Bolt shrink={.6} />
)}
review requested
</SidebarLink>
<SidebarLink
active={activeFilter === Filters.PARTICIPATING}
color="#00A0F5"
onClick={() => onSetActiveFilter(Filters.PARTICIPATING)}
>
{activeFilter === Filters.PARTICIPATING ? (
<Icon.PeopleWhite shrink={.6} />
) : (
<Icon.People shrink={.6} />
)}
participating
</SidebarLink>
<SidebarLink
active={activeFilter === Filters.SUBSCRIBED}
color="#f68700"
onClick={() => onSetActiveFilter(Filters.SUBSCRIBED)}
>
<Icon.Bookmark shrink={.6} />
subscribed
</SidebarLink>
<SidebarLink
active={activeFilter === Filters.HOT}
color="#ee3044"
onClick={() => onSetActiveFilter(Filters.HOT)}
>
<Icon.Hot shrink={.6} />
hot
</SidebarLink>
<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.REVIEW_REQUESTED}
color="#2cbf73"
onClick={() => onSetActiveFilter(Filters.REVIEW_REQUESTED)}
>
{activeFilter === Filters.REVIEW_REQUESTED ? (
<Icon.BoltWhite shrink={.6} />
) : (
<Icon.Bolt shrink={.6} />
)}
review requested
</SidebarLink>
<SidebarLink
active={activeFilter === Filters.PARTICIPATING}
color="#00A0F5"
onClick={() => onSetActiveFilter(Filters.PARTICIPATING)}
>
{activeFilter === Filters.PARTICIPATING ? (
<Icon.PeopleWhite shrink={.6} />
) : (
<Icon.People shrink={.6} />
)}
participating
</SidebarLink>
<SidebarLink
active={activeFilter === Filters.SUBSCRIBED}
color="#f68700"
onClick={() => onSetActiveFilter(Filters.SUBSCRIBED)}
>
<Icon.Bookmark shrink={.6} />
subscribed
</SidebarLink>
<SidebarLink
active={activeFilter === Filters.HOT}
color="#ee3044"
onClick={() => onSetActiveFilter(Filters.HOT)}
>
<Icon.Hot shrink={.6} />
hot
</SidebarLink>
</FixedContainer>
</Sidebar>
<Notifications>
{isFetchingNotifications ? (
@ -364,24 +394,37 @@ export default function Scene ({
<p>no notifications</p>
</div>
) : (
<div>
{notifications.map(n => (
<NotificationRow key={n.id}>
<NotificationTitle>
<div style={{
float: 'left',
marginTop: -3
}}>
{getPRIssueIcon(n.subject.type, n.reason)}
</div>
<PRIssue>{n.subject.title} ({n.reason})</PRIssue>
{/* <Repository>{n.repository.name}</Repository> */}
</NotificationTitle>
{/* <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>
))}
</div>
<Table>
<tbody>
{notifications.map(n => (
<NotificationRow key={n.id}>
<TableItem>
<div style={{ float: 'left', marginTop: -3 }}>
{getPRIssueIcon(n.type, n.reason)}
</div>
</TableItem>
<TableItem width={500} onClick={() => window.open(n.url)}>
<NotificationTitle>
<PRIssue>{n.name} ({n.reason})</PRIssue>
</NotificationTitle>
</TableItem>
<TableItem width={200}>
<Repository>{n.repository}</Repository>
</TableItem>
<TableItem>
<NotificationTab>
<Icon.Done
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>
<Sidebar>

View File

@ -1,9 +1,17 @@
import React from 'react';
import {AuthConsumer} from './Auth';
import {StorageProvider} from './Storage';
import {MockNotifications} from '../utils/mocks';
const BASE_GITHUB_API_URL = 'https://api.github.com';
function subjectUrlToIssue (url) {
return url
.replace('api.github.com', 'github.com')
.replace('/repos/', '/')
.replace('/pulls/', '/pull/');
}
function processHeadersAndBodyJson (response) {
const entries = response.headers.entries();
const headers = {};
@ -29,7 +37,19 @@ function processHeadersAndBodyJson (response) {
// links.next.page
headers['link'] = links;
return [headers, response.json()];
// 304 will usually mean nothing has changed from our last fetch.
if (response.status === 304) {
return Promise.resolve({
headers,
json: null
});
}
// I can't get marking a notification as read to get past here??
return response.json().then(json => ({
headers,
json
}));
}
class NotificationsProvider extends React.Component {
@ -41,17 +61,16 @@ class NotificationsProvider extends React.Component {
state = {
loading: false,
error: null,
notifications: MockNotifications
error: null
}
requestPage = (page = 1) => {
requestPage = (page = 1, optimizePolling = true) => {
const headers = {
'Authorization': `token ${this.props.token}`,
'Content-Type': 'application/json',
};
if (this.last_modified) {
if (optimizePolling && this.last_modified) {
headers['If-Modified-Since'] = this.last_modified;
}
@ -60,13 +79,16 @@ class NotificationsProvider extends React.Component {
headers: headers
})
.then(processHeadersAndBodyJson)
.then(([headers, body]) => {
.then(({headers, json}) => {
// If there were updates, make sure we get the newest last-modified.
if (headers['last-modified']) {
this.last_modified = headers['last-modified'];
}
return body;
return {
headers,
json
};
});
}
@ -77,24 +99,65 @@ class NotificationsProvider extends React.Component {
});
}
fetchNotifications = () => {
fetchNotifications = (page = 1, optimizePolling = true) => {
if (!this.props.token) {
console.error('Unauthenitcated, aborting request.')
return false;
}
this.setState({ loading: true });
return this.mockRequestPage(1)
.then(notifications => this.processNotificationsChunk(notifications))
.catch(error => this.setState({ error }))
return this.requestPage(page, optimizePolling)
.then(({headers, json}) => {
if (json === null) return;
let nextPage = null;
const links = headers['link'];
if (links && links.next && links.next.page) {
nextPage = links.next.page;
}
return this.processNotificationsChunk(nextPage, json);
})
.catch(error => console.error(error) || this.setState({ error }))
.finally(() => this.setState({ loading: false }));
}
processNotificationsChunk = notificationsChunk => {
console.warn(notificationsChunk);
this.setState({
notifications: notificationsChunk
processNotificationsChunk = (nextPage, notificationsChunk) => {
console.warn('chunk', notificationsChunk)
let everythingUpdated = true;
notificationsChunk.forEach(n => {
const cached_n = this.props.getItemFromStorage(n.id);
// If we've seen this notification before and it hasn't updated, skip it.
if (cached_n && (cached_n.updated_at === n.updated_at)) {
// 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;
return;
}
// Else, update the cache.
this.updateNotification(n);
});
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 => {
const value = {
id: n.id, // @TODO can prob remove this id since its the key
updated_at: n.updated_at,
reason: n.reason,
type: n.subject.type,
name: n.subject.title,
url: subjectUrlToIssue(n.subject.url),
repository: n.repository.name,
};
this.props.setItemInStorage(n.id, value);
}
requestMarkAsRead = thread_id => {
@ -108,9 +171,12 @@ class NotificationsProvider extends React.Component {
headers: headers
})
.then(processHeadersAndBodyJson)
.then(([headers, body]) => {
console.warn(body)
return body;
.then(({headers, json}) => {
console.warn(headers, json);
console.warn('removing', thread_id);
this.props.removeItemFromStorage(thread_id);
this.props.refreshNotifications();
return Promise.resolve(json);
});
}
@ -130,6 +196,7 @@ class NotificationsProvider extends React.Component {
render () {
return this.props.children({
...this.state,
notifications: this.props.notifications,
fetchNotifications: this.fetchNotifications,
markAsRead: this.markAsRead
});
@ -139,11 +206,22 @@ class NotificationsProvider extends React.Component {
const withNotificationsProvider = WrappedComponent => props => (
<AuthConsumer>
{({ token }) => (
<NotificationsProvider token={token}>
{(notificationsApi) => (
<WrappedComponent {...props} notificationsApi={notificationsApi} />
<StorageProvider>
{({ refreshNotifications, notifications, getItem, setItem, removeItem }) => (
<NotificationsProvider
refreshNotifications={refreshNotifications}
notifications={notifications}
getItemFromStorage={getItem}
setItemInStorage={setItem}
removeItemFromStorage={removeItem}
token={token}
>
{(notificationsApi) => (
<WrappedComponent {...props} notificationsApi={notificationsApi} />
)}
</NotificationsProvider>
)}
</NotificationsProvider>
</StorageProvider>
)}
</AuthConsumer>
);

79
src/providers/Storage.js Normal file
View File

@ -0,0 +1,79 @@
import React from 'react';
const LOCAL_STORAGE_PREFIX = '__meteorite_noti_cache__';
class StorageProvider extends React.Component {
constructor (props) {
super(props);
this.last_modified = null;
}
state = {
loading: false,
error: null,
notifications: {}
}
// @TODO move all this storage stuff to its own provider
// this guy is concerned about updating the cache and syncing
// the storage provider will be concerned about providing the notifications.
componentWillMount () {
this.refreshNotifications();
}
/**
* Loads up the notifications state with the cache.
*/
refreshNotifications = () => {
const notifications = [];
Object.keys(localStorage).forEach(key => {
if (key.indexOf(LOCAL_STORAGE_PREFIX) > -1) {
const n = JSON.parse(localStorage.getItem(key));
notifications.push(n);
}
});
this.setState({
notifications
});
}
setItem = (id, value) => {
localStorage.setItem(`${LOCAL_STORAGE_PREFIX}${id}`, JSON.stringify(value));
}
removeItem = id => {
localStorage.removeItem(`${LOCAL_STORAGE_PREFIX}${id}`);
}
getItem = id => {
try {
return JSON.parse(localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${id}`));
} catch (e) {
return null;
}
}
render () {
return this.props.children({
...this.state,
setItem: this.setItem,
getItem: this.getItem,
removeItem: this.removeItem,
refreshNotifications: this.refreshNotifications
});
}
}
const withStorageProvider = WrappedComponent => props => (
<StorageProvider>
{(storageApi) => (
<WrappedComponent {...props} storageApi={storageApi} />
)}
</StorageProvider>
);
export {
StorageProvider,
withStorageProvider
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,7 +1,7 @@
.container-gradient {
/* background: radial-gradient(farthest-corner at -0% 100%, #7247ff 30%, #00ffbe 95%); */
background: radial-gradient(farthest-corner at -0% 100%, #9065ff 30%, #58fff7 95%);
background: radial-gradient(farthest-corner at -0% 100%, #6772e5 30%, #00cfff 95%);
background-size: 400% 400%;
-webkit-animation: gradientTransition 20s ease infinite;
@ -11,10 +11,10 @@
.button-container a {
text-align: center;
box-shadow: 0 1px 3px #4a4a4a5c;
box-shadow: 0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08);
margin: 0 auto;
cursor: pointer;
color: #9065ff;
color: #6772e5;
background: #fff;
border-radius: 4px;
-webkit-align-items: center;

View File

@ -1,14 +1,31 @@
@import url('https://rsms.me/inter/inter-ui.css');
@font-face {
font-family: 'Camphor';
font-weight: 500;
src: url('./fonts/Camphor-Regular.ttf');
}
@font-face {
font-family: 'Camphor';
font-weight: 300;
src: url('./fonts/Camphor-Light.ttf');
}
@font-face {
font-family: 'Camphor';
font-weight: 700;
src: url('./fonts/Camphor-Heavy.ttf');
}
::selection {
color: #fff;
background: #9065ff;
background: #6772e5;
}
html, body, * {
font-family: 'Inter UI', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
font-family: 'Camphor', 'Inter UI', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
color: #1a1a1a;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}