diff --git a/src/components/Icon/index.js b/src/components/Icon/index.js
index 95311a9..57672fa 100644
--- a/src/components/Icon/index.js
+++ b/src/components/Icon/index.js
@@ -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);
diff --git a/src/components/Icon/svg/bolt.svg b/src/components/Icon/svg/bolt.svg
index 2aac7c3..ed352d5 100644
--- a/src/components/Icon/svg/bolt.svg
+++ b/src/components/Icon/svg/bolt.svg
@@ -1,4 +1,4 @@
diff --git a/src/components/Icon/svg/help.svg b/src/components/Icon/svg/help.svg
new file mode 100644
index 0000000..6320581
--- /dev/null
+++ b/src/components/Icon/svg/help.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/Icon/svg/info.svg b/src/components/Icon/svg/info.svg
new file mode 100644
index 0000000..f8aba4c
--- /dev/null
+++ b/src/components/Icon/svg/info.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/Icon/svg/people-alt-white.svg b/src/components/Icon/svg/people-alt-white.svg
new file mode 100644
index 0000000..b075526
--- /dev/null
+++ b/src/components/Icon/svg/people-alt-white.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/Icon/svg/people-alt.svg b/src/components/Icon/svg/people-alt.svg
new file mode 100644
index 0000000..074db6b
--- /dev/null
+++ b/src/components/Icon/svg/people-alt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/Icon/svg/timer.svg b/src/components/Icon/svg/timer.svg
new file mode 100644
index 0000000..4c11051
--- /dev/null
+++ b/src/components/Icon/svg/timer.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/Icon/svg/undo.svg b/src/components/Icon/svg/undo.svg
new file mode 100644
index 0000000..fbc9edf
--- /dev/null
+++ b/src/components/Icon/svg/undo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/Icon/svg/user.svg b/src/components/Icon/svg/user.svg
new file mode 100644
index 0000000..1da0081
--- /dev/null
+++ b/src/components/Icon/svg/user.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/LoadingIcon/index.js b/src/components/LoadingIcon/index.js
index 35a0308..b26c5c8 100644
--- a/src/components/LoadingIcon/index.js
+++ b/src/components/LoadingIcon/index.js
@@ -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 (
+
diff --git a/src/enhance/index.js b/src/enhance/index.js
index 92ad373..88181cf 100644
--- a/src/enhance/index.js
+++ b/src/enhance/index.js
@@ -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}) => (
+
+ {mouseEvents => tooltip ? (
+
+ ) : (
+
+ )}
+
+);
diff --git a/src/pages/Notifications/SceneAlt.js b/src/pages/Notifications/SceneAlt.js
index 9456b17..5bced77 100644
--- a/src/pages/Notifications/SceneAlt.js
+++ b/src/pages/Notifications/SceneAlt.js
@@ -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 ({
- {isSearching && }
+ {isSearching && }
You've triaged {stagedTodayCount} notifications today
+ {/*
+ We shouldn't show all the notificaitons. Pointless and creates more noise.
+
- onSetActiveFilter(Filters.PARTICIPATING)}>
{activeFilter === Filters.PARTICIPATING ? (
-
+
) : (
-
+
)}
- {/* participating */}
- your triage
-
-
+ onSetActiveFilter(Filters.COMMENT)}>
{activeFilter === Filters.COMMENT ? (
-
+
) : (
-
+
)}
- commented
-
+ participating
+
- Report bugs
- Submit feedback
- See source code
+ Report bugs
+ Submit feedback
+ See source code
@@ -603,23 +619,23 @@ export default function Scene ({
flex: 1
}}>
-
+
onFetchNotifications()) : undefined}
+ onClick={!loading ? (() => onFetchNotifications()) : undefined}
/>
-
-
+
+
{
+ onClick={!loading ? (() => {
const response = window.confirm('Are you sure you want to clear the cache?');
if (response) {
onClearCache();
}
}) : undefined}
/>
-
+
{query ? (
@@ -636,12 +652,12 @@ export default function Scene ({
Showing results for '{query}'
-
+
onClearQuery()) : undefined}
+ onClick={!loading ? (() => onClearQuery()) : undefined}
/>
-
+
) : null}
@@ -659,45 +675,51 @@ export default function Scene ({
{first}-{last} of about {allNotificationsCount}
-
+
onChangePage(page - 1)) : undefined}
+ onClick={!loading && !isFirstPage ? (() => onChangePage(page - 1)) : undefined}
/>
-
-
+
+
onChangePage(page + 1)) : undefined}
+ onClick={!loading && !isLastPage ? (() => onChangePage(page + 1)) : undefined}
/>
-
+
- onSetActiveStatus(Status.QUEUED)}
href="javascript:void(0);">
- Queued
-
-
+ onSetActiveStatus(Status.STAGED)}
href="javascript:void(0);">
- Staged
-
-
+ onSetActiveStatus(Status.CLOSED)}
href="javascript:void(0);">
- Closed
-
+ Resolved
+
@@ -738,12 +760,23 @@ export default function Scene ({
flex={.65}
onClick={() => {
window.open(n.url);
- onStageThread(n.id)
+ onStageThread(n.id, n.repository)
}}>
{n.name}
- {getRelativeTime(n.updated_at)}
+
+ {getRelativeTime(n.updated_at)}
+ {n.isAuthor && (
+
+ )}
+
@@ -751,13 +784,34 @@ export default function Scene ({
switch (badge) {
case Badges.HOT:
// lots of `reasons` within short time frame
- return
+ return (
+
+ );
case Badges.OLD:
// old
- return
+ return (
+
+ );
case Badges.COMMENTS:
// lots of `reasons`
- return
+ return (
+
+ );
default:
return null;
}
@@ -771,21 +825,40 @@ export default function Scene ({
{n.repository}
-
+
{n.score}
-
-
- onStageThread(n.id, n.repository)) : undefined}
- />
-
-
- onMarkAsRead(n.id)) : undefined}
- />
-
+
+ {activeStatus === Status.QUEUED ? (
+
+ onStageThread(n.id, n.repository)) : undefined}
+ />
+
+ ) : (
+
+ onRestoreThread(n.id)) : undefined}
+ />
+
+ )}
+ {activeStatus === Status.CLOSED ? (
+
+ {}) : undefined}
+ />
+
+ ) : (
+
+ onMarkAsRead(n.id)) : undefined}
+ />
+
+ )}
))}
diff --git a/src/pages/Notifications/index.js b/src/pages/Notifications/index.js
index 7163f19..c765480 100644
--- a/src/pages/Notifications/index.js
+++ b/src/pages/Notifications/index.js
@@ -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
@@ -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}
diff --git a/src/providers/Notifications.js b/src/providers/Notifications.js
index 7b01bad..3bdcf06 100644
--- a/src/providers/Notifications.js
+++ b/src/providers/Notifications.js
@@ -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,
});
}
}
diff --git a/src/providers/Storage.js b/src/providers/Storage.js
index aae107a..937e977 100644
--- a/src/providers/Storage.js
+++ b/src/providers/Storage.js
@@ -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 });
diff --git a/src/styles/index.css b/src/styles/index.css
index 9b68900..0740da5 100644
--- a/src/styles/index.css
+++ b/src/styles/index.css
@@ -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;
+}