Deprecate /notifications-redesign to /notifications

This commit is contained in:
Nicholas Zuber 2020-01-27 18:54:41 -05:00
parent 015758fc9e
commit 9380afbeb9
9 changed files with 459 additions and 3062 deletions

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react';
import amplitude from 'amplitude-js';
import {
Redirect,
Router,
Location,
LocationProvider
@ -12,7 +13,6 @@ import {
Login,
Pricing,
Guide,
Notifications,
NotificationsRedesign,
} from './pages';
@ -41,6 +41,10 @@ function PageTracker ({location}) {
return null;
}
function RedirectShell () {
return <Redirect noThrow to={routes.NOTIFICATIONS} />;
}
class App extends Component {
render() {
return (
@ -54,8 +58,8 @@ class App extends Component {
<Login path={routes.LOGIN} />
<Pricing path={routes.PRICING} />
<Guide path={routes.GUIDE} />
<Notifications path={routes.NOTIFICATIONS} />
<NotificationsRedesign path={routes.REDESIGN_NOTIFICATIONS} />
<RedirectShell path={routes.REDESIGN_NOTIFICATIONS} />
<NotificationsRedesign path={routes.NOTIFICATIONS} />
</Router>
</LocationProvider>
</AuthProvider>

File diff suppressed because it is too large Load Diff

View File

@ -1,552 +0,0 @@
/** @jsx jsx */
import React from 'react';
import styled from '@emotion/styled';
import {css, jsx} from '@emotion/core';
import {Link as RouterLink} from '@reach/router';
import {routes} from '../../constants';
import {
BasicPageWrapper,
forSmallScreens,
forMobile
} from '../common';
import WorkflowToggle from './WorkflowToggle';
import {ReactComponent as CloudOffSvg} from '../../images/svg/icons/cloud_off.svg'
import {ReactComponent as NotificationsActiveSvg} from '../../images/svg/icons/notifications_active.svg'
import {ReactComponent as PriorityHighSvg} from '../../images/svg/icons/priority_high.svg'
import {ReactComponent as TuneSvg} from '../../images/svg/icons/tune.svg'
import {ReactComponent as SpeedSvg} from '../../images/svg/icons/speed.svg'
import {ReactComponent as GpsFixedSvg} from '../../images/svg/icons/gps_fixed.svg'
import {ReactComponent as WbIridescentSvg} from '../../images/svg/icons/wb_iridescent.svg'
import {ReactComponent as TimelineSvg} from '../../images/svg/icons/timeline.svg'
import ItemPng from '../../images/screenshots/item.png';
import ItemTwoPng from '../../images/screenshots/item-2.png';
import ScreenshotPng from '../../images/screenshots/new/dashboard.png';
import ScoresPng from '../../images/screenshots/new/scores.png';
import ReasonsPng from '../../images/screenshots/new/reasons.png';
import RobinLogo from '../../images/logos/robin-logo.png';
import ForwardLogo from '../../images/logos/forward-logo.png';
import FacebookLogo from '../../images/logos/facebook-logo.png';
import '../../styles/gradient.css';
import '../../styles/font.css';
const themeColor = '#27B768';
const ALT_BACKGROUND_COLOR = '#f6f2ed';
const ProductHuntButton = () => (
<a href="https://www.producthunt.com/posts/meteorite?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-meteorite" target="_blank">
<img
src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=145651&theme=dark"
alt="Meteorite - Smarter GitHub notifications. | Product Hunt Embed"
style={{width: 200, height: 43}}
width="200px"
height="43px" />
</a>
);
const Outer = styled('div')`
background: ${p => p.alt ? ALT_BACKGROUND_COLOR : 'none'};
`;
const Container = styled('div')`
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: ${p => p.column ? 'column' : 'row'};
max-width: 1080px;
min-height: 100px;
margin: 0 auto;
padding: 1rem 0;
margin-bottom: 5rem;
${forSmallScreens(`
padding-left: 2.5rem;
padding-right: 2.5rem;
`)}
${forMobile(`
margin-bottom: 2.5rem;
`)}
`;
const FlexItem = styled('div')`
flex: ${(({flex = 1}) => flex)};
align-items: center;
display: flex;
flex-wrap: wrap;
`;
const Button = styled(RouterLink)`
cursor: pointer;
position: relative;
display: inline-block;
text-decoration: none;
font-weight: 400;
color: #333333;
text-align: center;
vertical-align: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: transparent;
border: 0px solid transparent;
margin: 0rem 0.25rem;
width: max-content;
padding: 0.125rem 1rem;
font-size: 18px;
line-height: 1.75;
border-radius: 5px;
-webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
&:hover {
background-color: #f4f4f4;
border-color: #f4f4f4;
}
&:active {
background-color: #eee;
border-color: #eee;
}
`;
const MainButton = styled(Button)`
background-color: ${themeColor};
border-color: ${themeColor};
color: #fff;
font-size: 18px;
&:hover {
background-color: #249959;
border-color: #249959;
}
&:active {
background-color: #20894f;
border-color: #20894f;
}
`;
const HeroButton = styled(MainButton)`
font-size: 22px;
margin: -4px 8px 0;
margin-left: 0;
`;
const FlexBreak = styled('div')`
flex-basis: 100%;
height: 0;
min-height: ${p => p.height || 0}px;
`;
const HeroTitle = styled('h1')`
font-size: 68px;
line-height: 68px;
margin: 0 auto 6px;
font-family: medium-marketing-display-font,Georgia,Cambria,Times New Roman,Times,serif;
font-weight: 500;
${forMobile(`
font-size: 52px;
line-height: 58px;
`)}
`;
const HeroSubtitle = styled('h1')`
color: #6c757d;
font-size: 24px;
line-height: 28px;
margin: 0 auto 16px;
width: 80%;
margin-left: 0;
font-family: medium-content-sans-serif-font, Inter UI, system-ui, sans-serif;
font-weight: 500;
${forMobile(`
margin: 0 auto 32px;
font-size: 20px;
line-height: 24px;
`)}
`;
const Title = styled('h1')`
font-size: 38px;
line-height: 38px;
margin: 0 auto 12px;
font-family: medium-marketing-display-font,Georgia,Cambria,Times New Roman,Times,serif;
font-weight: 500;
text-align: center;
`;
const Subtitle = styled('h1')`
color: #6c757d;
font-size: 20px;
line-height: 24px;
margin: 0 auto 16px;
font-family: medium-content-sans-serif-font, Inter UI, system-ui, sans-serif;
font-weight: 500;
max-width: 540px;
text-align: center;
`;
const ItemImage = styled('img')`
width: 847px;
box-shadow: rgba(84, 70, 35, 0) 0px 4px 18px, rgba(84, 70, 35, 0.15) 0px 2px 8px;
border-radius: 6px;
margin-left: ${props => props.left || 0}px;
margin-top: ${props => props.top || 0}px;
transform: rotate(${props => props.deg || 0}deg) scale(${props => props.scale || 1});
`;
const HeroLeft = styled(FlexItem)`
z-index: 1;
flex-grow: 1;
width: 50%;
${forMobile(`
margin: 0 auto;
width: 100%;
text-align: center;
`)}
`;
const HeroRight = styled(FlexItem)`
flex-grow: 1;
width: 50%;
${forMobile(`
display: none;
`)}
`;
const DotsBackground = styled('div')`
position: absolute;
height: 400px;
width: 100%;
margin-left: -60px;
background: radial-gradient(transparent 50%, #fffefd), \
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAKElEQVQoU2NkIBIwEqmOgQ4KX715/x/mHDERQbiNGFZTXyGuUKC+rwHAcQwLu0IifQAAAABJRU5ErkJggg==) repeat;
${forMobile(`
width: 90%;
margin: 0 auto;
`)}
`;
const FooterImageContainer = styled('div')`
position: relative;
height: 250px;
width: 100%;
text-align: center;
background: radial-gradient(transparent 50%, ${ALT_BACKGROUND_COLOR}), \
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAKElEQVQoU2NkIBIwEqmOgQ4KX715/x/mHDERQbiNGFZTXyGuUKC+rwHAcQwLu0IifQAAAABJRU5ErkJggg==) repeat;
${forMobile(`
display: none;
`)}
`;
const CompanyQuotesContainer = styled('div')`
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 4rem;
${forMobile(`
flex-direction: column;
max-width: 300px;
margin: 0 auto;
`)}
`;
const HorizontalFlexContainer = styled('div')`
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
margin-top: 4rem;
`;
const FeatureItem = styled('div')(({color}) => `
flex: 1;
${forMobile(`
flex: 1 0 34%;
`)}
flex-direction: column;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 4rem;
padding: 0 12px;
svg {
background: ${color}28;
fill: ${color};
border-radius: 100%;
padding: 12px;
height: 24px;
width: 24px;
}
h3 {
font-size: 24px;
line-height: 25px;
text-align: center;
margin: 18px auto 8px;
font-family: medium-marketing-display-font,Georgia,Cambria,Times New Roman,Times,serif;
font-weight: 500;
}
p {
font-size: 17px;
line-height: 22px;
margin: 0;
text-align: center;
}
`);
const HorizontalListItem = styled('div')(({last}) => `
flex: 1;
border-right: ${last ? '0px' : '1px'} solid rgba(214, 212, 209, 0.3);
padding: 0 32px;
${forMobile(`
border-right: 0;
border-bottom: ${last ? '0px' : '1px'} solid rgba(214, 212, 209, 0.3);
padding: 32px 0;
`)}
`);
const Quote = styled('p')`
margin: 0;
font-family: medium-content-title-font, Inter UI, sans-serif;
font-size: 18px;
font-weight: 400;
&:before {
content: open-quote;
}
&:after {
content: close-quote;
}
`;
const CompanyPerson = styled('div')`
transform: scale(0.95);
display: flex;
padding: 8px 0;
margin: 0;
img {
display: block;
height: 30px;
width: 30px;
border-radius: 2px;
}
span {
display: block;
padding: 0 16px;
font-size: 16px;
line-height: 20px;
color: #37352f80;
}
`;
export default function Scene (props) {
const baseItemOffset = {
scale: 0.1,
top: -185,
left: 20
};
return (
<BasicPageWrapper {...props}>
{/* Hero */}
<Container css={css`
${forMobile(`
flex-direction: column;
`)}
`}>
<HeroLeft>
<HeroTitle>{'Manage your notifications.'}</HeroTitle>
<HeroSubtitle>{'Meteorite helps organize, filter, and prioritize your\
GitHub notifications to make your life easier'}</HeroSubtitle>
<FlexBreak />
<div css={css`
display: flex;
justify-content: center;
align-items: center;
${forMobile(`
display: block;
margin: 0 auto;
width: 300px;
`)}
`}>
<HeroButton css={css`${forMobile(`margin-bottom: 12px;`)}`} to={routes.LOGIN}>{'Login / Sign up'}</HeroButton>
<ProductHuntButton />
</div>
</HeroLeft>
<HeroRight>
<DotsBackground />
<ItemImage
scale={baseItemOffset.scale + 0.8}
left={baseItemOffset.left + 8}
top={25}
src={ItemTwoPng} />
<ItemImage
deg={0}
scale={baseItemOffset.scale + 0.95}
left={baseItemOffset.left}
top={baseItemOffset.top + 4}
src={ItemPng} />
</HeroRight>
</Container>
{/* Testimonials */}
<Container column>
<FlexBreak height={60} />
<Title>{'Hear what others are saying'}</Title>
<Subtitle>{'Loved by other human beings, just like you'}</Subtitle>
<CompanyQuotesContainer>
<HorizontalListItem>
<Quote>{`So good! I love the importance sorting!`}</Quote>
<CompanyPerson>
<img src={FacebookLogo} />
<span>
{'— Mike Grabowski'}<br />
{'Software Architect, React Native'}
</span>
</CompanyPerson>
</HorizontalListItem>
<HorizontalListItem>
<Quote>{`I've been using it for a bit and it's so useful, especially if you use GitHub for work.`}</Quote>
<CompanyPerson>
<img src={RobinLogo} />
<span>
{'— Trevor Suarez'}<br />
{'Robin, Backend Software Engineer'}
</span>
</CompanyPerson>
</HorizontalListItem>
<HorizontalListItem last>
<Quote>{`Awww sh*t, nice.`}</Quote>
<CompanyPerson>
<img src={ForwardLogo} />
<span>
{'— Chris Walker'}<br />
{'Forward, Software Engineer'}
</span>
</CompanyPerson>
</HorizontalListItem>
</CompanyQuotesContainer>
</Container>
{/* Lifecycle */}
<Outer alt>
<Container column>
<FlexBreak height={60} />
<Title>{'The Notification Lifecycle'}</Title>
<Subtitle>{'GitHub notifications can actually be your friend with a little bit of discipline.\
All we need to do is filter out the noise & organize things in a way that makes sense.'}</Subtitle>
<WorkflowToggle
easeTimingMs={100}
items={[
{
id: 0,
title: 'Filter The Noise',
description: 'Stay focused on the important things. We\'ll only show the notifications that matter to you.',
image: ScreenshotPng
},
{
id: 1,
title: 'Highlight The Callouts',
description: 'When things stand out, you shouldn\'t miss it. We mark notifications when there\'s something interesting going down.',
image: ReasonsPng
},
{
id: 2,
title: 'Sort By Importance',
description: 'Don\'t get lost at sea the most important notifications stay at the top of the list.',
image: ScoresPng
},
]}
/>
<FlexBreak height={100} />
<Title css={css`font-size: 32px; line-height: 38px; margin-bottom: 24px;`}>{'Start using Meteorite absolutely free'}</Title>
<HeroButton to={routes.LOGIN} css={css`margin: 0 auto;`}>{'Login / Sign up'}</HeroButton>
<FlexBreak height={80} />
</Container>
</Outer>
{/* Features */}
<Container column>
<FlexBreak height={20} />
<Title>{'Features built for getting things done.'}</Title>
<Subtitle>{'All of the features of Meteorite are specifically designed'}<br />
{'for optimizing your workday'}</Subtitle>
<HorizontalFlexContainer>
<FeatureItem color={'#9C27B0'}>
<CloudOffSvg />
<h3>{'Serverless'}</h3>
<p>{'Any notification scoring and storing is done completely offline.'}</p>
</FeatureItem>
<FeatureItem color={'#ffc915'}>
<NotificationsActiveSvg />
<h3>{'Desktop Notifications'}</h3>
<p>{'Get notified when we do ability to turn on desktop notifications.'}</p>
</FeatureItem>
<FeatureItem color={'#27B768'}>
<PriorityHighSvg />
<h3>{'Auto Sorting'}</h3>
<p>{'Keep your most important notifications at the top of the list.'}</p>
</FeatureItem>
<FeatureItem color={'#00A0F5'}>
<TuneSvg />
<h3>{'Filter Noise'}</h3>
<p>{'Any notifications that don\'t directly involve you are hidden.'}</p>
</FeatureItem>
<FlexBreak />
<FeatureItem color={'#EE3F46'}>
<SpeedSvg />
<h3>{'Dead Simple'}</h3>
<p>{'No integrations just log in and start working.'}</p>
</FeatureItem>
<FeatureItem color={'#10293c'}>
<GpsFixedSvg />
<h3>{'Live Updates'}</h3>
<p>{'All of your notifications are processed in real time.'}</p>
</FeatureItem>
<FeatureItem color={'#fd9446'}>
<WbIridescentSvg />
<h3>{'Reasoning'}</h3>
<p>{'We\'ll also tell you why you\'re getting each notification.'}</p>
</FeatureItem>
<FeatureItem color={'#fc46fd'}>
<TimelineSvg />
<h3>{'Statistics'}</h3>
<p>{'Better understand how you work with data visualizations.'}</p>
</FeatureItem>
</HorizontalFlexContainer>
</Container>
{/* Closer */}
<Outer alt>
<Container column>
<FlexBreak height={80} />
<Title>{'Better notifications for everyone.'}</Title>
<FlexBreak height={20} />
<HeroButton to={routes.LOGIN} css={css`margin: 0 auto;`}>{'Login / Sign up'}</HeroButton>
<FlexBreak height={40} />
<FooterImageContainer>
<ItemImage
scale={baseItemOffset.scale + 0.8}
top={60}
src={ItemPng} />
<ItemImage
deg={0}
scale={baseItemOffset.scale + 0.95}
top={-30}
src={ItemTwoPng} />
</FooterImageContainer>
<FlexBreak height={40} />
</Container>
</Outer>
</BasicPageWrapper>
);
};

View File

@ -3,7 +3,7 @@ import { compose } from 'recompose';
import { withAuthProvider } from '../../providers/Auth';
import { withCookiesProvider } from '../../providers/Cookies';
import { OAUTH_TOKEN_COOKIE } from '../../constants/cookies';
import Scene from './Scene.new';
import Scene from './Scene';
class HomePage extends React.Component {
onLogout = () => {

View File

@ -24,7 +24,7 @@ class LoginPage extends React.Component {
render () {
if (this.props.authApi.token) {
return <Redirect noThrow to={routes.REDESIGN_NOTIFICATIONS} />
return <Redirect noThrow to={routes.NOTIFICATIONS} />
}
return (

File diff suppressed because it is too large Load Diff

View File

@ -1,466 +0,0 @@
import React from 'react';
import moment from 'moment';
import { Redirect, navigate } from "@reach/router";
import { compose } from 'recompose';
import { withNotificationsProvider } from '../../providers/Notifications';
import { withAuthProvider } from '../../providers/Auth';
import { withCookiesProvider } from '../../providers/Cookies';
import { withStorageProvider } from '../../providers/Storage';
import { OAUTH_TOKEN_COOKIE } from '../../constants/cookies';
import { routes } from '../../constants';
import { Filters } from '../../constants/filters';
import { Status } from '../../constants/status';
import { Reasons, Badges } from '../../constants/reasons';
import Scene, { getMessageFromReasons } from './Scene';
import issueIcon from '../../images/issue-bg.png';
import prIcon from '../../images/pr-bg.png';
import tabIcon from '../../images/icon.png';
import tabDotIcon from '../../images/iconDot.png';
const PER_PAGE = 10;
// @TODO Move these functions.
/**
* Given a notification, give it a score based on its importance.
*
* There are some interesting workarounds that go into this algorithm to account
* for GitHub's broken notifications API -- but we will get to that later. First,
* let's start off with the basics of scoring.
*
* There are a few "reasons" that we can be getting a notification, each having
* an initial weight of importance:
*
* - MENTION -> 8
* - ASSIGN -> 14
* - REVIEW_REQUESTED -> 30
* - SUBSCRIBED -> 3
* - COMMENT -> 3
* - AUTHOR -> 10
* - OTHER -> 2
*
* There are some rules that go to giving out these scores, primarily being the
* first time we see one of these unique reasons, we award the notification with
* the respective score, but a reason that transitions into itself will be awarded
* a degraded score of min(ceil(n/3), 2). For example:
*
* - null, MENTION, MENTION -> 0, 8, 3
* - null, ASSIGN, ASSIGN, REVIEW_REQUESTED, -> 0, 14, 5, 20
* - null, SUBSCRIBED, SUBSCRIBED, SUBSCRIBED -> 0, 3, 2, 2
*
* @param {Object} notification Some notification to score.
* @return {number} The score.
*/
function scoreOf (notification) {
const {reasons} = notification;
let score = 0;
let prevReason = null;
for (let i = 0; i < reasons.length; i++) {
const reason = reasons[i].reason;
if (prevReason && reason === prevReason) {
const degradedScore = Math.ceil(scoreOfReason[reason] / 3);
score += Math.max(degradedScore, 2);
} else {
score += scoreOfReason[reason];
}
prevReason = reason;
}
return score;
};
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. The last update should be within the past 30 minutes.
// The specific time frame and reasons count is subject to change.
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) {
badges.push(Badges.HOT);
}
}
// If there's a lot of activity going on within the thread in general.
// The specific nunmber should be relative to average number of thread lengths.
// We can track a running statistic as we see notifications update.
if (len > 6) {
badges.push(Badges.COMMENTS);
}
// 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.
if (notification.reasons.some(r => r.reason === Reasons.REVIEW_REQUESTED) &&
timeSinceLastUpdate > 60 * 4) {
badges.push(Badges.OLD);
}
return badges;
};
const scoreOfReason = {
[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 => ({
...notification,
score: scoreOf(notification),
badges: badgesOf(notification)
});
class NotificationsPage extends React.Component {
constructor (props) {
super(props);
this.notificationSent = false;
this.isUnreadTab = false;
}
state = {
currentTime: moment(),
error: null,
notificationSent: false,
isFirstTimeUser: false,
isSearching: false,
query: null,
activeFilter: Filters.PARTICIPATING,
activeStatus: Status.QUEUED,
currentPage: 1
}
componentDidMount () {
const isFirstTimeUser = !this.props.storageApi.getUserItem('hasOnboarded');
// Harsh but fair.
if (window.outerWidth < 1100) {
navigate(routes.REDESIGN_NOTIFICATIONS);
return;
}
if (isFirstTimeUser) {
this.setState({isFirstTimeUser: true});
// this.props.storageApi.setUserItem('hasOnboarded', true);
}
this.props.notificationsApi.fetchNotifications();
this.tabSyncer = setInterval(() => {
if (!document.hidden && this.isUnreadTab) {
this.updateTabIcon(false);
}
}, 2000);
this.syncer = setInterval(() => {
this.props.notificationsApi.fetchNotificationsSync()
.then(error => this.setState({error: null}))
.catch(error => this.setState({error}));
this.setState({currentTime: moment()});
}, 8 * 1000);
}
shouldComponentUpdate (nextProps, nextState) {
if (this.props.notificationsApi.newChanges !== nextProps.notificationsApi.newChanges) {
this.notificationSent = false;
}
// The idea here is if we've just updated the prevNotifications state, then
// we don't want to trigger a rerender.
return nextState.prevNotifications === this.state.prevNotifications;
}
componentWillUnmount () {
clearInterval(this.syncer);
clearInterval(this.tabSyncer);
}
onChangePage = page => {
this.setState({ currentPage: page });
}
onSetActiveFilter = filter => {
this.setState({ activeFilter: filter, currentPage: 1 });
}
onSetActiveStatus = status => {
this.setState({ activeStatus: status, currentPage: 1 });
}
onClearQuery = () => {
this.setState({ query: null });
}
onLogout = () => {
// Remove cookie and invalidate token on client.
this.props.cookiesApi.removeCookie(OAUTH_TOKEN_COOKIE);
this.props.authApi.invalidateToken();
}
onSearch = event => {
const text = event.target.value;
// Ignore empty queries.
if (text.length <= 0) {
return;
}
this.setState({ isSearching: true });
setTimeout(() => {
this.setState({
query: text,
isSearching: false
});
}, 500);
}
enhancedOnStageThread = (thread_id, repository) => {
this.props.storageApi.incrStat('stagedCount');
this.props.storageApi.incrStat(repository + '-stagedCount');
this.props.notificationsApi.stageThread(thread_id);
}
enhancedOnMarkAsRead = (thread_id, repository) => {
this.props.storageApi.incrStat('stagedCount');
this.props.storageApi.incrStat(repository + '-stagedCount');
this.props.notificationsApi.markAsRead(thread_id);
}
restoreThread = thread_id => {
this.props.notificationsApi.restoreThread(thread_id);
}
setNotificationsPermission = (...args) => {
this.props.notificationsApi.setNotificationsPermission(...args);
}
updateTabIcon (hasUnread = true) {
this.isUnreadTab = hasUnread;
var link = document.querySelector("link[rel*='icon']") || document.createElement('link');
link.rel = 'shortcut icon';
link.href = hasUnread ? tabDotIcon : tabIcon;
document.getElementsByTagName('head')[0].appendChild(link);
}
sendWebNotification = newNotifcations => {
if (this.notificationSent || newNotifcations.length === 0) {
return;
}
// Only show these notifications and title change if the tab is out of focus.
if (!document.hidden) {
return;
}
// Set this even if we don't actually send the notification due to permissions.
this.notificationSent = true;
this.updateTabIcon();
// No permission, no notification.
if (this.props.notificationsApi.notificationsPermission !== 'granted') {
return;
}
const n = newNotifcations[0];
const reasonByline = getMessageFromReasons(n.reasons, n.type);
const additionalInfo = newNotifcations.length > 1
? ` (+${newNotifcations.length} more)`
: '';
const notification = new Notification(n.name + additionalInfo, {
body: reasonByline,
icon: n.type === "Issue" ? issueIcon : prIcon,
badge: n.type === "Issue" ? issueIcon : prIcon,
requireInteraction: true,
});
notification.addEventListener('click', () => {
this.updateTabIcon(false);
this.enhancedOnStageThread(n.id, n.repository);
window.open(n.url);
})
// Manually close for legacy browser support.
setTimeout(notification.close.bind(notification), 10000);
}
getFilteredNotifications = () => {
const {notifications} = this.props.notificationsApi;
let filterMethod = () => true;
switch (this.state.activeFilter) {
case Filters.PARTICIPATING:
filterMethod = n => (
n.reasons.some(({ reason }) => (
reason === Reasons.REVIEW_REQUESTED ||
reason === Reasons.ASSIGN ||
reason === Reasons.MENTION ||
reason === Reasons.AUTHOR
))
);
break;
case Filters.ASSIGNED:
filterMethod = n => (
n.reasons.some(({ reason }) => reason === Reasons.ASSIGN)
);
break;
case Filters.REVIEW_REQUESTED:
filterMethod = n => (
n.reasons.some(({ reason }) => reason === Reasons.REVIEW_REQUESTED)
);
break;
case Filters.COMMENT:
filterMethod = n => (
n.reasons.some(({ reason }) => reason === Reasons.COMMENT)
);
break;
default:
filterMethod = () => true;
}
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 notificationsToRender = [];
switch (this.state.activeStatus) {
case Status.CLOSED:
notificationsToRender = notificationsClosed;
break;
case Status.STAGED:
notificationsToRender = notificationsStaged;
break;
case Status.QUEUED:
default:
notificationsToRender = notificationsQueued;
}
let scoredAndSortedNotifications = notificationsToRender
.map(decorateWithScore)
.sort((a, b) => b.score - a.score);
// We gotta make sure to search notifications before we paginate.
// Otherwise we'd just end up searching on the current page, which is bad.
if (this.state.query) {
scoredAndSortedNotifications = scoredAndSortedNotifications.filter(n => (
n.name.toLowerCase().indexOf(this.state.query.toLowerCase()) > -1)
)
}
if (this.props.notificationsApi.newChanges) {
const filteredNewChanges = this.props.notificationsApi.newChanges.filter(n => (
scoredAndSortedNotifications.some(fn => fn.id === n.id)
));
if (filteredNewChanges.length > 0) {
this.sendWebNotification(filteredNewChanges);
}
}
return {
notifications: scoredAndSortedNotifications,
queuedCount: notificationsQueued.length,
stagedCount: notificationsStaged.length,
closedCount: notificationsClosed.length,
};
}
render () {
if (!this.props.authApi.token) {
return <Redirect noThrow to={routes.HOME} />
}
const {
fetchNotifications,
markAllAsStaged,
clearCache,
notificationsPermission,
loading: isFetchingNotifications,
error: fetchingNotificationsError,
} = this.props.notificationsApi;
const {
notifications: scoredAndSortedNotifications,
queuedCount,
stagedCount,
closedCount,
} = this.getFilteredNotifications();
let firstIndex = (this.state.currentPage - 1) * PER_PAGE;
let lastIndex = (this.state.currentPage * PER_PAGE);
let notificationsOnPage = scoredAndSortedNotifications.slice(firstIndex, lastIndex);
let lastPage = Math.ceil(scoredAndSortedNotifications.length / PER_PAGE);
let firstNumbered = firstIndex + 1;
let lastNumbered = Math.min(lastIndex, scoredAndSortedNotifications.length);
if (scoredAndSortedNotifications.length === 0) {
firstIndex = 0;
lastIndex = 0;
notificationsOnPage = [];
lastPage = 1;
firstNumbered = 0;
lastNumbered = 0;
}
const stagedTodayCount = this.props.storageApi.getStat('stagedCount')[0];
const stagedStatistics = this.props.storageApi.getStat(
'stagedCount',
this.state.currentTime.clone().startOf('week').subtract(1, 'week'),
this.state.currentTime.clone().endOf('week')
);
return (
<Scene
currentTime={this.state.currentTime}
stagedStatistics={stagedStatistics}
isFirstTimeUser={this.state.isFirstTimeUser}
setNotificationsPermission={this.setNotificationsPermission}
notificationsPermission={notificationsPermission}
queuedCount={queuedCount}
stagedCount={stagedCount}
closedCount={closedCount}
stagedTodayCount={stagedTodayCount || 0}
first={firstNumbered}
last={lastNumbered}
lastPage={lastPage}
allNotificationsCount={scoredAndSortedNotifications.length}
notifications={notificationsOnPage}
query={this.state.query}
page={this.state.currentPage}
activeStatus={this.state.activeStatus}
activeFilter={this.state.activeFilter}
onChangePage={this.onChangePage}
onLogout={this.onLogout}
onSetActiveStatus={this.onSetActiveStatus}
onSearch={this.onSearch}
onClearQuery={this.onClearQuery}
onFetchNotifications={fetchNotifications}
onMarkAsRead={this.enhancedOnMarkAsRead}
onMarkAllAsStaged={markAllAsStaged}
onClearCache={clearCache}
onStageThread={this.enhancedOnStageThread}
onRestoreThread={this.restoreThread}
onRefreshNotifications={this.props.storageApi.refreshNotifications}
isSearching={this.state.isSearching}
isFetchingNotifications={isFetchingNotifications}
fetchingNotificationsError={fetchingNotificationsError || this.state.error}
onSetActiveFilter={this.onSetActiveFilter}
/>
);
}
};
const enhance = compose(
withStorageProvider,
withAuthProvider,
withCookiesProvider,
withNotificationsProvider
);
export default enhance(NotificationsPage);

View File

@ -255,7 +255,7 @@ export function BasicPageWrapper ({loggedIn, onLogout, children}) {
<LoginContainer>
<Button to={routes.GUIDE}>{'Guide'}</Button>
<Button to={routes.PRICING}>{'Pricing'}</Button>
<Button to={routes.REDESIGN_NOTIFICATIONS} css={css`
<Button to={routes.NOTIFICATIONS} css={css`
&::after {
content: "";
position: absolute;

View File

@ -1,5 +1,4 @@
export {default as Home} from './Home';
export {default as Notifications} from './Notifications';
export {default as NotificationsRedesign} from './NotificationsRedesign';
export {default as Login} from './Login';
export {default as Pricing} from './Pricing';