diff --git a/src/components/Logo/icon-black.png b/src/components/Logo/icon-black.png new file mode 100644 index 0000000..f0c89fe Binary files /dev/null and b/src/components/Logo/icon-black.png differ diff --git a/src/components/Logo/index.js b/src/components/Logo/index.js index 9f7de46..21bf3f7 100644 --- a/src/components/Logo/index.js +++ b/src/components/Logo/index.js @@ -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 ( diff --git a/src/pages/Notifications/Scene.js b/src/pages/Notifications/Scene.js index acfaa22..e24d7e1 100644 --- a/src/pages/Notifications/Scene.js +++ b/src/pages/Notifications/Scene.js @@ -677,632 +677,616 @@ function getPRIssueIcon (type, _reasons) { } export default function Scene ({ - currentTime, - stagedStatistics, - isFirstTimeUser, - notificationsPermission, - queuedCount, - stagedCount, - closedCount, - first, - last, - lastPage, - page, - notifications, - query, - activeStatus, - allNotificationsCount, - stagedTodayCount, - onChangePage, - onSetActiveStatus, - onClearQuery, - onLogout, - onSearch, - onMarkAsRead, - onMarkAllAsStaged, - onFetchNotifications, - onStageThread, - onRestoreThread, - isSearching, - isFetchingNotifications, - onClearCache, - fetchingNotificationsError, - activeFilter, - onSetActiveFilter, - setNotificationsPermission, - highestScore, - lowestScore, - hasUnread, + // currentTime, + // stagedStatistics, + // isFirstTimeUser, + // notificationsPermission, + // queuedCount, + // stagedCount, + // closedCount, + // first, + // last, + // lastPage, + // page, + // notifications, + // query, + // activeStatus, + // allNotificationsCount, + // stagedTodayCount, + // onChangePage, + // onSetActiveStatus, + // onClearQuery, + // onLogout, + // onSearch, + // onMarkAsRead, + // onMarkAllAsStaged, + // onFetchNotifications, + // onStageThread, + // onRestoreThread, + // isSearching, + // isFetchingNotifications, + // onClearCache, + // fetchingNotificationsError, + // activeFilter, + // onSetActiveFilter, + // setNotificationsPermission, + // highestScore, + // lowestScore, + // hasUnread, ...props }) { - const loading = isSearching || isFetchingNotifications; - const isFirstPage = page === 1; - const isLastPage = page === lastPage; + const loading = props.isSearching || props.isFetchingNotifications; + const isFirstPage = props.page === 1; + const isLastPage = props.page === props.lastPage; - const NotificationsIcon = notificationsPermission === 'granted' - ? Icon.NotificationsOn - : Icon.NotificationsOff; + // const NotificationsIcon = notificationsPermission === 'granted' + // ? Icon.NotificationsOn + // : Icon.NotificationsOff; - if (isFirstTimeUser && notifications.length > 10) { - // probably prompt to mark all as read to start out since they prob don't use notifs - } + // if (isFirstTimeUser && notifications.length > 10) { + // // probably prompt to mark all as read to start out since they prob don't use notifs + // } - stagedStatistics = stagedStatistics.map(n => parseInt(n, 10)); + // stagedStatistics = stagedStatistics.map(n => parseInt(n, 10)); - const highestStagedCount = stagedStatistics.reduce((n, m) => Math.max(n, m), 0); - let lastWeekStats = stagedStatistics.slice(0, 7); - let thisWeekStats = stagedStatistics.slice(7); + // const highestStagedCount = stagedStatistics.reduce((n, m) => Math.max(n, m), 0); + // let lastWeekStats = stagedStatistics.slice(0, 7); + // let thisWeekStats = stagedStatistics.slice(7); - // Trim off the weekends. - lastWeekStats = lastWeekStats.slice(1, -1); - thisWeekStats = thisWeekStats.slice(1, -1); + // // Trim off the weekends. + // lastWeekStats = lastWeekStats.slice(1, -1); + // thisWeekStats = thisWeekStats.slice(1, -1); return ( ); - return ( -
- -
- { - onSetActiveStatus(Status.QUEUED); - onSetActiveFilter(Filters.PARTICIPATING); - }} - /> - beta - - - event.target.select()} - type="text" - placeholder="Search for notifications" - onEnter={onSearch} - /> - {isSearching && } - -
- home -
-
- sign out -
-
-
-
-
- - -
-

- - {currentTime.format('h:mma')} -

- {currentTime.format('dddd, MMMM Do')} - You've triaged {stagedTodayCount} notifications today -
- onSetActiveFilter(Filters.PARTICIPATING)}> - {activeFilter === Filters.PARTICIPATING ? ( - - ) : ( - - )} - all your updates - - onSetActiveFilter(Filters.REVIEW_REQUESTED)}> - {activeFilter === Filters.REVIEW_REQUESTED ? ( - - ) : ( - - )} - review requested - - onSetActiveFilter(Filters.ASSIGNED)}> - {activeFilter === Filters.ASSIGNED ? ( - - ) : ( - - )} - assigned - - onSetActiveFilter(Filters.COMMENT)}> - {activeFilter === Filters.COMMENT ? ( - - ) : ( - - )} - commented - - -
- - {/* Last week's statistics */} - - {lastWeekStats.map((dayStats, i) => ( - - ))} - - {/* This week's ongoing statistics */} - - {thisWeekStats.map((dayStats, i) => ( - = i + 1} - /> - ))} - - {/* Wrapper for tooltips */} - {/* - {thisWeekStats.map((dayStats, i) => ( - - ))} - */} - -
- -
- Report bugs - Submit feedback - See source code -
-
-
-
-
- - - onFetchNotifications()) : undefined} - /> - - - { - const response = window.confirm('Are you sure you want to mark all your notifications as read?'); - if (response) { - onMarkAllAsStaged(); - } - }) : undefined} - /> - - - { - const response = window.confirm('Are you sure you want to clear the cache?'); - if (response) { - onClearCache(); - } - }) : undefined} - /> - - - { - switch(notificationsPermission) { - case 'granted': - return setNotificationsPermission('denied'); - case 'denied': - case 'default': - default: - Notification.requestPermission().then(result => { - return setNotificationsPermission(result); - }); - } - }) : undefined} - /> - - {query ? ( - - - - onClearQuery()) : undefined} - /> - - - ) : null} -
- - - onChangePage(page - 1)) : undefined} - /> - - - onChangePage(page + 1)) : undefined} - /> - -
-
- - onSetActiveStatus(Status.QUEUED)} - href="javascript:void(0);"> - Unread - - onSetActiveStatus(Status.STAGED)} - href="javascript:void(0);"> - Read - - onSetActiveStatus(Status.CLOSED)} - href="javascript:void(0);"> - Archived - - - - - {isFetchingNotifications ? ( - - - - ) : fetchingNotificationsError ? ( - - - An error occurred when fetching notifications.
- onFetchNotifications()} href="#">Try again? -
-
- ) : notifications.length <= 0 ? ( - -

- No - {activeStatus === Status.QUEUED ? ( - ' unread ' - ) : activeStatus === Status.STAGED ? ( - ' read ' - ) : ( - ' archived ' - )} - notifications -

-

- 🎉 You're all set here for the moment

-
- ) : ( - - - - {notifications.map(n => ( - - -
- {getPRIssueIcon(n.type, n.reasons)} -
-
- { - window.open(n.url); - onStageThread(n.id, n.repository) - }}> - - {n.name} - - - {getRelativeTime(n.updated_at)} - {n.isAuthor && ( - - )} - - - - {getMessageFromReasons(n.reasons, n.type)} - - - - - {activeStatus === Status.QUEUED && n.badges.map(badge => { - switch (badge) { - case Badges.HOT: - // lots of `reasons` within short time frame - return ( - - ); - case Badges.OLD: - // old - return ( - - ); - case Badges.COMMENTS: - // lots of `reasons` - return ( - - ); - default: - return null; - } - })} - - - - window.open(n.repositoryUrl)} - style={{cursor: 'pointer', userSelect: 'none'}}> - {n.repository} - - - - +{n.score} - - {activeStatus === Status.QUEUED ? ( - - onStageThread(n.id, n.repository)) : undefined} - /> - - ) : ( - - onRestoreThread(n.id)) : undefined} - /> - - )} - {activeStatus === Status.CLOSED ? ( - -   - - ) : ( - - onMarkAsRead(n.id, n.repository)) : undefined} - /> - - )} - -
- ))} - -
- {!loading && Page {page} out of {lastPage}} -
- )} -
-
-
-
-
- ); + // return ( + //
+ // + //
+ // { + // onSetActiveStatus(Status.QUEUED); + // onSetActiveFilter(Filters.PARTICIPATING); + // }} + // /> + // beta + // + // + // event.target.select()} + // type="text" + // placeholder="Search for notifications" + // onEnter={onSearch} + // /> + // {isSearching && } + // + //
+ // home + //
+ //
+ // sign out + //
+ //
+ //
+ //
+ //
+ // + // + //
+ //

+ // + // {currentTime.format('h:mma')} + //

+ // {currentTime.format('dddd, MMMM Do')} + // You've triaged {stagedTodayCount} notifications today + //
+ // onSetActiveFilter(Filters.PARTICIPATING)}> + // {activeFilter === Filters.PARTICIPATING ? ( + // + // ) : ( + // + // )} + // all your updates + // + // onSetActiveFilter(Filters.REVIEW_REQUESTED)}> + // {activeFilter === Filters.REVIEW_REQUESTED ? ( + // + // ) : ( + // + // )} + // review requested + // + // onSetActiveFilter(Filters.ASSIGNED)}> + // {activeFilter === Filters.ASSIGNED ? ( + // + // ) : ( + // + // )} + // assigned + // + // onSetActiveFilter(Filters.COMMENT)}> + // {activeFilter === Filters.COMMENT ? ( + // + // ) : ( + // + // )} + // commented + // + // + //
+ // + // {/* Last week's statistics */} + // + // {lastWeekStats.map((dayStats, i) => ( + // + // ))} + // + // {/* This week's ongoing statistics */} + // + // {thisWeekStats.map((dayStats, i) => ( + // = i + 1} + // /> + // ))} + // + // {/* Wrapper for tooltips */} + // {/* + // {thisWeekStats.map((dayStats, i) => ( + // + // ))} + // */} + // + //
+ // + //
+ // Report bugs + // Submit feedback + // See source code + //
+ //
+ //
+ //
+ //
+ // + // + // onFetchNotifications()) : undefined} + // /> + // + // + // { + // const response = window.confirm('Are you sure you want to mark all your notifications as read?'); + // if (response) { + // onMarkAllAsStaged(); + // } + // }) : undefined} + // /> + // + // + // { + // const response = window.confirm('Are you sure you want to clear the cache?'); + // if (response) { + // onClearCache(); + // } + // }) : undefined} + // /> + // + // + // { + // switch(notificationsPermission) { + // case 'granted': + // return setNotificationsPermission('denied'); + // case 'denied': + // case 'default': + // default: + // Notification.requestPermission().then(result => { + // return setNotificationsPermission(result); + // }); + // } + // }) : undefined} + // /> + // + // {query ? ( + // + // + // + // onClearQuery()) : undefined} + // /> + // + // + // ) : null} + //
+ // + // + // onChangePage(page - 1)) : undefined} + // /> + // + // + // onChangePage(page + 1)) : undefined} + // /> + // + //
+ //
+ // + // onSetActiveStatus(Status.QUEUED)} + // href="javascript:void(0);"> + // Unread + // + // onSetActiveStatus(Status.STAGED)} + // href="javascript:void(0);"> + // Read + // + // onSetActiveStatus(Status.CLOSED)} + // href="javascript:void(0);"> + // Archived + // + // + // + // + // {isFetchingNotifications ? ( + // + // + // + // ) : fetchingNotificationsError ? ( + // + // + // An error occurred when fetching notifications.
+ // onFetchNotifications()} href="#">Try again? + //
+ //
+ // ) : notifications.length <= 0 ? ( + // + //

+ // No + // {activeStatus === Status.QUEUED ? ( + // ' unread ' + // ) : activeStatus === Status.STAGED ? ( + // ' read ' + // ) : ( + // ' archived ' + // )} + // notifications + //

+ //

+ // 🎉 You're all set here for the moment

+ //
+ // ) : ( + // + // + // + // {notifications.map(n => ( + // + // + //
+ // {getPRIssueIcon(n.type, n.reasons)} + //
+ //
+ // { + // window.open(n.url); + // onStageThread(n.id, n.repository) + // }}> + // + // {n.name} + // + // + // {getRelativeTime(n.updated_at)} + // {n.isAuthor && ( + // + // )} + // + // + // + // {getMessageFromReasons(n.reasons, n.type)} + // + // + // + // + // {activeStatus === Status.QUEUED && n.badges.map(badge => { + // switch (badge) { + // case Badges.HOT: + // // lots of `reasons` within short time frame + // return ( + // + // ); + // case Badges.OLD: + // // old + // return ( + // + // ); + // case Badges.COMMENTS: + // // lots of `reasons` + // return ( + // + // ); + // default: + // return null; + // } + // })} + // + // + // + // window.open(n.repositoryUrl)} + // style={{cursor: 'pointer', userSelect: 'none'}}> + // {n.repository} + // + // + // + // +{n.score} + // + // {activeStatus === Status.QUEUED ? ( + // + // onStageThread(n.id, n.repository)) : undefined} + // /> + // + // ) : ( + // + // onRestoreThread(n.id)) : undefined} + // /> + // + // )} + // {activeStatus === Status.CLOSED ? ( + // + //   + // + // ) : ( + // + // onMarkAsRead(n.id, n.repository)) : undefined} + // /> + // + // )} + // + //
+ // ))} + // + //
+ // {!loading && Page {page} out of {lastPage}} + //
+ // )} + //
+ //
+ //
+ //
+ //
+ // ); } diff --git a/src/pages/Notifications/index.js b/src/pages/Notifications/index.js index 460de44..28b9062 100644 --- a/src/pages/Notifications/index.js +++ b/src/pages/Notifications/index.js @@ -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} /> ); } diff --git a/src/pages/Notifications/redesign/Scene.js b/src/pages/Notifications/redesign/Scene.js index 8b9c101..ca6a6b4 100644 --- a/src/pages/Notifications/redesign/Scene.js +++ b/src/pages/Notifications/redesign/Scene.js @@ -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 ( - + } + { + window.scrollTo(0, 0); + }} + size={36} + /> + {user && ( + + + {user.name} + + + )} - + -
+
{ + event.stopPropagation(); + onFetchNotifications(); + setDropdownOpen(false); + }}>

