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 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
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 ({
|
|||||||
|
|
||||||
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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, * {
|
||||||
|
Loading…
Reference in New Issue
Block a user