Add a lot of the functionality

This commit is contained in:
Nicholas Zuber 2019-03-31 22:13:30 -04:00
parent e20690001e
commit 1d73c9fc10
7 changed files with 988 additions and 707 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import meteoriteLogo from './logo-white.png'; import meteoriteLogo from './icon-black.png';
export default function LoadingIcon ({ style, size, ...props }) { export default function LoadingIcon ({ style, size, ...props }) {
return ( return (

File diff suppressed because it is too large Load Diff

View File

@ -148,7 +148,8 @@ class NotificationsPage extends React.Component {
activeStatus: View.UNREAD, activeStatus: View.UNREAD,
currentPage: 1, currentPage: 1,
sort: Sort.SCORE, sort: Sort.SCORE,
descending: false descending: false,
user: null
} }
componentDidMount () { componentDidMount () {
@ -160,6 +161,9 @@ class NotificationsPage extends React.Component {
} }
this.props.notificationsApi.fetchNotifications(); this.props.notificationsApi.fetchNotifications();
this.props.notificationsApi.requestUser().then(user => {
this.setState({user});
});
this.tabSyncer = setInterval(() => { this.tabSyncer = setInterval(() => {
if (!document.hidden && this.isUnreadTab) { if (!document.hidden && this.isUnreadTab) {
@ -216,6 +220,7 @@ class NotificationsPage extends React.Component {
// Ignore empty queries. // Ignore empty queries.
if (text.length <= 0) { if (text.length <= 0) {
this.onClearQuery();
return; return;
} }
@ -335,9 +340,9 @@ class NotificationsPage extends React.Component {
const filteredNotifications = notifications.filter(filterMethod); const filteredNotifications = notifications.filter(filterMethod);
const notificationsQueued = filteredNotifications.filter(n => n.status === Status.QUEUED); let notificationsQueued = filteredNotifications.filter(n => n.status === Status.QUEUED);
const notificationsStaged = filteredNotifications.filter(n => n.status === Status.STAGED); let notificationsStaged = filteredNotifications.filter(n => n.status === Status.STAGED);
const notificationsClosed = filteredNotifications.filter(n => n.status === Status.CLOSED); let notificationsClosed = filteredNotifications.filter(n => n.status === Status.CLOSED);
let notificationsToRender = []; let notificationsToRender = [];
switch (this.state.activeStatus) { switch (this.state.activeStatus) {
@ -389,7 +394,16 @@ class NotificationsPage extends React.Component {
if (this.state.query) { if (this.state.query) {
scoredAndSortedNotifications = scoredAndSortedNotifications.filter(n => ( scoredAndSortedNotifications = scoredAndSortedNotifications.filter(n => (
n.name.toLowerCase().indexOf(this.state.query.toLowerCase()) > -1) n.name.toLowerCase().indexOf(this.state.query.toLowerCase()) > -1)
) );
notificationsQueued = notificationsQueued.filter(n => (
n.name.toLowerCase().indexOf(this.state.query.toLowerCase()) > -1)
);
notificationsStaged = notificationsStaged.filter(n => (
n.name.toLowerCase().indexOf(this.state.query.toLowerCase()) > -1)
);
notificationsClosed = notificationsClosed.filter(n => (
n.name.toLowerCase().indexOf(this.state.query.toLowerCase()) > -1)
);
} }
if (this.props.notificationsApi.newChanges) { if (this.props.notificationsApi.newChanges) {
@ -465,9 +479,9 @@ class NotificationsPage extends React.Component {
isFirstTimeUser={this.state.isFirstTimeUser} isFirstTimeUser={this.state.isFirstTimeUser}
setNotificationsPermission={this.setNotificationsPermission} setNotificationsPermission={this.setNotificationsPermission}
notificationsPermission={notificationsPermission} notificationsPermission={notificationsPermission}
queuedCount={queuedCount} unreadCount={queuedCount}
stagedCount={stagedCount} readCount={stagedCount}
closedCount={closedCount} archivedCount={closedCount}
stagedTodayCount={stagedTodayCount || 0} stagedTodayCount={stagedTodayCount || 0}
first={firstNumbered} first={firstNumbered}
last={lastNumbered} last={lastNumbered}
@ -503,6 +517,7 @@ class NotificationsPage extends React.Component {
setDescending={descending => this.setState({descending})} setDescending={descending => this.setState({descending})}
view={this.state.activeStatus} view={this.state.activeStatus}
setView={this.onSetActiveStatus} setView={this.onSetActiveStatus}
user={this.state.user}
/> />
); );
} }

View File

@ -3,10 +3,12 @@
import React from 'react'; import React from 'react';
import moment from 'moment'; import moment from 'moment';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import {css, jsx} from '@emotion/core'; import {css, jsx, keyframes} from '@emotion/core';
import {useSpring, useTransition, animated} from 'react-spring' import {useSpring, useTransition, animated} from 'react-spring'
import Icon from '../../../components/Icon'; import Icon from '../../../components/Icon';
import Logo from '../../../components/Logo';
import LoadingIcon from '../../../components/LoadingIcon'; import LoadingIcon from '../../../components/LoadingIcon';
import {Reasons, Badges} from '../../../constants/reasons';
import {withOnEnter} from '../../../enhance'; import {withOnEnter} from '../../../enhance';
import {Sort, View} from '../index'; import {Sort, View} from '../index';
@ -19,8 +21,21 @@ const Mode = {
COMMENTED: 2 COMMENTED: 2
}; };
// @TODO if GitHub ever fixes their API, we can use `reasons` to know when // ========================================================================
// a PR/Issue merges/closes/etc. // START OF 'MOVE TO A UTILS FILE'
// ========================================================================
function stringOfType (type) {
switch (type) {
case 'PullRequest':
return 'pull request';
case 'Issue':
return 'issue';
default:
return 'task';
}
}
function getPRIssueIcon (type, _reasons) { function getPRIssueIcon (type, _reasons) {
const scale = 1.5; const scale = 1.5;
switch (type) { switch (type) {
@ -43,21 +58,73 @@ function getPRIssueIcon (type, _reasons) {
return null; return null;
} }
} }
function getRelativeTime (time) {
const currentTime = moment();
const targetTime = moment(time);
const diffMinutes = currentTime.diff(targetTime, 'minutes');
if (diffMinutes < 1)
return 'Just now';
if (diffMinutes < 5)
return 'Few minutes ago';
if (diffMinutes < 60)
return diffMinutes + ' minutes ago';
if (diffMinutes < 60 * 24)
return Math.floor(diffMinutes / 60) + ' hours ago';
function colorOfScore (score, min, max) { const diffDays = currentTime.diff(targetTime, 'days');
const ratio = (score - min) / (max - min); if (diffDays === 1)
if (ratio > .9) return '#ec1461'; return 'Yesterday';
if (ratio > .8) return '#ec5314'; if (diffDays <= 7)
if (ratio > .7) return '#ec5314'; return 'Last ' + targetTime.format('dddd');
if (ratio > .6) return '#ec7b14'; // @TODO implement longer diffs
if (ratio > .5) return '#ec7b14'; return 'Over a week ago';
if (ratio > .4) return '#ec9914';
if (ratio > .3) return '#ec9914';
if (ratio > .2) return '#ecad14';
if (ratio > .1) return '#ecad14';
return '#ecc114';
} }
function getMessageFromReasons (reasons, type) {
switch (reasons[reasons.length - 1].reason) {
case Reasons.ASSIGN:
return 'You were assigned';
case Reasons.AUTHOR:
return 'There was activity on this thread you created';
case Reasons.COMMENT:
return 'Somebody left a comment';
case Reasons.MENTION:
return 'You were mentioned';
case Reasons.REVIEW_REQUESTED:
return 'Your review was requested';
case Reasons.SUBSCRIBED:
return 'There was an update and you\'re subscribed';
case Reasons.OTHER:
default:
return 'Something was updated';
}
}
// ========================================================================
// END OF 'MOVE TO A UTILS FILE'
// ========================================================================
function createColorOfScore (min, max) {
return function (score) {
const ratio = (score - min) / (max - min);
if (ratio > .9) return '#ec1461';
if (ratio > .8) return '#ec5314';
if (ratio > .7) return '#ec5314';
if (ratio > .6) return '#ec7b14';
if (ratio > .5) return '#ec7b14';
if (ratio > .4) return '#ec9914';
if (ratio > .3) return '#ec9914';
if (ratio > .2) return '#ecad14';
if (ratio > .1) return '#ecad14';
return '#ecc114';
}
}
const loadingKeyframe = keyframes`
100% {
transform: translateX(100%);
}
`;
const Container = styled('div')` const Container = styled('div')`
position: relative; position: relative;
display: block; display: block;
@ -219,7 +286,6 @@ const SearchField = styled('div')`
position: relative; position: relative;
float: left; float: left;
text-align: left; text-align: left;
// box-shadow: rgba(84,70,35,0.01) 0px 2px 19px 8px, rgba(84, 70, 35, 0.11) 0px 2px 12px;
align-items: center; align-items: center;
height: 36px; height: 36px;
font-size: 13px; font-size: 13px;
@ -236,6 +302,7 @@ const SearchField = styled('div')`
} }
&:focus-within { &:focus-within {
border: 1px solid #457cff; border: 1px solid #457cff;
box-shadow: rgba(84,70,35,0.01) 0px 2px 19px 8px, rgba(84, 70, 35, 0.11) 0px 2px 12px;
} }
i { i {
color: #bfc5d1; color: #bfc5d1;
@ -271,7 +338,7 @@ const SearchInput = styled('input')`
&:focus { &:focus {
opacity: 1; opacity: 1;
color: rgb(55, 53, 47); color: rgb(55, 53, 47);
width: 500px; width: 300px;
} }
`; `;
const EnhancedSearchInput = withOnEnter(SearchInput); const EnhancedSearchInput = withOnEnter(SearchInput);
@ -415,7 +482,7 @@ const NotificationRow = styled(NotificationRowHeader)`
position: relative; position: relative;
background: ${WHITE}; background: ${WHITE};
border-radius: 4px; border-radius: 4px;
padding: 2px 18px; padding: 6px 18px;
font-size: 14px; font-size: 14px;
margin-bottom: 12px; margin-bottom: 12px;
border-radius: 6px; border-radius: 6px;
@ -424,14 +491,42 @@ const NotificationRow = styled(NotificationRowHeader)`
transition: all 200ms ease; transition: all 200ms ease;
&:hover { &:hover {
box-shadow: rgba(84,70,35,0.01) 0px 2px 19px 8px, rgba(84, 70, 35, 0.11) 0px 2px 12px; box-shadow: rgba(84,70,35,0.01) 0px 2px 19px 8px, rgba(84, 70, 35, 0.11) 0px 2px 12px;
// background: rgb(252, 250, 248); };
&:active {
background: rgb(252, 250, 248);
}; };
`; `;
const NotificationBlock = styled('tbody')` const LoadingNotificationRow = styled(NotificationRowHeader)`
position: relative;
background: ${WHITE};
height: 62px;
overflow: hidden;
border-radius: 4px;
padding: 6px 18px;
font-size: 14px;
margin-bottom: 12px;
border-radius: 6px;
cursor: pointer;
user-select: none;
transition: all 200ms ease;
opacity: 0.75;
&:after {
background: linear-gradient(90deg, transparent, #f9f8f5, transparent);
display: block;
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
transform: translateX(-100%);
animation: ${loadingKeyframe} 2.0s infinite;
}
`; `;
const NotificationBlock = styled('tbody')``;
const AnimatedNotificationRow = animated(NotificationRow); const AnimatedNotificationRow = animated(NotificationRow);
const AnimatedNotificationsBlock = animated(NotificationBlock); const AnimatedNotificationsBlock = animated(NotificationBlock);
@ -448,6 +543,65 @@ const NotificationCell = styled('td')`
}}; }};
`; `;
const NotificationTitle = styled('span')``;
const NotificationByline = styled('span')`
display: block;
margin-top: 4px;
font-size: 12px;
color: #8893a7cc;
i {
margin-right: 4px;
font-size: 10px;
color: #8893a7cc;
}
span {
margin-left: 12px;
font-size: 12px;
font-weight: 500;
color: #8893a7cc;
i {
margin-right: 4px;
font-size: 9px;
color: #8893a7cc;
}
}
`;
const ProfileContainer = styled('div')`
display: flex;
justify-content: center;
align-items: center;
border-left: 1px solid #edeef0;
padding: 0 22px;
position: absolute;
right: 0;
transition: all 200ms ease;
user-select: none;
cursor: pointer;
i {
transition: all 200ms ease;
color: #bfc5d1a3
}
&:hover {
background: rgba(233, 233, 233, .25);
i {
color: #bfc5d1
}
}
`;
const ProfileName = styled('span')`
font-size: 14px;
font-weight: 500;
margin: 0 12px;
`;
const ProfilePicture = styled('img')`
height: 36px;
width: 36px;
border-radius: 4px;
`;
const NotificationIconWrapper = styled('div')` const NotificationIconWrapper = styled('div')`
background: #DBE7FF; background: #DBE7FF;
width: 48px; width: 48px;
@ -535,7 +689,7 @@ function SortingItem ({children, selected, onChange, descending, setDescending,
if (selected) { if (selected) {
setDescending(!descending); setDescending(!descending);
} else { } else {
setDescending(true); setDescending(false);
} }
onChange(props.sort) onChange(props.sort)
}} }}
@ -576,7 +730,14 @@ export default function Scene ({
onClearQuery, onClearQuery,
onSearch, onSearch,
isSearching, isSearching,
user,
onFetchNotifications,
onMarkAllAsStaged,
onClearCache,
setNotificationsPermission,
onStageThread,
}) { }) {
console.warn('unreadCount', unreadCount)
const [menuOpen, setMenuOpen] = React.useState(false); const [menuOpen, setMenuOpen] = React.useState(false);
const [dropdownOpen, setDropdownOpen] = React.useState(false); const [dropdownOpen, setDropdownOpen] = React.useState(false);
const [mode, setMode] = React.useState(Mode.ALL); const [mode, setMode] = React.useState(Mode.ALL);
@ -603,15 +764,6 @@ export default function Scene ({
// } // }
// }); // });
const props = useSpring({
from: {opacity: 0, transform: 'translate3d(50px, 0, 0)'},
to: {opacity: 1, transform: 'translate3d(0, 0, 0)'},
config: {
tension: 300,
duration: 200,
}
});
React.useEffect(() => { React.useEffect(() => {
const body = window.document.querySelector('body'); const body = window.document.querySelector('body');
const hideDropdownMenu = () => setDropdownOpen(false); const hideDropdownMenu = () => setDropdownOpen(false);
@ -621,7 +773,14 @@ export default function Scene ({
return ( return (
<Container> <Container>
<Row style={{height: COLLAPSED_WIDTH}}> <Row css={css`
position: fixed;
top: 0;
height: ${COLLAPSED_WIDTH};
background: ${WHITE};
z-index: 10;
width: 100%;
`}>
<MenuHeaderItem expand={menuOpen}> <MenuHeaderItem expand={menuOpen}>
<MenuIconItem <MenuIconItem
alwaysActive alwaysActive
@ -653,9 +812,26 @@ export default function Scene ({
backgroundColor: 'white' backgroundColor: 'white'
}} />} }} />}
</SearchField> </SearchField>
<Logo
style={{left: '50%', marginLeft: -18, position: 'absolute', opacity: 0.25}}
onClick={() => {
window.scrollTo(0, 0);
}}
size={36}
/>
{user && (
<ProfileContainer>
<ProfilePicture src={user.avatar_url} />
<ProfileName>{user.name}</ProfileName>
<i className="fas fa-caret-down"></i>
</ProfileContainer>
)}
</ContentHeaderItem> </ContentHeaderItem>
</Row> </Row>
<Row style={{height: `calc(100% - ${COLLAPSED_WIDTH})`}}> <Row css={css`
height: calc(100% - ${COLLAPSED_WIDTH});
margin-top: ${COLLAPSED_WIDTH};
`}>
<MenuContainerItem expand={menuOpen}> <MenuContainerItem expand={menuOpen}>
<MenuIconItem <MenuIconItem
mode={Mode.ALL} mode={Mode.ALL}
@ -700,19 +876,46 @@ export default function Scene ({
</IconLink> </IconLink>
<InteractionMenu show={dropdownOpen}> <InteractionMenu show={dropdownOpen}>
<Card> <Card>
<div> <div onClick={event => {
event.stopPropagation();
onFetchNotifications();
setDropdownOpen(false);
}}>
<h2>Reload notifications</h2> <h2>Reload notifications</h2>
<p>Manually fetch new notifications instead of waiting for the sync</p> <p>Manually fetch new notifications instead of waiting for the sync</p>
</div> </div>
<div> <div onClick={event => {
event.stopPropagation();
const response = window.confirm('Are you sure you want to mark all your notifications as read?');
void (response && onMarkAllAsStaged());
setDropdownOpen(false);
}}>
<h2>Mark all as read</h2> <h2>Mark all as read</h2>
<p>Move all your unread notifications to the read tab</p> <p>Move all your unread notifications to the read tab</p>
</div> </div>
<div> <div onClick={event => {
event.stopPropagation();
const response = window.confirm('Are you sure you want to clear the cache?');
void (response && onClearCache());
setDropdownOpen(false);
}}>
<h2>Empty cache</h2> <h2>Empty cache</h2>
<p>Clear all the notifications that are being tracked in your local storage</p> <p>Clear all the notifications that are being tracked in your local storage</p>
</div> </div>
<div> <div onClick={event => {
event.stopPropagation();
switch(notificationsPermission) {
case 'granted':
return setNotificationsPermission('denied');
case 'denied':
case 'default':
default:
Notification.requestPermission().then(result => {
return setNotificationsPermission(result);
});
}
setDropdownOpen(false);
}}>
<h2>Turn {hasNotificationsOn ? 'off' : 'on'} notifications</h2> <h2>Turn {hasNotificationsOn ? 'off' : 'on'} notifications</h2>
<p> <p>
{hasNotificationsOn {hasNotificationsOn
@ -740,6 +943,7 @@ export default function Scene ({
{'Unread'} {'Unread'}
{unreadCount > 0 && ( {unreadCount > 0 && (
<span css={css` <span css={css`
transition: all 200ms ease;
background: ${view === View.UNREAD ? '#4880ff' : '#bfc5d1'}; background: ${view === View.UNREAD ? '#4880ff' : '#bfc5d1'};
color: ${WHITE}; color: ${WHITE};
font-size: 9px; font-size: 9px;
@ -762,6 +966,7 @@ export default function Scene ({
{'Read'} {'Read'}
{readCount > 0 && ( {readCount > 0 && (
<span css={css` <span css={css`
transition: all 200ms ease;
background: ${view === View.READ ? '#4880ff' : '#bfc5d1'}; background: ${view === View.READ ? '#4880ff' : '#bfc5d1'};
color: ${WHITE}; color: ${WHITE};
font-size: 9px; font-size: 9px;
@ -784,6 +989,7 @@ export default function Scene ({
{'Archived'} {'Archived'}
{archivedCount > 0 && ( {archivedCount > 0 && (
<span css={css` <span css={css`
transition: all 200ms ease;
background: ${view === View.ARCHIVED ? '#4880ff' : '#bfc5d1'}; background: ${view === View.ARCHIVED ? '#4880ff' : '#bfc5d1'};
color: ${WHITE}; color: ${WHITE};
font-size: 9px; font-size: 9px;
@ -815,7 +1021,7 @@ export default function Scene ({
margin-right: 8px; margin-right: 8px;
span { span {
font-size: 13px; font-size: 13px;
color: #797d8c; color: #37352f;
font-weight: 600; font-weight: 600;
vertical-align: text-top; vertical-align: text-top;
} }
@ -900,52 +1106,24 @@ export default function Scene ({
&nbsp; &nbsp;
</NotificationCell> </NotificationCell>
</NotificationRowHeader> </NotificationRowHeader>
<AnimatedNotificationsBlock style={props} page={page}> {loading ? (
{notifications.map(item => ( <NotificationBlock>
<AnimatedNotificationRow key={notifications.id}> <LoadingNotificationRow />
{/* Type */} <LoadingNotificationRow />
<NotificationCell width={80}> <LoadingNotificationRow />
{getPRIssueIcon(item.type, item.reasons)} <LoadingNotificationRow />
</NotificationCell> <LoadingNotificationRow />
{/* Title */} <LoadingNotificationRow />
<NotificationCell flex={4} css={css` <LoadingNotificationRow />
font-weight: 500; </NotificationBlock>
`}> ) : (
{item.name} <NotificationCollection
</NotificationCell> notifications={notifications}
{/* Repository */} page={page}
<NotificationCell flex={2} css={css` colorOfScore={createColorOfScore(lowestScore, highestScore)}
font-weight: 500; onTitleClick={onStageThread}
color: #8994A6; />
`}> )}
{'@' + item.repository}
</NotificationCell>
{/* Score */}
<NotificationCell width={60} css={css`
font-weight: 600;
color: ${colorOfScore(item.score, lowestScore, highestScore)};
font-size: 12px;
text-align: center;
`}>
{'+' + item.score}
</NotificationCell>
<NotificationCell width={80} css={css`
i {
padding: 13px 0;
text-align: center;
width: 40px;
}
`}>
<IconLink>
<i className="fas fa-check"></i>
</IconLink>
<IconLink>
<i className="fas fa-times"></i>
</IconLink>
</NotificationCell>
</AnimatedNotificationRow>
))}
</AnimatedNotificationsBlock>
</NotificationsTable> </NotificationsTable>
</NotificationsSection> </NotificationsSection>
</ContentItem> </ContentItem>
@ -953,3 +1131,82 @@ export default function Scene ({
</Container> </Container>
); );
} }
function NotificationCollection ({
notifications,
colorOfScore,
onTitleClick,
page
}) {
const props = useSpring({
from: {opacity: 0},
to: {opacity: 1},
config: {
duration: 200,
}
});
return (
<AnimatedNotificationsBlock style={props} page={page}>
{notifications.map(item => (
<AnimatedNotificationRow key={notifications.id}>
{/* Type */}
<NotificationCell width={80}>
{getPRIssueIcon(item.type, item.reasons)}
</NotificationCell>
{/* Title */}
<NotificationCell
flex={4}
onClick={() => {
window.open(item.url);
onTitleClick(item.id, item.repository);
}}
css={css`
font-weight: 500;
`}>
<NotificationTitle>
{item.name}
</NotificationTitle>
<NotificationByline>
{getMessageFromReasons(item.reasons, item.type)}
{` ${getRelativeTime(item.updated_at).toLowerCase()}`}
</NotificationByline>
</NotificationCell>
{/* Repository */}
<NotificationCell
flex={2}
onClick={() => window.open(item.repositoryUrl)}
css={css`
font-weight: 500;
color: #8994A6;
`}>
{'@' + item.repository}
</NotificationCell>
{/* Score */}
<NotificationCell width={60} css={css`
font-weight: 600;
color: ${colorOfScore(item.score)};
font-size: 12px;
text-align: center;
`}>
{'+' + item.score}
</NotificationCell>
<NotificationCell width={80} css={css`
i {
padding: 13px 0;
text-align: center;
width: 40px;
}
`}>
<IconLink>
<i className="fas fa-check"></i>
</IconLink>
<IconLink>
<i className="fas fa-times"></i>
</IconLink>
</NotificationCell>
</AnimatedNotificationRow>
))}
</AnimatedNotificationsBlock>
);
}

View File

@ -86,7 +86,9 @@ class NotificationsProvider extends React.Component {
} }
// Only update if our notifications prop changes. // Only update if our notifications prop changes.
// All other props "changing" should NOT trigger a rerender. // All other props "changing" should NOT trigger a rerender.
return this.props.notifications !== nextProps.notifications; return (
this.props.notifications !== nextProps.notifications
);
} }
// The web notificaitons API doesn't let users revoke notifications permission // The web notificaitons API doesn't let users revoke notifications permission
@ -99,6 +101,27 @@ class NotificationsProvider extends React.Component {
this.forceUpdate(); this.forceUpdate();
} }
requestUser = () => {
const headers = {
'Authorization': `token ${this.props.token}`,
'Content-Type': 'application/json',
};
// @TODO probably add timestamp
const cachedUser = this.props.getUserItem('user-model');
if (cachedUser) {
return Promise.resolve(cachedUser);
}
return fetch(`${BASE_GITHUB_API_URL}/user`, {
method: 'GET',
headers: headers
}).then(processHeadersAndBodyJson)
.then(({json}) => {
this.props.setUserItem('user-model', json);
});
}
requestPage = (page = 1, optimizePolling = true) => { requestPage = (page = 1, optimizePolling = true) => {
// Fetch all notifications from a month ago, including ones that have been read. // Fetch all notifications from a month ago, including ones that have been read.
// We can tell in the response if a notification has been read or not, so we can // We can tell in the response if a notification has been read or not, so we can
@ -395,6 +418,7 @@ class NotificationsProvider extends React.Component {
render () { render () {
return this.props.children({ return this.props.children({
...this.state, ...this.state,
requestUser: this.requestUser,
notifications: this.props.notifications, notifications: this.props.notifications,
fetchNotifications: this.fetchNotifications, fetchNotifications: this.fetchNotifications,
fetchNotificationsSync: this.requestFetchNotifications, fetchNotificationsSync: this.requestFetchNotifications,

View File

@ -18,12 +18,13 @@
::selection { ::selection {
color: #fff; color: #fff;
background: #24292e; background: #4880ff;
} }
html, body { html, body {
/* height: 100%; */ /* height: 100%; */
width: 100%; width: 100%;
scroll-behavior: smooth;
} }
html, body, * { html, body, * {