Update UI, add tooltips

This commit is contained in:
Nicholas Zuber 2018-11-07 19:51:55 -05:00
parent bd3f346e27
commit 50773e97cc
17 changed files with 349 additions and 107 deletions

View File

@ -16,7 +16,9 @@ import convo from './svg/convo.svg';
import doneAll from './svg/done-all.svg';
import done from './svg/done.svg';
import hot from './svg/hot.svg';
import help from './svg/help.svg';
import inbox from './svg/inbox.svg';
import info from './svg/info.svg';
import inboxWhite from './svg/inbox-white.svg';
import locked from './svg/locked.svg';
import lowPriority from './svg/low_priority.svg';
@ -24,6 +26,8 @@ import menu from './svg/menu.svg';
import next from './svg/next.svg';
import people from './svg/people.svg';
import peopleWhite from './svg/people-white.svg';
import peopleAlt from './svg/people-alt.svg';
import peopleAltWhite from './svg/people-alt-white.svg';
import prev from './svg/prev.svg';
import refresh from './svg/refresh.svg';
import search from './svg/search.svg';
@ -31,7 +35,10 @@ import settings from './svg/settings.svg';
import starAlt from './svg/star-alt.svg';
import star from './svg/star.svg';
import trash from './svg/trash.svg';
import timer from './svg/timer.svg';
import unlocked from './svg/unlocked.svg';
import undo from './svg/undo.svg';
import user from './svg/user.svg';
import x from './svg/x.svg';
import issue_closed from './svg/github/issue-closed.svg';
@ -76,7 +83,9 @@ Icon.Convo = createIcon(convo);
Icon.DoneAll = createIcon(doneAll);
Icon.Done = createIcon(done);
Icon.Hot = createIcon(hot);
Icon.Help = createIcon(help);
Icon.Inbox = createIcon(inbox);
Icon.Info = createIcon(info);
Icon.InboxWhite = createIcon(inboxWhite);
Icon.Locked = createIcon(locked);
Icon.LowPriority = createIcon(lowPriority);
@ -84,6 +93,8 @@ Icon.Menu = createIcon(menu);
Icon.Next = createIcon(next);
Icon.People = createIcon(people);
Icon.PeopleWhite = createIcon(peopleWhite);
Icon.PeopleAlt = createIcon(peopleAlt);
Icon.PeopleAltWhite = createIcon(peopleAltWhite);
Icon.Prev = createIcon(prev);
Icon.Refresh = createIcon(refresh);
Icon.Search = createIcon(search);
@ -91,7 +102,10 @@ Icon.Settings = createIcon(settings);
Icon.StarAlt = createIcon(starAlt);
Icon.Star = createIcon(star);
Icon.Trash = createIcon(trash);
Icon.Timer = createIcon(timer);
Icon.Unlocked = createIcon(unlocked);
Icon.Undo = createIcon(undo);
Icon.User = createIcon(user);
Icon.X = createIcon(x);
Icon.IssueClosed = createIcon(issue_closed);

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z" />
<path fill="#2cbf73" d="M12 2.02c-5.51 0-9.98 4.47-9.98 9.98s4.47 9.98 9.98 9.98 9.98-4.47 9.98-9.98S17.51 2.02 12 2.02zM11.48 20v-6.26H8L13 4v6.26h3.35L11.48 20z" />
<path d="M12 2.02c-5.51 0-9.98 4.47-9.98 9.98s4.47 9.98 9.98 9.98 9.98-4.47 9.98-9.98S17.51 2.02 12 2.02zM11.48 20v-6.26H8L13 4v6.26h3.35L11.48 20z" />
</svg>

Before

Width:  |  Height:  |  Size: 301 B

After

Width:  |  Height:  |  Size: 286 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg>

After

Width:  |  Height:  |  Size: 381 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>

After

