Update UI, add tooltips
@ -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);
|
||||
|
@ -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 |
1
src/components/Icon/svg/help.svg
Normal 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 |
1
src/components/Icon/svg/info.svg
Normal 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 |
1
src/components/Icon/svg/people-alt-white.svg
Normal 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 |
1
src/components/Icon/svg/people-alt.svg
Normal 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 |
1
src/components/Icon/svg/timer.svg
Normal 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 |
1
src/components/Icon/svg/undo.svg
Normal 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 |
1
src/components/Icon/svg/user.svg
Normal 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 |
@ -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,
|
||||
|
17
src/components/LoadingIcon/loader-white.svg
Normal 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 |
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
))}
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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 });
|
||||
|
@ -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;
|
||||
}
|
||||
|