mirror of
https://github.com/nickzuber/meteorite.git
synced 2024-11-29 09:31:15 +03:00
Add a lot of the functionality
This commit is contained in:
parent
e20690001e
commit
1d73c9fc10
BIN
src/components/Logo/icon-black.png
Normal file
BIN
src/components/Logo/icon-black.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import meteoriteLogo from './logo-white.png';
|
||||
import meteoriteLogo from './icon-black.png';
|
||||
|
||||
export default function LoadingIcon ({ style, size, ...props }) {
|
||||
return (
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -148,7 +148,8 @@ class NotificationsPage extends React.Component {
|
||||
activeStatus: View.UNREAD,
|
||||
currentPage: 1,
|
||||
sort: Sort.SCORE,
|
||||
descending: false
|
||||
descending: false,
|
||||
user: null
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
@ -160,6 +161,9 @@ class NotificationsPage extends React.Component {
|
||||
}
|
||||
|
||||
this.props.notificationsApi.fetchNotifications();
|
||||
this.props.notificationsApi.requestUser().then(user => {
|
||||
this.setState({user});
|
||||
});
|
||||
|
||||
this.tabSyncer = setInterval(() => {
|
||||
if (!document.hidden && this.isUnreadTab) {
|
||||
@ -216,6 +220,7 @@ class NotificationsPage extends React.Component {
|
||||
|
||||
// Ignore empty queries.
|
||||
if (text.length <= 0) {
|
||||
this.onClearQuery();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -335,9 +340,9 @@ class NotificationsPage extends React.Component {
|
||||
|
||||
const filteredNotifications = notifications.filter(filterMethod);
|
||||
|
||||
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 notificationsQueued = filteredNotifications.filter(n => n.status === Status.QUEUED);
|
||||
let notificationsStaged = filteredNotifications.filter(n => n.status === Status.STAGED);
|
||||
let notificationsClosed = filteredNotifications.filter(n => n.status === Status.CLOSED);
|
||||
|
||||
let notificationsToRender = [];
|
||||
switch (this.state.activeStatus) {
|
||||
@ -389,7 +394,16 @@ class NotificationsPage extends React.Component {
|
||||
if (this.state.query) {
|
||||
scoredAndSortedNotifications = scoredAndSortedNotifications.filter(n => (
|
||||
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) {
|
||||
@ -465,9 +479,9 @@ class NotificationsPage extends React.Component {
|
||||
isFirstTimeUser={this.state.isFirstTimeUser}
|
||||
setNotificationsPermission={this.setNotificationsPermission}
|
||||
notificationsPermission={notificationsPermission}
|
||||
queuedCount={queuedCount}
|
||||
stagedCount={stagedCount}
|
||||
closedCount={closedCount}
|
||||
unreadCount={queuedCount}
|
||||
readCount={stagedCount}
|
||||
archivedCount={closedCount}
|
||||
stagedTodayCount={stagedTodayCount || 0}
|
||||
first={firstNumbered}
|
||||
last={lastNumbered}
|
||||
@ -503,6 +517,7 @@ class NotificationsPage extends React.Component {
|
||||
setDescending={descending => this.setState({descending})}
|
||||
view={this.state.activeStatus}
|
||||
setView={this.onSetActiveStatus}
|
||||
user={this.state.user}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -3,10 +3,12 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
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 Icon from '../../../components/Icon';
|
||||
import Logo from '../../../components/Logo';
|
||||
import LoadingIcon from '../../../components/LoadingIcon';
|
||||
import {Reasons, Badges} from '../../../constants/reasons';
|
||||
import {withOnEnter} from '../../../enhance';
|
||||
import {Sort, View} from '../index';
|
||||
|
||||
@ -19,8 +21,21 @@ const Mode = {
|
||||
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) {
|
||||
const scale = 1.5;
|
||||
switch (type) {
|
||||
@ -43,21 +58,73 @@ function getPRIssueIcon (type, _reasons) {
|
||||
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 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 diffDays = currentTime.diff(targetTime, 'days');
|
||||
if (diffDays === 1)
|
||||
return 'Yesterday';
|
||||
if (diffDays <= 7)
|
||||
return 'Last ' + targetTime.format('dddd');
|
||||
// @TODO implement longer diffs
|
||||
return 'Over a week ago';
|
||||
}
|
||||
|
||||
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')`
|
||||
position: relative;
|
||||
display: block;
|
||||
@ -219,7 +286,6 @@ const SearchField = styled('div')`
|
||||
position: relative;
|
||||
float: 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;
|
||||
height: 36px;
|
||||
font-size: 13px;
|
||||
@ -236,6 +302,7 @@ const SearchField = styled('div')`
|
||||
}
|
||||
&:focus-within {
|
||||
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 {
|
||||
color: #bfc5d1;
|
||||
@ -271,7 +338,7 @@ const SearchInput = styled('input')`
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
color: rgb(55, 53, 47);
|
||||
width: 500px;
|
||||
width: 300px;
|
||||
}
|
||||
`;
|
||||
const EnhancedSearchInput = withOnEnter(SearchInput);
|
||||
@ -415,7 +482,7 @@ const NotificationRow = styled(NotificationRowHeader)`
|
||||
position: relative;
|
||||
background: ${WHITE};
|
||||
border-radius: 4px;
|
||||
padding: 2px 18px;
|
||||
padding: 6px 18px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 6px;
|
||||
@ -424,14 +491,42 @@ const NotificationRow = styled(NotificationRowHeader)`
|
||||
transition: all 200ms ease;
|
||||
&:hover {
|
||||
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 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')`
|
||||
background: #DBE7FF;
|
||||
width: 48px;
|
||||
@ -535,7 +689,7 @@ function SortingItem ({children, selected, onChange, descending, setDescending,
|
||||
if (selected) {
|
||||
setDescending(!descending);
|
||||
} else {
|
||||
setDescending(true);
|
||||
setDescending(false);
|
||||
}
|
||||
onChange(props.sort)
|
||||
}}
|
||||
@ -576,7 +730,14 @@ export default function Scene ({
|
||||
onClearQuery,
|
||||
onSearch,
|
||||
isSearching,
|
||||
user,
|
||||
onFetchNotifications,
|
||||
onMarkAllAsStaged,
|
||||
onClearCache,
|
||||
setNotificationsPermission,
|
||||
onStageThread,
|
||||
}) {
|
||||
console.warn('unreadCount', unreadCount)
|
||||
const [menuOpen, setMenuOpen] = React.useState(false);
|
||||
const [dropdownOpen, setDropdownOpen] = React.useState(false);
|
||||
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(() => {
|
||||
const body = window.document.querySelector('body');
|
||||
const hideDropdownMenu = () => setDropdownOpen(false);
|
||||
@ -621,7 +773,14 @@ export default function Scene ({
|
||||
|
||||
return (
|
||||
<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}>
|
||||
<MenuIconItem
|
||||
alwaysActive
|
||||
@ -653,9 +812,26 @@ export default function Scene ({
|
||||
backgroundColor: 'white'
|
||||
}} />}
|
||||
</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>
|
||||
</Row>
|
||||
<Row style={{height: `calc(100% - ${COLLAPSED_WIDTH})`}}>
|
||||
<Row css={css`
|
||||
height: calc(100% - ${COLLAPSED_WIDTH});
|
||||
margin-top: ${COLLAPSED_WIDTH};
|
||||
`}>
|
||||
<MenuContainerItem expand={menuOpen}>
|
||||
<MenuIconItem
|
||||
mode={Mode.ALL}
|
||||
@ -700,19 +876,46 @@ export default function Scene ({
|
||||
</IconLink>
|
||||
<InteractionMenu show={dropdownOpen}>
|
||||
<Card>
|
||||
<div>
|
||||
<div onClick={event => {
|
||||
event.stopPropagation();
|
||||
onFetchNotifications();
|
||||
setDropdownOpen(false);
|
||||
}}>
|
||||
<h2>Reload notifications</h2>
|
||||
<p>Manually fetch new notifications instead of waiting for the sync</p>
|
||||
</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>
|
||||
<p>Move all your unread notifications to the read tab</p>
|
||||
</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>
|
||||
<p>Clear all the notifications that are being tracked in your local storage</p>
|
||||
</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>
|
||||
<p>
|
||||
{hasNotificationsOn
|
||||
@ -740,6 +943,7 @@ export default function Scene ({
|
||||
{'Unread'}
|
||||
{unreadCount > 0 && (
|
||||
<span css={css`
|
||||
transition: all 200ms ease;
|
||||
background: ${view === View.UNREAD ? '#4880ff' : '#bfc5d1'};
|
||||
color: ${WHITE};
|
||||
font-size: 9px;
|
||||
@ -762,6 +966,7 @@ export default function Scene ({
|
||||
{'Read'}
|
||||
{readCount > 0 && (
|
||||
<span css={css`
|
||||
transition: all 200ms ease;
|
||||
background: ${view === View.READ ? '#4880ff' : '#bfc5d1'};
|
||||
color: ${WHITE};
|
||||
font-size: 9px;
|
||||
@ -784,6 +989,7 @@ export default function Scene ({
|
||||
{'Archived'}
|
||||
{archivedCount > 0 && (
|
||||
<span css={css`
|
||||
transition: all 200ms ease;
|
||||
background: ${view === View.ARCHIVED ? '#4880ff' : '#bfc5d1'};
|
||||
color: ${WHITE};
|
||||
font-size: 9px;
|
||||
@ -815,7 +1021,7 @@ export default function Scene ({
|
||||
margin-right: 8px;
|
||||
span {
|
||||
font-size: 13px;
|
||||
color: #797d8c;
|
||||
color: #37352f;
|
||||
font-weight: 600;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
@ -900,52 +1106,24 @@ export default function Scene ({
|
||||
|
||||
</NotificationCell>
|
||||
</NotificationRowHeader>
|
||||
<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} css={css`
|
||||
font-weight: 500;
|
||||
`}>
|
||||
{item.name}
|
||||
</NotificationCell>
|
||||
{/* Repository */}
|
||||
<NotificationCell flex={2} css={css`
|
||||
font-weight: 500;
|
||||
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>
|
||||
{loading ? (
|
||||
<NotificationBlock>
|
||||
<LoadingNotificationRow />
|
||||
<LoadingNotificationRow />
|
||||
<LoadingNotificationRow />
|
||||
<LoadingNotificationRow />
|
||||
<LoadingNotificationRow />
|
||||
<LoadingNotificationRow />
|
||||
<LoadingNotificationRow />
|
||||
</NotificationBlock>
|
||||
) : (
|
||||
<NotificationCollection
|
||||
notifications={notifications}
|
||||
page={page}
|
||||
colorOfScore={createColorOfScore(lowestScore, highestScore)}
|
||||
onTitleClick={onStageThread}
|
||||
/>
|
||||
)}
|
||||
</NotificationsTable>
|
||||
</NotificationsSection>
|
||||
</ContentItem>
|
||||
@ -953,3 +1131,82 @@ export default function Scene ({
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@ -86,7 +86,9 @@ class NotificationsProvider extends React.Component {
|
||||
}
|
||||
// Only update if our notifications prop changes.
|
||||
// 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
|
||||
@ -99,6 +101,27 @@ class NotificationsProvider extends React.Component {
|
||||
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) => {
|
||||
// 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
|
||||
@ -395,6 +418,7 @@ class NotificationsProvider extends React.Component {
|
||||
render () {
|
||||
return this.props.children({
|
||||
...this.state,
|
||||
requestUser: this.requestUser,
|
||||
notifications: this.props.notifications,
|
||||
fetchNotifications: this.fetchNotifications,
|
||||
fetchNotificationsSync: this.requestFetchNotifications,
|
||||
|
@ -18,12 +18,13 @@
|
||||
|
||||
::selection {
|
||||
color: #fff;
|
||||
background: #24292e;
|
||||
background: #4880ff;
|
||||
}
|
||||
|
||||
html, body {
|
||||
/* height: 100%; */
|
||||
width: 100%;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
html, body, * {
|
||||
|
Loading…
Reference in New Issue
Block a user