Width:  |  Height:  |  Size: 234 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#fff" d="M11.99 2c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10zm3.61 6.34c1.07 0 1.93.86 1.93 1.93 0 1.07-.86 1.93-1.93 1.93-1.07 0-1.93-.86-1.93-1.93-.01-1.07.86-1.93 1.93-1.93zm-6-1.58c1.3 0 2.36 1.06 2.36 2.36 0 1.3-1.06 2.36-2.36 2.36s-2.36-1.06-2.36-2.36c0-1.31 1.05-2.36 2.36-2.36zm0 9.13v3.75c-2.4-.75-4.3-2.6-5.14-4.96 1.05-1.12 3.67-1.69 5.14-1.69.53 0 1.2.08 1.9.22-1.64.87-1.9 2.02-1.9 2.68zM11.99 20c-.27 0-.53-.01-.79-.04v-4.07c0-1.42 2.94-2.13 4.4-2.13 1.07 0 2.92.39 3.84 1.15-1.17 2.97-4.06 5.09-7.45 5.09z"/><path fill="none" d="M0 0h24v24H0z"/></svg>

After

Width:  |  Height:  |  Size: 683 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M11.99 2c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10zm3.61 6.34c1.07 0 1.93.86 1.93 1.93 0 1.07-.86 1.93-1.93 1.93-1.07 0-1.93-.86-1.93-1.93-.01-1.07.86-1.93 1.93-1.93zm-6-1.58c1.3 0 2.36 1.06 2.36 2.36 0 1.3-1.06 2.36-2.36 2.36s-2.36-1.06-2.36-2.36c0-1.31 1.05-2.36 2.36-2.36zm0 9.13v3.75c-2.4-.75-4.3-2.6-5.14-4.96 1.05-1.12 3.67-1.69 5.14-1.69.53 0 1.2.08 1.9.22-1.64.87-1.9 2.02-1.9 2.68zM11.99 20c-.27 0-.53-.01-.79-.04v-4.07c0-1.42 2.94-2.13 4.4-2.13 1.07 0 2.92.39 3.84 1.15-1.17 2.97-4.06 5.09-7.45 5.09z"/><path fill="none" d="M0 0h24v24H0z"/></svg>

After

Width:  |  Height:  |  Size: 670 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path fill="#00cd94" d="M15 1H9v2h6V1zm-4 13h2V8h-2v6zm8.03-6.61l1.42-1.42c-.43-.51-.9-.99-1.41-1.41l-1.42 1.42C16.07 4.74 14.12 4 12 4c-4.97 0-9 4.03-9 9s4.02 9 9 9 9-4.03 9-9c0-2.12-.74-4.07-1.97-5.61zM12 20c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z"/></svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"/></svg>

After