Reload notifications

Manually fetch new notifications instead of waiting for the sync

-
+
{ + event.stopPropagation(); + const response = window.confirm('Are you sure you want to mark all your notifications as read?'); + void (response && onMarkAllAsStaged()); + setDropdownOpen(false); + }}>

Mark all as read

Move all your unread notifications to the read tab

-
+
{ + event.stopPropagation(); + const response = window.confirm('Are you sure you want to clear the cache?'); + void (response && onClearCache()); + setDropdownOpen(false); + }}>

Empty cache

Clear all the notifications that are being tracked in your local storage

-
+
{ + event.stopPropagation(); + switch(notificationsPermission) { + case 'granted': + return setNotificationsPermission('denied'); + case 'denied': + case 'default': + default: + Notification.requestPermission().then(result => { + return setNotificationsPermission(result); + }); + } + setDropdownOpen(false); + }}>

Turn {hasNotificationsOn ? 'off' : 'on'} notifications

{hasNotificationsOn @@ -740,6 +943,7 @@ export default function Scene ({ {'Unread'} {unreadCount > 0 && ( 0 && ( 0 && ( - - {notifications.map(item => ( - - {/* Type */} - - {getPRIssueIcon(item.type, item.reasons)} - - {/* Title */} - - {item.name} - - {/* Repository */} - - {'@' + item.repository} - - {/* Score */} - - {'+' + item.score} - - - - - - - - - - - ))} - + {loading ? ( + + + + + + + + + + ) : ( + + )} @@ -953,3 +1131,82 @@ export default function Scene ({ ); } + +function NotificationCollection ({ + notifications, + colorOfScore, + onTitleClick, + page +}) { + const props = useSpring({ + from: {opacity: 0}, + to: {opacity: 1}, + config: { + duration: 200, + } + }); + + return ( + + {notifications.map(item => ( + + {/* Type */} + + {getPRIssueIcon(item.type, item.reasons)} + + {/* Title */} + { + window.open(item.url); + onTitleClick(item.id, item.repository); + }} + css={css` + font-weight: 500; + `}> + + {item.name} + + + {getMessageFromReasons(item.reasons, item.type)} + {` ${getRelativeTime(item.updated_at).toLowerCase()}`} + + + {/* Repository */} + window.open(item.repositoryUrl)} + css={css` + font-weight: 500; + color: #8994A6; + `}> + {'@' + item.repository} + + {/* Score */} + + {'+' + item.score} + + + + + + + + + + + ))} + + ); +} diff --git a/src/providers/Notifications.js b/src/providers/Notifications.js index eea5bcb..3ea4e93 100644 --- a/src/providers/Notifications.js +++ b/src/providers/Notifications.js @@ -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, diff --git a/src/styles/index.css b/src/styles/index.css index 6c26c35..e9a8b05 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -18,12 +18,13 @@ ::selection { color: #fff; - background: #24292e; + background: #4880ff; } html, body { /* height: 100%; */ width: 100%; + scroll-behavior: smooth; } html, body, * {