-
notifications
-
sign out
+
+ notifications
+ sign out
) : (
-
-
sign in
+
+ sign in
)}
Control your GitHub notifications
Prioritize the tasks that keep you and your team most productive
-
+
let's get started
+ {
+ const section = document.querySelector('#learnMore');
+ const y = section.getBoundingClientRect().top + window.scrollY;
+ window.scroll({
+ top: y,
+ behavior: 'smooth'
+ });
+ }}
+ style={{
+ marginLeft: 20,
+ color: '#fff',
+ background: 'none'
+ }}>
+ learn more
+
+
View and contribute on GitHub
@@ -508,31 +596,34 @@ export default function Scene ({loggedIn, onLogout, ...props}) {
+
-
-
- -
- {createImagePlaceholder('badges')}
-
- -
-
Surface the things that matter the most.
-
-
- The most important issues and pull requests that require your presence are called out and brought to your attention.
-
-
-
- We listen for updates with your notifications and let you know why and when things change.
-
-
-
- Super charge your day by focusing on getting things done, rather than sifting through notifications or emails.
-
-
-
+
+
+
+ -
+ {createImagePlaceholder('badges')}
+
+ -
+
Surface the things that matter the most.
+
+
+ The most important issues and pull requests that require your presence are called out and brought to your attention.
+
+
+
+ We listen for updates with your notifications and let you know why and when things change.
+
+
+
+ Super charge your day by focusing on getting things done, rather than sifting through notifications or emails.
+
+
+
+
-
+
-
- -
-
Your time matters, so
we keep things simple.
-
-
- All of the information we use to make your notifications more useful is kept offline and kept on your own computer.
-
-
-
- Simply sign in and start working — no complicated or intrusive set up needed.
-
-
-
- No distractions — we only show you updates on things that matter to you.
-
-
- -
- {createImagePlaceholder('reason')}
-
-
+
+
+ -
+
Your time matters, so
we keep things simple.
+
+
+ All of the information we use to make your notifications more useful is kept offline and kept on your own computer.
+
+
+
+ Simply sign in and start working — no complicated or intrusive set up needed.
+
+
+
+ No distractions — we only show you updates on things that matter to you.
+
+
+ -
+ {createImagePlaceholder('reason')}
+
+
+
+
+
+
+ Meteorite is the assistant for your
GitHub notifications.
+
+
+ -
+
+
+
All of the information we use to make your notifications more useful is kept offline and kept on your own computer.
+
+
+
+ Simply sign in and start working — no complicated or intrusive set up needed.
+
+
+
+ No distractions — we only show you updates on things that matter to you.
+
+
+ -
+
+
+
All of the information we use to make your notifications more useful is kept offline and kept on your own computer.
+
+
+
+ Simply sign in and start working — no complicated or intrusive set up needed.
+
+
+
+ No distractions — we only show you updates on things that matter to you.
+
+
+
);
diff --git a/src/pages/Notifications/Scene.js b/src/pages/Notifications/Scene.js
index 27811ed..677c536 100644
--- a/src/pages/Notifications/Scene.js
+++ b/src/pages/Notifications/Scene.js
@@ -16,18 +16,29 @@ import '../../styles/gradient.css';
/* eslint-disable jsx-a11y/anchor-is-valid */
/* eslint-disable no-script-url */
-function getMessageFromReasons (reasons) {
+function stringOfType (type) {
+ switch (type) {
+ case 'PullRequest':
+ return 'pull request';
+ case 'Issue':
+ return 'issue';
+ default:
+ return 'task';
+ }
+}
+
+function getMessageFromReasons (reasons, type) {
switch (reasons[reasons.length - 1].reason) {
case Reasons.ASSIGN:
- return 'You were just assigned a task';
+ return 'You were assigned this ' + stringOfType(type);
case Reasons.AUTHOR:
- return 'There was activity on a thread you created';
+ return 'There was activity on this thread you created';
case Reasons.COMMENT:
- return 'Somebody just left a comment';
+ return 'Somebody left a comment';
case Reasons.MENTION:
- return 'You were just @mentioned';
+ return 'You were @mentioned';
case Reasons.REVIEW_REQUESTED:
- return 'Your review was just requested';
+ return 'Your review was requested for this ' + stringOfType(type);
case Reasons.SUBSCRIBED:
return 'There was an update and you\'re subscribed';
case Reasons.OTHER:
@@ -86,11 +97,9 @@ const InlineBlockContainer = styled('div')({
const NotificationsContainer = styled('div')({
position: 'relative',
- background: '#fff',
margin: '0 auto',
padding: 0,
width: '100%',
- height: '100%',
display: 'flex',
flexDirection: 'row',
overflowX: 'hidden',
@@ -117,7 +126,6 @@ const GeneralOptionsContainer = styled(NavigationContainer)({
minHeight: 60,
width: '95%',
margin: 0,
- background: '#fff',
padding: '8px 16px',
paddingTop: 18,
flex: '0 0 50px',
@@ -350,7 +358,6 @@ const NotificationRow = styled('tr')({
width: '100%',
borderRadius: 4,
margin: '0 auto',
- background: '#fff',
padding: '8px 16px',
transition: 'all 0.1s ease-in-out',
boxSizing: 'border-box',
@@ -468,6 +475,9 @@ function getPRIssueIcon (type, reasons) {
}
export default function Scene ({
+ currentTime,
+ isFirstTimeUser,
+ notificationsPermission,
queuedCount,
stagedCount,
closedCount,
@@ -496,12 +506,23 @@ export default function Scene ({
fetchingNotificationsError,
activeFilter,
onSetActiveFilter,
+ setNotificationsPermission
}) {
const loading = isSearching || isFetchingNotifications;
const isFirstPage = page === 1;
const isLastPage = page === lastPage;
+ const NotificationsIcon = notificationsPermission === 'granted'
+ ? Icon.NotificationsOn
+ : Icon.NotificationsOff;
+
console.log('notifications', notifications)
+ console.log('isFirstTimeUser', isFirstTimeUser)
+ console.log('notificationsPermission', notificationsPermission)
+
+ if (isFirstTimeUser && notifications.length > 5) {
+ // alert('hello, clear ur shit');
+ }
return (
@@ -578,14 +599,14 @@ export default function Scene ({
marginRight: '5px',
top: '-3px',
}} />
- {moment().format('h:mma')}
+ {currentTime.format('h:mma')}
{moment().format('dddd, MMMM Do')}
+ }}>{currentTime.format('dddd, MMMM Do')}
+
+ {
+ switch(notificationsPermission) {
+ case 'granted':
+ return setNotificationsPermission('denied');
+ case 'denied':
+ case 'default':
+ default:
+ Notification.requestPermission().then(result => {
+ return setNotificationsPermission(result);
+ });
+ }
+ }) : undefined}
+ />
+
{query ? (
@@ -856,7 +894,7 @@ export default function Scene ({
}}
shrink={.5}
/>
- {getMessageFromReasons(n.reasons)}
+ {getMessageFromReasons(n.reasons, n.type)}
diff --git a/src/pages/Notifications/index.js b/src/pages/Notifications/index.js
index 3e60932..cf68d01 100644
--- a/src/pages/Notifications/index.js
+++ b/src/pages/Notifications/index.js
@@ -13,7 +13,7 @@ import { Status } from '../../constants/status';
import { Reasons, Badges } from '../../constants/reasons';
import Scene from './Scene';
-const PER_PAGE = 15;
+const PER_PAGE = 10;
// @TODO Move these functions.
@@ -64,7 +64,6 @@ function scoreOf (notification) {
return score;
};
-// @TODO implement this
function badgesOf (notification) {
const badges = [];
const len = notification.reasons.length;
@@ -96,13 +95,15 @@ function badgesOf (notification) {
};
const scoreOfReason = {
- [Reasons.ASSIGN]: 18,
- [Reasons.AUTHOR]: 10,
- [Reasons.MENTION]: 12,
- [Reasons.OTHER]: 2,
- [Reasons.REVIEW_REQUESTED]: 30,
+ [Reasons.ASSIGN]: 21,
+ [Reasons.AUTHOR]: 11,
+ [Reasons.MENTION]: 17,
+ [Reasons.TEAM_MENTION]: 11,
+ [Reasons.OTHER]: 4,
+ [Reasons.REVIEW_REQUESTED]: 29,
[Reasons.SUBSCRIBED]: 3,
[Reasons.COMMENT]: 6,
+ [Reasons.STATE_CHANGE]: 5,
};
const decorateWithScore = notification => ({
@@ -113,6 +114,9 @@ const decorateWithScore = notification => ({
class NotificationsPage extends React.Component {
state = {
+ currentTime: moment(),
+ prevNotifications: [],
+ isFirstTimeUser: false,
isSearching: false,
query: null,
activeFilter: Filters.PARTICIPATING,
@@ -121,10 +125,17 @@ class NotificationsPage extends React.Component {
}
componentDidMount () {
- this.props.notificationsApi.fetchNotifications();
+ const isFirstTimeUser = !this.props.storageApi.getUserItem('hasOnboarded');
+ if (isFirstTimeUser) {
+ this.setState({isFirstTimeUser: true});
+ // this.props.storageApi.setUserItem('hasOnboarded', true);
+ }
+
+ this.props.notificationsApi.fetchNotifications();
this.syncer = setInterval(() => {
this.props.notificationsApi.fetchNotificationsSync();
+ this.setState({currentTime: moment()});
}, 8 * 1000);
}
@@ -132,6 +143,10 @@ class NotificationsPage extends React.Component {
clearInterval(this.syncer);
}
+ diffForWebNotification (nextProps, nextState) {
+ this.sendWebNotification();
+ }
+
onChangePage = page => {
this.setState({ currentPage: page });
}
@@ -181,6 +196,16 @@ class NotificationsPage extends React.Component {
this.props.notificationsApi.restoreThread(thread_id);
}
+ setNotificationsPermission = (...args) => {
+ this.props.notificationsApi.setNotificationsPermission(...args);
+ }
+
+ sendWebNotification = () => {
+ var img = '../images/icon.png';
+ var text = 'HEY! Your task "null" is now overdue.';
+ var notification = new Notification('Meteorite', { body: text, icon: img });
+ }
+
render () {
if (!this.props.authApi.token) {
return
@@ -191,37 +216,40 @@ class NotificationsPage extends React.Component {
markAsRead,
markAllAsStaged,
clearCache,
+ notificationsPermission,
notifications,
loading: isFetchingNotifications,
error: fetchingNotificationsError,
} = this.props.notificationsApi;
// @TODO Move all this out of the render method.
+ // nick, do this ^ so you can fire off a web noti when the filtered/final
+ // notifications have a diff
let filterMethod = () => true;
switch (this.state.activeFilter) {
case Filters.PARTICIPATING:
filterMethod = n => (
n.reasons.some(({ reason }) => (
- reason === 'review_requested' ||
- reason === 'assign' ||
- reason === 'mention' ||
- reason === 'author'
+ reason === Reasons.REVIEW_REQUESTED ||
+ reason === Reasons.ASSIGN ||
+ reason === Reasons.MENTION ||
+ reason === Reasons.AUTHOR
))
);
break;
case Filters.ASSIGNED:
filterMethod = n => (
- n.reasons.some(({ reason }) => reason === 'assign')
+ n.reasons.some(({ reason }) => reason === Reasons.ASSIGN)
);
break;
case Filters.REVIEW_REQUESTED:
filterMethod = n => (
- n.reasons.some(({ reason }) => reason === 'review_requested')
+ n.reasons.some(({ reason }) => reason === Reasons.REVIEW_REQUESTED)
);
break;
case Filters.COMMENT:
filterMethod = n => (
- n.reasons.some(({ reason }) => reason === 'comment')
+ n.reasons.some(({ reason }) => reason === Reasons.COMMENT)
);
break;
default:
@@ -279,6 +307,10 @@ class NotificationsPage extends React.Component {
return (
{
+ this.setState({notificationsPermission: permission});
+ this.props.setUserItem('notificationsPermission', permission);
+ this.forceUpdate();
+ }
+
requestPage = (page = 1, optimizePolling = true) => {
const headers = {
'Authorization': `token ${this.props.token}`,
@@ -107,16 +121,23 @@ class NotificationsProvider extends React.Component {
}
requestFetchNotifications = (page = 1, optimizePolling = true) => {
+ if (this.state.syncing) {
+ // Don't try to send off another request if we're already trying to get one.
+ return Promise.reject();
+ }
+
+ this.setState({syncing: true});
return this.requestPage(page, optimizePolling)
.then(({headers, json}) => {
- if (json === null) return;
+ if (json === null) return [];
let nextPage = null;
const links = headers['link'];
if (links && links.next && links.next.page) {
nextPage = links.next.page;
}
return this.processNotificationsChunk(nextPage, json);
- });
+ })
+ .finally(() => this.setState({syncing: false}));
}
fetchNotifications = (page = 1, optimizePolling = true) => {
@@ -125,9 +146,15 @@ class NotificationsProvider extends React.Component {
return false;
}
+ if (this.state.loading) {
+ // Don't try to fetch if we're already fetching
+ return Promise.reject();
+ }
+
this.setState({ loading: true });
return this.requestFetchNotifications(page, optimizePolling)
- .catch(error => this.setState({ error }))
+ .then(() => this.setState({error: null}))
+ .catch(error =>this.setState({error}))
.finally(() => this.setState({ loading: false }));
}
@@ -199,9 +226,15 @@ class NotificationsProvider extends React.Component {
return false;
}
+ if (this.state.loading) {
+ // Don't try to fetch if we're already fetching
+ return Promise.reject();
+ }
+
this.setState({ loading: true });
return this.requestMarkAsRead(thread_id)
- .catch(error => this.setState({ error }))
+ .then(() => this.setState({error: null}))
+ .catch(error => this.setState({error}))
.finally(() => this.setState({ loading: false }));
}
@@ -217,7 +250,8 @@ class NotificationsProvider extends React.Component {
clearCache = () => {
this.setState({ loading: true });
return this.requestClearCache()
- .catch(error => this.setState({ error }))
+ .then(() => this.setState({error: null}))
+ .catch(error => this.setState({error}))
.finally(() => this.setState({ loading: false }));
}
@@ -278,21 +312,24 @@ class NotificationsProvider extends React.Component {
markAllAsStaged = () => {
this.setState({ loading: true });
return this.requestStageAll()
- .catch(error => this.setState({ error }))
+ .then(() => this.setState({error: null}))
+ .catch(error => this.setState({error}))
.finally(() => this.setState({ loading: false }));
}
stageThread = thread_id => {
this.setState({ loading: true });
return this.requestStageThread(thread_id)
- .catch(error => this.setState({ error }))
+ .then(() => this.setState({error: null}))
+ .catch(error => this.setState({error}))
.finally(() => this.setState({ loading: false }));
}
restoreThread = thread_id => {
this.setState({ loading: true });
return this.requestRestoreThread(thread_id)
- .catch(error => this.setState({ error }))
+ .then(() => this.setState({error: null}))
+ .catch(error => this.setState({error}))
.finally(() => this.setState({ loading: false }));
}
@@ -337,6 +374,7 @@ class NotificationsProvider extends React.Component {
clearCache: this.clearCache,
stageThread: this.stageThread,
restoreThread: this.restoreThread,
+ setNotificationsPermission: this.setNotificationsPermission,
});
}
}
@@ -350,6 +388,8 @@ const withNotificationsProvider = WrappedComponent => props => (
notifications,
getItem,
setItem,
+ getUserItem,
+ setUserItem,
clearCache,
removeItem
}) => (
@@ -358,6 +398,8 @@ const withNotificationsProvider = WrappedComponent => props => (
notifications={notifications}
getItemFromStorage={getItem}
setItemInStorage={setItem}
+ getUserItem={getUserItem}
+ setUserItem={setUserItem}
clearStorageCache={clearCache}
removeItemFromStorage={removeItem}
token={token}
diff --git a/src/providers/Storage.js b/src/providers/Storage.js
index f5165a0..7950c6e 100644
--- a/src/providers/Storage.js
+++ b/src/providers/Storage.js
@@ -1,38 +1,14 @@
import React from 'react';
import moment from 'moment';
import {Status} from '../constants/status';
-import {Reasons} from '../constants/reasons';
+import {createMockNotifications} from '../utils/mocks';
+
+const mockNotifications = createMockNotifications(100);
export const LOCAL_STORAGE_PREFIX = '__meteorite_noti_cache__';
+export const LOCAL_STORAGE_USER_PREFIX = '__meteorite_user_cache__';
export const LOCAL_STORAGE_STATISTIC_PREFIX = '__meteorite_statistic_cache__';
-const getMockReasons = n => {
- const reasons = Object.values(Reasons);
- const len = reasons.length;
- return new Array(n).fill(0).map(_ => ({
- reason: reasons[Math.floor(Math.random() * len)],
- time: moment().format()
- }));
-};
-
-const getMockNotification = randomNumber => ({
- id: randomNumber,
- updated_at: moment().format(),
- status: (randomNumber > 0.8 ? Status.STAGED : Status.QUEUED),
- reasons: getMockReasons(Math.ceil(randomNumber * 10)),
- type: ['Issue', 'PullRequest'][Math.floor(randomNumber * 2)],
- name: 'Mock - Fake notification name',
- url: 'https://github.com/test/repo/pull',
- repository: 'test/mock',
- number: Math.ceil(randomNumber * 1000),
- repositoryUrl: 'https://github.com/test/repo',
-});
-
-const mockNotifications = new Array(1000);
-for (let i = 0; i < mockNotifications.length; i++) {
- mockNotifications[i] = getMockNotification(Math.random());
-}
-
class StorageProvider extends React.Component {
constructor (props) {
super(props);
@@ -142,10 +118,22 @@ class StorageProvider extends React.Component {
try {
return JSON.parse(window.localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${id}`));
} catch (e) {
- return null;
+ return window.localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${id}`);
}
}
+ getUserItem = id => {
+ try {
+ return JSON.parse(window.localStorage.getItem(`${LOCAL_STORAGE_USER_PREFIX}${id}`));
+ } catch (e) {
+ return window.localStorage.getItem(`${LOCAL_STORAGE_USER_PREFIX}${id}`);
+ }
+ }
+
+ setUserItem = (id, value) => {
+ window.localStorage.setItem(`${LOCAL_STORAGE_USER_PREFIX}${id}`, JSON.stringify(value));
+ }
+
removeItem = id => {
// We never really want to purge anything from the cache if we can help it,
// since there's always a chance that a read notification can be resurrected.
@@ -169,6 +157,8 @@ class StorageProvider extends React.Component {
...this.state,
setItem: this.setItem,
getItem: this.getItem,
+ getUserItem: this.getUserItem,
+ setUserItem: this.setUserItem,
removeItem: this.removeItem,
clearCache: this.clearCache,
refreshNotifications: this.refreshNotifications,
diff --git a/src/styles/gradient.css b/src/styles/gradient.css
index 857a75e..74243e8 100644
--- a/src/styles/gradient.css
+++ b/src/styles/gradient.css
@@ -11,6 +11,7 @@
}
.button-container a {
+ position: relative;
text-align: center;
box-shadow: 0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08);
margin: 0 auto;
@@ -38,13 +39,25 @@
display: -ms-inline-flexbox;
display: inline-flex;
}
-.button-container a:hover {
- background: #f9f9f9;
- box-shadow: 0 2px 6px #4a4a4a5c;
+
+.button-container a:before {
+ content: "";
+ transition: all 150ms ease;
+ background: rgba(190, 197, 208, 0.25);
+ border-radius: 4px;
+ display: block;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ transform: scale(0);
}
-.button-container a:active {
- background: #e4e4e4;
- box-shadow: 0 0 0 #4a4a4a5c;
+.button-container a:hover:before {
+ transform: scale(1);
+}
+.button-container a:active:before {
+ background: rgba(190, 197, 208, 0.5)
}
.button-container-alt a {
@@ -114,10 +127,10 @@
}
@media only screen and (max-width: 1400px) {
- #section {
+ .section {
flex-direction: column !important;
}
- #item-text {
+ .item-text {
width: 600px;
}
}