Width:  |  Height:  |  Size: 280 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@ -1,11 +1,19 @@
import React from 'react';
import loader from './loader.svg';
import loaderAlt from './loader-alt.svg';
import loaderWhite from './loader-white.svg';
export default function LoadingIcon ({ style, size, alt, white, ...props }) {
let url = loader;
if (white) {
url = loaderWhite;
} else if (alt) {
url = loaderAlt;
}
export default function LoadingIcon ({ style, size, alt, ...props }) {
return (
<div style={{
background: `url(${(alt ? loaderAlt : loader)}) center center no-repeat`,
background: `url(${(url)}) center center no-repeat`,
position: 'relative',
height: size || 100,
width: size || 100,

View File

@ -0,0 +1,17 @@
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<svg width="22" height="22" viewBox="-1 -1 22 22" xmlns="http://www.w3.org/2000/svg" stroke="#e4e4e4">
<g fill="none" fill-rule="evenodd">
<g transform="translate(1 1)" stroke-width="2.6">
<circle stroke="none" cx="9" cy="9" r="9"/>
<path stroke="#fff" d="M18,9 C18,4.03 13.97,0 9,0">
<animateTransform
attributeName="transform"
type="rotate"
from="0 9 9"
to="360 9 9"
dur=".5s"
repeatCount="indefinite"/>
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 704 B

View File

@ -10,3 +10,74 @@ export const withOnEnter = WrappedComponent => ({onEnter, ...props}) => (
}}
/>
);
class Tooltip extends React.Component {
constructor (props) {
super(props);
this.id = ('tooltip-id-' + Math.random()).replace(/\./g, '');
}
static defaultProps = {
tooltipOffsetX: 0,
tooltipOffsetY: 0
}
getTooltipElement = () => document.querySelector(`#${this.id}`);
onMouseEnter = event => {
if (this.getTooltipElement()) {
return;
}
const {tooltipOffsetX, tooltipOffsetY} = this.props;
const {x, y, height} = event.target.getBoundingClientRect();
const text = document.createTextNode(this.props.message);
const tooltipElement = document.createElement('div');
tooltipElement.setAttribute('id', this.id);
tooltipElement.setAttribute('class', 'react-tooltip');
tooltipElement.setAttribute(
'style',
`top: ${y + tooltipOffsetY}px; left: ${x + tooltipOffsetX}px;`
);
tooltipElement.appendChild(text);
document.querySelector('body').appendChild(tooltipElement);
this.timeout = setTimeout(() => {
tooltipElement.setAttribute(
'style',
`top: ${y + tooltipOffsetY}px; left: ${x + tooltipOffsetX}px; opacity: .83;`
);
}, 500);
}
onMouseLeave = () => {
clearTimeout(this.timeout);
const tooltipElement = this.getTooltipElement();
if (tooltipElement) {
tooltipElement.parentNode.removeChild(tooltipElement);
}
}
render () {
return this.props.children({
onMouseEnter: this.onMouseEnter,
onMouseLeave: this.onMouseLeave,
});
}
}
export const withTooltip = WrappedComponent => ({tooltip, tooltipOffsetX, tooltipOffsetY, ...props}) => (
<Tooltip
message={tooltip}
tooltipOffsetX={tooltipOffsetX}
tooltipOffsetY={tooltipOffsetY}>
{mouseEvents => tooltip ? (
<WrappedComponent {...props} {...mouseEvents} />
) : (
<WrappedComponent {...props} />
)}
</Tooltip>
);

View File

@ -8,7 +8,7 @@ import Logo from '../../components/Logo';
import LoadingIcon from '../../components/LoadingIcon';
import {routes} from '../../constants';
import {Filters} from '../../constants/filters';
import {withOnEnter} from '../../enhance';
import {withOnEnter, withTooltip} from '../../enhance';
import {Status} from '../../constants/status';
import {Badges} from '../../constants/reasons';
import '../../styles/gradient.css';
@ -405,6 +405,14 @@ const SmallLink = styled('a')({
}
});
const EnhancedTab = withTooltip(Tab);
const EnhancedNavTab = withTooltip(NavTab);
const EnhancedNotificationTab = withTooltip(NotificationTab);
const EnhancedSidebarLink = withTooltip(SidebarLink);
const EnhancedIconHot = withTooltip(Icon.Hot);
const EnhancedIconTimer = withTooltip(Icon.Timer);
const EnhancedIconConvo = withTooltip(Icon.Convo);
function getPRIssueIcon (type, reasons) {
const grow = 1.0;
switch (type) {
@ -443,6 +451,7 @@ export default function Scene ({
onFetchNotifications,
onRefreshNotifications,
onStageThread,
onRestoreThread,
isSearching,
isFetchingNotifications,
onClearCache,
@ -450,7 +459,7 @@ export default function Scene ({
activeFilter,
onSetActiveFilter,
}) {
const isLoading = isSearching || isFetchingNotifications;
const loading = isSearching || isFetchingNotifications;
const isFirstPage = page === 1;
const isLastPage = page === lastPage;
@ -479,12 +488,12 @@ export default function Scene ({
<SearchField>
<Icon.Search size={48} opacity={.45} />
<EnhancedSearchInput
disabled={isLoading}
disabled={loading}
type="text"
placeholder="Search for notifications"
onEnter={onSearch}
/>
{isSearching && <LoadingIcon alt={true} size={48} />}
{isSearching && <LoadingIcon white={true} size={48} />}
</SearchField>
<div style={{display: 'inline-block'}} className="button-container-alt">
<Link style={{
@ -544,6 +553,9 @@ export default function Scene ({
opacity: 0.5,
}}>You've triaged {stagedTodayCount} notifications today</span>
</div>
{/*
We shouldn't show all the notificaitons. Pointless and creates more noise.
<SidebarLink
active={activeFilter === Filters.ALL}
color="#00A0F5"
@ -555,29 +567,33 @@ export default function Scene ({
)}
all notifications
</SidebarLink>
<SidebarLink
*/}
<EnhancedSidebarLink
tooltip="All the updates for issues and pull requests that are your responsibility to deal with"
tooltipOffsetX={130}
active={activeFilter === Filters.PARTICIPATING}
color="#00d19a"
onClick={() => onSetActiveFilter(Filters.PARTICIPATING)}>
{activeFilter === Filters.PARTICIPATING ? (
<Icon.PeopleWhite shrink={.6} />
<Icon.BoltWhite shrink={.6} />
) : (
<Icon.People shrink={.6} />
<Icon.Bolt shrink={.6} />
)}
{/* participating */}
your triage
</SidebarLink>
<SidebarLink
your updates
</EnhancedSidebarLink>
<EnhancedSidebarLink
tooltip="Updates for issues and pull requests that you have commented on"
tooltipOffsetX={100}
active={activeFilter === Filters.COMMENT}
color="#f12c3f"
color="#00A0F5"
onClick={() => onSetActiveFilter(Filters.COMMENT)}>
{activeFilter === Filters.COMMENT ? (
<Icon.BookmarkAltWhite shrink={.6} />
<Icon.PeopleAltWhite shrink={.6} />
) : (
<Icon.BookmarkAlt shrink={.6} />
<Icon.PeopleAlt shrink={.6} />
)}
commented
</SidebarLink>
participating
</EnhancedSidebarLink>
<div style={{
padding: 14,
margin: 21,
@ -592,9 +608,9 @@ export default function Scene ({
padding: 14,
margin: 21,
}}>
<SmallLink>Report bugs</SmallLink>
<SmallLink>Submit feedback</SmallLink>
<SmallLink>See source code</SmallLink>
<SmallLink target="_blank" href="https://github.com/nickzuber/meteorite/issues">Report bugs</SmallLink>
<SmallLink target="_blank" href="https://github.com/nickzuber/meteorite/issues">Submit feedback</SmallLink>
<SmallLink target="_blank" href="https://github.com/nickzuber/meteorite">See source code</SmallLink>
</div>
</FixedContainer>
</Sidebar>
@ -603,23 +619,23 @@ export default function Scene ({
flex: 1
}}>
<GeneralOptionsContainer>
<Tab disabled={isLoading}>
<EnhancedTab tooltip={!loading ? "Refresh your notifications" : null} disabled={loading}>
<Icon.Refresh
opacity={0.9}
onClick={!isLoading ? (() => onFetchNotifications()) : undefined}
onClick={!loading ? (() => onFetchNotifications()) : undefined}
/>
</Tab>
<Tab disabled={isLoading}>
</EnhancedTab>
<EnhancedTab tooltip={!loading ? "Delete all of your notifications from the cache" : null} disabled={loading}>
<Icon.Trash
opacity={0.9}
onClick={!isLoading ? (() => {
onClick={!loading ? (() => {
const response = window.confirm('Are you sure you want to clear the cache?');
if (response) {
onClearCache();
}
}) : undefined}
/>
</Tab>
</EnhancedTab>
{query ? (
<React.Fragment>
<div style={{display: 'inline-block'}} className="button-container-alt">
@ -636,12 +652,12 @@ export default function Scene ({
Showing results for '{query}'
</a>
</div>
<Tab disabled={isLoading}>
<EnhancedTab disabled={loading}>
<Icon.X
opacity={0.9}
onClick={!isLoading ? (() => onClearQuery()) : undefined}
onClick={!loading ? (() => onClearQuery()) : undefined}
/>
</Tab>
</EnhancedTab>
</React.Fragment>
) : null}
<div style={{float: 'right'}}>
@ -659,45 +675,51 @@ export default function Scene ({
{first}-{last} of about {allNotificationsCount}
</a>
</div>
<Tab disabled={isLoading || isFirstPage}>
<EnhancedTab disabled={loading || isFirstPage}>
<Icon.Prev
opacity={0.9}
onClick={!isLoading && !isFirstPage ? (() => onChangePage(page - 1)) : undefined}
onClick={!loading && !isFirstPage ? (() => onChangePage(page - 1)) : undefined}
/>
</Tab>
<Tab disabled={isLoading || isLastPage}>
</EnhancedTab>
<EnhancedTab disabled={loading || isLastPage}>
<Icon.Next
opacity={0.9}
onClick={!isLoading && !isLastPage ? (() => onChangePage(page + 1)) : undefined}
onClick={!loading && !isLastPage ? (() => onChangePage(page + 1)) : undefined}
/>
</Tab>
</EnhancedTab>
</div>
</GeneralOptionsContainer>
<GeneralOptionsContainer style={{paddingTop: 4}}>
<NavTab
<EnhancedNavTab
tooltip="New updates that you haven't dealt with yet"
tooltipOffsetX={55}
number={queuedCount}
color="#00d19a"
active={activeStatus === Status.QUEUED}
onClick={() => onSetActiveStatus(Status.QUEUED)}
href="javascript:void(0);">
Queued
</NavTab>
<NavTab
Unread
</EnhancedNavTab>
<EnhancedNavTab
tooltip="Notifications that you've seen, clicked on, or otherwise have handled"
tooltipOffsetX={55}
number={stagedCount}
color="#009ef8"
active={activeStatus === Status.STAGED}
onClick={() => onSetActiveStatus(Status.STAGED)}
href="javascript:void(0);">
Staged
</NavTab>
<NavTab
Read
</EnhancedNavTab>
<EnhancedNavTab
tooltip="Stale and old notifications that are considered closed out and finished"
tooltipOffsetX={55}
number={closedCount}
color="#f12c3f"
active={activeStatus === Status.CLOSED}
onClick={() => onSetActiveStatus(Status.CLOSED)}
href="javascript:void(0);">
Closed
</NavTab>
Resolved
</EnhancedNavTab>
</GeneralOptionsContainer>
<NotificationsContainer>
<Notifications>
@ -738,12 +760,23 @@ export default function Scene ({
flex={.65}
onClick={() => {
window.open(n.url);
onStageThread(n.id)
onStageThread(n.id, n.repository)
}}>
<NotificationTitle>
<PRIssue after={n.number}>{n.name}</PRIssue>
</NotificationTitle>
<Timestamp>{getRelativeTime(n.updated_at)}</Timestamp>
<Timestamp>
{getRelativeTime(n.updated_at)}
{n.isAuthor && (
<Icon.User
shrink={0.5}
style={{
display: 'inline-block',
top: -3
}}
/>
)}
</Timestamp>
</TableItem>
<TableItem width={100}>
<InlineBlockContainer>
@ -751,13 +784,34 @@ export default function Scene ({
switch (badge) {
case Badges.HOT:
// lots of `reasons` within short time frame
return <Icon.Hot shrink={0.75} />
return (
<EnhancedIconHot
tooltip="Lots of recent activity"
tooltipOffsetX={-15}
tooltipOffsetY={-10}
shrink={0.75}
/>
);
case Badges.OLD:
// old
return <Icon.Alarm shrink={0.75} />
return (
<EnhancedIconTimer
tooltip="Old pull request that needs your review"
tooltipOffsetX={-15}
tooltipOffsetY={-10}
shrink={0.75}
/>
);
case Badges.COMMENTS:
// lots of `reasons`
return <Icon.Convo shrink={0.75} />
return (
<EnhancedIconConvo
tooltip="Very talkative thread"
tooltipOffsetX={-15}
tooltipOffsetY={-10}
shrink={0.75}
/>
);
default:
return null;
}
@ -771,21 +825,40 @@ export default function Scene ({
{n.repository}</Repository>
</TableItem>
<TableItem width={150} style={{textAlign: 'right'}}>
<NotificationTab>
<EnhancedNotificationTab>
{n.score}
</NotificationTab>
<NotificationTab>
<Icon.Check
opacity={0.9}
onClick={!isLoading ? (() => onStageThread(n.id, n.repository)) : undefined}
/>
</NotificationTab>
<NotificationTab>
<Icon.X
opacity={0.9}
onClick={!isLoading ? (() => onMarkAsRead(n.id)) : undefined}
/>
</NotificationTab>
</EnhancedNotificationTab>
{activeStatus === Status.QUEUED ? (
<EnhancedNotificationTab tooltip={!loading ? "Mark as read" : null}>
<Icon.Check
opacity={0.9}
onClick={!loading ? (() => onStageThread(n.id, n.repository)) : undefined}
/>
</EnhancedNotificationTab>
) : (
<EnhancedNotificationTab tooltip={!loading ? "Revert back to unread" : null}>
<Icon.Undo
opacity={0.9}
onClick={!loading ? (() => onRestoreThread(n.id)) : undefined}
/>
</EnhancedNotificationTab>
)}
{activeStatus === Status.CLOSED ? (
<EnhancedNotificationTab>
<Icon.Help
shrink={0.8}
opacity={0.9}
onClick={!loading ? (() => {}) : undefined}
/>
</EnhancedNotificationTab>
) : (
<EnhancedNotificationTab tooltip={!loading ? "Mark as resolved" : null}>
<Icon.X
opacity={0.9}
onClick={!loading ? (() => onMarkAsRead(n.id)) : undefined}
/>
</EnhancedNotificationTab>
)}
</TableItem>
</NotificationRow>
))}

View File

@ -69,10 +69,12 @@ function scoreOf (notification) {
function badgesOf (notification) {
const badges = [];
const len = notification.reasons.length;
const timeSinceLastUpdate = moment().diff(moment(notification.reasons[len - 1].time), 'minutes');
// If there are more than 4 reasons, and the last 4 reasons have happened within
// an hour of each other.
// an hour of each other. The last update should be within the past 30 minutes.
// The specific time frame and reasons count is subject to change.
if (len >= 4) {
if (len >= 4 && timeSinceLastUpdate < 30) {
const oldestReference = moment(notification.reasons[len - 4].time);
const newestReference = moment(notification.reasons[len - 1].time);
if (newestReference.diff(oldestReference, 'hours') <= 1) {
@ -87,9 +89,8 @@ function badgesOf (notification) {
}
// If you've been tagged in for review and the most recent update happened over
// 4 hours ago, that specific time is subject to change.
// @TODO i changed this to 1 for testing, that's def too early.
if (notification.reasons.some(r => r.reason === Reasons.REVIEW_REQUESTED) &&
moment().diff(moment(notification.reasons[notification.reasons.length - 1].time).hours, 'hours') > 1) {
timeSinceLastUpdate > 60 * 4) {
badges.push(Badges.OLD);
}
return badges;
@ -177,6 +178,11 @@ class NotificationsPage extends React.Component {
this.props.notificationsApi.stageThread(thread_id);
}
restoreThread = thread_id => {
console.warn('restoring thread');
this.props.notificationsApi.restoreThread(thread_id);
}
render () {
if (!this.props.authApi.token) {
return <Redirect noThrow to={routes.LOGIN} />
@ -286,6 +292,7 @@ class NotificationsPage extends React.Component {
onMarkAsRead={markAsRead}
onClearCache={clearCache}
onStageThread={this.enhancedOnStageThread}
onRestoreThread={this.restoreThread}
onRefreshNotifications={this.props.storageApi.refreshNotifications}
isSearching={this.state.isSearching}
isFetchingNotifications={isFetchingNotifications}

View File

@ -171,35 +171,6 @@ class NotificationsProvider extends React.Component {
});
}
updateNotification = (n, prevReason = null) => {
let reasons = [];
const newReason = {
reason: n.reason,
time: n.updated_at
}
if (prevReason) {
reasons = prevReason.concat(newReason);
console.warn('MULTIPLE REASONS', reasons)
} else {
reasons = [newReason];
}
const value = {
id: n.id,
updated_at: n.updated_at,
status: Status.QUEUED,
reasons: reasons,
type: n.subject.type,
name: n.subject.title,
url: cleanResponseUrl(n.subject.url),
repository: n.repository.full_name,
number: n.subject.url.split('/').pop(),
repositoryUrl: cleanResponseUrl(n.repository.url)
};
this.props.setItemInStorage(n.id, value);
}
requestMarkAsRead = thread_id => {
const headers = {
'Authorization': `token ${this.props.token}`,
@ -270,6 +241,24 @@ class NotificationsProvider extends React.Component {
});
}
requestRestoreThread = thread_id => {
return new Promise((resolve, reject) => {
console.warn('restoring thread', thread_id);
const cached_n = this.props.getItemFromStorage(thread_id);
if (cached_n) {
const newValue = {
...cached_n,
status: Status.QUEUED
};
this.props.setItemInStorage(thread_id, newValue);
this.props.refreshNotifications();
return resolve();
} else {
throw new Error(`Attempted to restore thread ${thread_id} that wasn't found in the cache.`);
}
});
}
stageThread = thread_id => {
this.setState({ loading: true });
return this.requestStageThread(thread_id)
@ -277,6 +266,44 @@ class NotificationsProvider extends React.Component {
.finally(() => this.setState({ loading: false }));
}
restoreThread = thread_id => {
this.setState({ loading: true });
return this.requestRestoreThread(thread_id)
.catch(error => this.setState({ error }))
.finally(() => this.setState({ loading: false }));
}
updateNotification = (n, prevReason = null) => {
let reasons = [];
const newReason = {
reason: n.reason,
time: n.updated_at
}
if (prevReason) {
reasons = prevReason.concat(newReason);
console.warn('MULTIPLE REASONS', reasons)
} else {
reasons = [newReason];
}
// Notification model
const value = {
id: n.id,
isAuthor: reasons.some(r => r.reason === 'author'),
updated_at: n.updated_at,
status: Status.QUEUED,
reasons: reasons,
type: n.subject.type,
name: n.subject.title,
url: cleanResponseUrl(n.subject.url),
repository: n.repository.full_name,
number: n.subject.url.split('/').pop(),
repositoryUrl: cleanResponseUrl(n.repository.url)
};
this.props.setItemInStorage(n.id, value);
}
render () {
return this.props.children({
...this.state,
@ -285,7 +312,8 @@ class NotificationsProvider extends React.Component {
fetchNotificationsSync: this.requestFetchNotifications,
markAsRead: this.markAsRead,
clearCache: this.clearCache,
stageThread: this.stageThread
stageThread: this.stageThread,
restoreThread: this.restoreThread,
});
}
}

View File

@ -73,16 +73,17 @@ class StorageProvider extends React.Component {
return acc;
}, []);
// @TODO fix this
// Document is out of focus, the we had notifications before this update,
// and there was a change in notifications in the most recent update.
if (!document.hasFocus() &&
this.state.notifications.length > 0 &&
notifications.length !== this.state.notifications.length
) {
this.setTitle('(1) ' + this.originalTitle);
} else {
this.setTitle(this.originalTitle);
}
// if (!document.hasFocus() &&
// this.state.notifications.length > 0 &&
// notifications.length !== this.state.notifications.length
// ) {
// this.setTitle('(1) ' + this.originalTitle);
// } else {
// this.setTitle(this.originalTitle);
// }
this.setState({ notifications });
// this.setState({ notifications: mockNotifications });

View File

@ -77,3 +77,19 @@ p {
-webkit-font-feature-settings: "calt" 1, "kern" 1, "liga" 1;
font-feature-settings: "calt" 1, "kern" 1, "liga" 1;
}
.react-tooltip {
z-index: 999999;
pointer-events: none;
position: absolute;
background: #242a31;
color: #fff;
padding: 4px 8px;
font-weight: 600;
font-size: 11px;
border-radius: 4px;
white-space: nowrap;
transform: translateX(-35%) translateY(-15px);
opacity: 0;
transition: all 100ms ease-in;
}