Many tickets and new readme

This commit is contained in:
Nicholas Zuber 2018-11-09 00:37:04 -05:00
parent 8652ec4429
commit 2d98079896
23 changed files with 905 additions and 1243 deletions

BIN
.github/icon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
.github/template.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -1,7 +1,11 @@
# meteorite
# <img width="650" src=".github/icon.png" /> meteorite
> Smarter GitHub notifications.
<div align="center">
<img width="650" src=".github/template.png" />
</div>
## License
This software is free to use under the MIT License. See [this reference](https://opensource.org/licenses/MIT) for license text and copyright information.

View File

@ -64,7 +64,7 @@
"start": "node scripts/start.js",
"build": "node scripts/build.js && cp build/index.html build/200.html",
"test": "node scripts/test.js",
"deploy": "surge"
"deploy": "npm run build && surge ./build"
},
"eslintConfig": {
"extends": "react-app"

View File

@ -5,6 +5,7 @@ import alarm from './svg/alarm.svg';
import allInbox from './svg/all_inbox.svg';
import back from './svg/back.svg';
import bolt from './svg/bolt.svg';
import boltAlt from './svg/bolt-alt.svg';
import boltWhite from './svg/bolt-white.svg';
import bookmarkAlt from './svg/bookmark-alt.svg';
import bookmarkAltWhite from './svg/bookmark-alt-white.svg';
@ -43,6 +44,14 @@ import x from './svg/x.svg';
import zap from './svg/zap.svg';
import ear from './svg/ear.svg';
import ring from './svg/ring.svg';
import eye from './svg/eye.svg';
import eyeWhite from './svg/eye-white.svg';
import warn from './svg/warn.svg';
import bubbles from './svg/bubbles.svg';
import cloudOff from './svg/cloudoff.svg';
import tag from './svg/tag.svg';
import tagWhite from './svg/tag-white.svg';
import sync from './svg/sync.svg';
import issue_closed from './svg/github/issue-closed.svg';
import issue_open from './svg/github/issue-open.svg';
@ -76,6 +85,7 @@ Icon.AllInbox = createIcon(allInbox);
Icon.Back = createIcon(back);
Icon.Bolt = createIcon(bolt);
Icon.BoltWhite = createIcon(boltWhite);
Icon.BoltAlt = createIcon(boltAlt);
Icon.BookmarkAlt = createIcon(bookmarkAlt);
Icon.BookmarkAltWhite = createIcon(bookmarkAltWhite);
Icon.Bookmark = createIcon(bookmark);
@ -113,6 +123,14 @@ Icon.X = createIcon(x);
Icon.Zap = createIcon(zap);
Icon.Ear = createIcon(ear);
Icon.Ring = createIcon(ring);
Icon.Eye = createIcon(eye);
Icon.EyeWhite = createIcon(eyeWhite);
Icon.Warn = createIcon(warn);
Icon.Bubbles = createIcon(bubbles);
Icon.CloudOffWhite = createIcon(cloudOff);
Icon.Tag = createIcon(tag);
Icon.TagWhite = createIcon(tagWhite);
Icon.Sync = createIcon(sync);
Icon.IssueClosed = createIcon(issue_closed);
Icon.IssueOpen = createIcon(issue_open);

View File

@ -0,0 +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="red" 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>

After

Width:  |  Height:  |  Size: 297 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><circle cx="7.2" cy="14.4" r="3.2"/><circle cx="14.8" cy="18" r="2"/><circle cx="15.2" cy="8.8" r="4.8"/></svg>

After

Width:  |  Height:  |  Size: 231 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path fill="#fff" d="M19.35 10.04C18.67 6.59 15.64 4 12 4c-1.48 0-2.85.43-4.01 1.17l1.46 1.46C10.21 6.23 11.08 6 12 6c3.04 0 5.5 2.46 5.5 5.5v.5H19c1.66 0 3 1.34 3 3 0 1.13-.64 2.11-1.56 2.62l1.45 1.45C23.16 18.16 24 16.68 24 15c0-2.64-2.05-4.78-4.65-4.96zM3 5.27l2.75 2.74C2.56 8.15 0 10.77 0 14c0 3.31 2.69 6 6 6h11.73l2 2L21 20.73 4.27 4 3 5.27zM7.73 10l8 8H6c-2.21 0-4-1.79-4-4s1.79-4 4-4h1.73z"/></svg>

After

Width:  |  Height:  |  Size: 528 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path fill="#fff" d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>

After

Width:  |  Height:  |  Size: 360 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>

After

Width:  |  Height:  |  Size: 347 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 361 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path fill="#fff" d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/></svg>

After

Width:  |  Height:  |  Size: 400 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/></svg>

After

Width:  |  Height:  |  Size: 387 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>

After

Width:  |  Height:  |  Size: 188 B

View File

@ -1,5 +1,6 @@
export const Filters = {
ALL: 'all',
ASSIGNED: 'assign',
REVIEW_REQUESTED: 'review_requested',
PARTICIPATING: 'participating',
SUBSCRIBED: 'subscribed',

View File

@ -31,7 +31,7 @@ class Tooltip extends React.Component {
}
const {tooltipOffsetX, tooltipOffsetY} = this.props;
const {x, y, height} = event.target.getBoundingClientRect();
const {x, y} = event.target.getBoundingClientRect();
const text = document.createTextNode(this.props.message);
const tooltipElement = document.createElement('div');

View File

@ -7,7 +7,7 @@ import Icon from '../../components/Icon';
import Logo from '../../components/Logo';
import '../../styles/gradient.css';
function createImagePlaceholder () {
function createImagePlaceholder (highlight) {
return (
<ImagePlaceholder>
{/* navigation backdrop */}
@ -136,19 +136,21 @@ function createImagePlaceholder () {
<div style={{
position: 'absolute',
background: '#fff',
top: 190,
left: -20,
right: -20,
top: highlight === 'badges' ? 190 : 170,
left: highlight === 'badges' ? -20 : 0,
right: highlight === 'badges' ? -20 : 0,
height: 50,
borderRadius: 4,
boxShadow: 'rgba(130, 126, 126, 0.27) 0px 3px 8px',
boxShadow: highlight === 'badges'
? 'rgba(130, 126, 126, 0.27) 0px 3px 8px'
: '0 0 0',
}}>
<div style={{
position: 'absolute',
background: '#dee1e6',
top: 15,
left: 30,
width: 200,
left: highlight === 'badges' ? 30 : 100,
width: highlight === 'badges' ? 160 : 120,
height: 10,
borderRadius: 50
}} />
@ -156,7 +158,7 @@ function createImagePlaceholder () {
position: 'absolute',
background: '#dee1e6',
top: 30,
left: 30,
left: highlight === 'badges' ? 30 : 100,
width: 50,
height: 7,
borderRadius: 50
@ -164,20 +166,44 @@ function createImagePlaceholder () {
<div style={{
position: 'absolute',
top: 12,
left: 300,
left: 315,
width: 30,
height: 30
}}>
<Icon.Hot shrink={1.1} />
{highlight === 'badges' ? (
<Icon.Hot shrink={1.1} />
) : (
<div
style={{
background: '#f42839',
height: 15,
width: 15,
marginTop: 5,
borderRadius: '100%'
}}
/>
)}
</div>
<div style={{
position: 'absolute',
top: 13,
left: 330,
top: 12,
left: 345,
width: 30,
height: 30
}}>
<Icon.Convo shrink={1.1} />
{highlight === 'badges' ? (
<Icon.Convo shrink={1.1} />
) : (
<div
style={{
background: '#009ef8',
height: 15,
width: 15,
marginTop: 5,
borderRadius: '100%'
}}
/>
)}
</div>
<div style={{
position: 'absolute',
@ -211,19 +237,21 @@ function createImagePlaceholder () {
<div style={{
position: 'absolute',
background: '#fff',
top: 280,
left: -20,
right: -20,
top: highlight === 'badges' ? 280 : 220,
left: highlight === 'badges' ? -20 : 0,
right: highlight === 'badges' ? -20 : 0,
height: 50,
borderRadius: 4,
boxShadow: 'rgba(130, 126, 126, 0.27) 0px 3px 8px',
boxShadow: highlight === 'badges'
? 'rgba(130, 126, 126, 0.27) 0px 3px 8px'
: '0 0 0',
}}>
<div style={{
position: 'absolute',
background: '#dee1e6',
top: 15,
left: 30,
width: 220,
left: highlight === 'badges' ? 30 : 100,
width: highlight === 'badges' ? 220 : 140,
height: 10,
borderRadius: 50
}} />
@ -231,7 +259,7 @@ function createImagePlaceholder () {
position: 'absolute',
background: '#dee1e6',
top: 30,
left: 30,
left: highlight === 'badges' ? 30 : 100,
width: 30,
height: 7,
borderRadius: 50
@ -240,7 +268,7 @@ function createImagePlaceholder () {
position: 'absolute',
background: '#dee1e6',
top: 30,
left: 64,
left: highlight === 'badges' ? 64 : 134,
width: 7,
height: 7,
borderRadius: '100%'
@ -248,11 +276,23 @@ function createImagePlaceholder () {
<div style={{
position: 'absolute',
top: 12,
left: 315,
left: 330,
width: 30,
height: 30
}}>
<Icon.Timer shrink={1.1} />
{highlight === 'badges' ? (
<Icon.Timer shrink={1.1} />
) : (
<div
style={{
background: '#00d299',
height: 15,
width: 15,
marginTop: 5,
borderRadius: '100%'
}}
/>
)}
</div>
<div style={{
position: 'absolute',
@ -293,8 +333,8 @@ const Section = styled('div')({
display: 'flex',
alignItems: 'center',
flexDirection: 'row',
margin: '40px auto',
padding: '80px 0'
margin: '28px auto 0',
padding: '60px 0'
}, ({alt}) => alt && ({
background: '#24292e',
'p': {
@ -303,6 +343,9 @@ const Section = styled('div')({
'h2': {
color: '#fff'
},
'@media (max-width: 1000px)': {
flexDirection: 'column'
}
}));
const Item = styled('div')({
@ -311,9 +354,10 @@ const Item = styled('div')({
padding: '24px 72px',
'h2': {
marginTop: 0,
fontSize: 36,
marginLeft: 15,
fontSize: 42,
textAlign: 'left',
fontWeight: 700
fontWeight: 600
},
'p': {
fontSize: 18
@ -339,6 +383,7 @@ const ImagePlaceholder = styled('div')({
position: 'relative',
display: 'block',
height: 400,
width: 600,
background: '#fff',
borderRadius: 8,
boxShadow: '0 2px 8px rgba(179, 179, 179, 0.25)'
@ -348,7 +393,7 @@ const ImagePlaceholder = styled('div')({
const Header = styled('h1')({
color: '#fff',
padding: '0 20px',
margin: '0 auto 20px',
margin: '0 auto 48px',
letterSpacing: '-1.0px'
});
@ -364,7 +409,7 @@ const SubHeader = styled(Header)({
const LandingHeader = styled('div')({
position: 'relative',
width: '100%',
margin: '54px 20px',
margin: '54px 20px 78px',
maxWidth: 1000,
display: 'flex',
justifyContent: 'space-between',
@ -421,7 +466,7 @@ export default function Scene ({loggedIn, onLogout, ...props}) {
<div>
<div className="container-gradient" style={{
width: '100%',
height: 600,
minHeight: 600,
position: 'relative',
display: 'flex',
flexDirection: 'column',
@ -443,7 +488,7 @@ export default function Scene ({loggedIn, onLogout, ...props}) {
)}
</LandingHeader>
<LandingMessage>
<Header>Control your notifications</Header>
<Header>Control your GitHub notifications</Header>
<SubHeader>Prioritize the tasks that keep you and your team most productive</SubHeader>
<div className="button-container">
<RouterLink to={routes.LOGIN}>let's get started</RouterLink>
@ -465,50 +510,54 @@ export default function Scene ({loggedIn, onLogout, ...props}) {
</LandingMessage>
<Curve />
</div>
<Section>
<Section id="section">
<Item style={{flex: '0 0 2.5%', padding: 0}} />
<Item>
{createImagePlaceholder()}
{createImagePlaceholder('badges')}
</Item>
<Item>
<h2>Surface the most important tasks to tackle as they happen</h2>
<Item id="item-text">
<h2>Surface the things that matter the most.</h2>
<ItemText>
<Icon.Ring />
<p>The issues and pull requests that require your attention the most are called out for you.</p>
<p>The most important issues and pull requests that require your presence are called out and brought to your attention.</p>
</ItemText>
<ItemText>
<Icon.Ear />
<p>We listen for updates with your notifications and let you know as soon as things change.</p>
<p>We listen for updates with your notifications and let you know <i>why</i> and <i>when</i> things change.</p>
</ItemText>
<ItemText>
<Icon.Zap />
<p>Super charge your day by focusing on getting things done, rather than sifting through notifications.</p>
<p>Super charge your day by focusing on getting things done, rather than sifting through notifications or emails.</p>
</ItemText>
</Item>
<Item style={{flex: '0 0 2.5%', padding: 0}} />
</Section>
<Section alt={true} style={{paddingTop: 140}}>
<Section id="section" alt={true} style={{paddingTop: 140, overflowX: 'hidden'}}>
<Curve style={{
bottom: 'auto',
marginBottom: 0,
marginTop: -1,
top: 0,
transform: 'translateX(-50%) rotate(180deg)'
}} />
<Item style={{flex: '0 0 2.5%', padding: 0}} />
<Item>
<h2>Surface the most important tasks to tackle as they happen</h2>
<Item id="item-text">
<h2>Your privacy matters, so<br />we keep things offline.</h2>
<ItemText>
<Icon.PeopleWhite />
<p>The issues and pull requests that require your attention the most are called out for you.</p>
<Icon.CloudOffWhite />
<p>a</p>
</ItemText>
<ItemText>
<Icon.BoltWhite />
<p>We listen for updates with your notifications and let you know as soon as things change.</p>
<Icon.CloudOffWhite />
<p>a</p>
</ItemText>
<ItemText>
<Icon.BookmarkAltWhite />
<p>Super charge your day by focusing on getting things done, rather than sifting through notifications.</p>
<Icon.CloudOffWhite />
<p>a</p>
</ItemText>
</Item>
<Item>
{createImagePlaceholder()}
{createImagePlaceholder('reason')}
</Item>
<Item style={{flex: '0 0 2.5%', padding: 0}} />
</Section>

File diff suppressed because it is too large Load Diff

View File

@ -1,898 +0,0 @@
import React from 'react';
import moment from 'moment';
import {VictoryPie, VictoryChart} from "victory";
import {Link} from "@reach/router";
import styled from 'react-emotion';
import Icon from '../../components/Icon';
import Logo from '../../components/Logo';
import LoadingIcon from '../../components/LoadingIcon';
import {routes} from '../../constants';
import {Filters} from '../../constants/filters';
import {withOnEnter, withTooltip} from '../../enhance';
import {Status} from '../../constants/status';
import {Badges} from '../../constants/reasons';
import '../../styles/gradient.css';
/* eslint-disable jsx-a11y/anchor-is-valid */
function getRelativeTime (time) {
const currentTime = moment();
const targetTime = moment(time);
const diffMinutes = currentTime.diff(targetTime, 'minutes');
if (diffMinutes < 1)
return 'Just now';
if (diffMinutes < 5)
return 'Few minutes ago';
if (diffMinutes < 60)
return diffMinutes + ' minutes ago';
if (diffMinutes < 60 * 24)
return Math.floor(diffMinutes / 60) + ' hours ago';
const diffDays = currentTime.diff(targetTime, 'days');
if (diffDays === 1)
return 'Yesterday';
if (diffDays <= 7)
return 'Last ' + targetTime.format('dddd');
// @TODO implement longer diffs
return 'Long time ago';
}
const UnofficialReleaseTag = styled('span')({
color: 'white',
position: 'absolute',
left: '21px',
bottom: '0px',
fontSize: '9px',
background: '#f42839',
fontWeight: '800',
padding: '2px 4px',
borderRadius: '4px',
textTransform: 'uppercase',
});
const FixedContainer = styled('div')({
height: '80%',
maxWidth: 270,
display: 'block',
position: 'fixed'
});
const InlineBlockContainer = styled('div')({
'div': {
display: 'inline-block'
}
});
const NotificationsContainer = styled('div')({
position: 'relative',
background: '#fff',
margin: '0 auto',
padding: 0,
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'row',
overflowX: 'hidden',
boxSizing: 'border-box'
});
const NavigationContainer = styled('div')({
position: 'fixed',
top: 0,
boxSizing: 'border-box',
margin: '0 auto',
width: '100%',
height: 60,
color: 'hsla(0,0%,100%,.75)',
paddingBottom: '12px',
paddingTop: '12px',
zIndex: '100',
});
const GeneralOptionsContainer = styled(NavigationContainer)({
position: 'relative',
zIndex: '1',
height: 'initial',
minHeight: 60,
width: '95%',
margin: 0,
background: '#fff',
padding: '8px 16px',
paddingTop: 18,
flex: '0 0 50px',
'button': {
display: 'inline-flex',
margin: 0
}
});
const Sidebar = styled('div')({
flex: '0 0 300px',
padding: '32px 20px',
paddingRight: 0,
display: 'flex',
justifyContent: 'center',
});
const SidebarLink = styled('a')({}, ({active, color}) => ({
textAlign: 'left',
userSelect: 'none',
margin: '0 auto 5px',
position: 'relative',
cursor: 'pointer',
borderRadius: 4,
alignItems: 'center',
padding: '0 14px',
height: 40,
width: 200,
fontSize: '12px',
fontWeight: 600,
letterSpacing: 0.5,
textTransform: 'capitalize',
textDecoration: 'none',
transition: 'background 0.12s ease-in-out',
display: 'flex',
background: active ? color : 'none',
color: active ? '#fff' : '#202124',
':before': {
content: '""',
transition: 'all 150ms ease',
background: 'rgba(190, 197, 208, 0.25)',
borderRadius: 4,
display: 'block',
top: 0,
bottom: 0,
right: 0,
left: 0,
position: 'absolute',
transform: 'scale(0)'
},
':hover:before': {
transform: active ? 'scale(0)' : 'scale(1)',
},
':active:before': {
background: 'rgba(190, 197, 208, 0.5)'
},
'div': {
marginRight: 5
}
}));
const Notifications = styled('div')({
flex: 1,
});
const NavTab = styled('a')({
position: 'relative',
textTransform: 'capitalize',
userSelect: 'none',
borderRadius: 4,
textDecoration: 'none',
fontWeight: '500',
fontSize: '14px',
textAlign: 'left',
opacity: 0.6,
padding: '20px 32px',
paddingLeft: '16px',
width: '150px',
display: 'inline-block',
margin: 0,
transition: 'all 150ms ease',
':hover': {
background: 'rgba(190, 197, 208, 0.25)',
},
}, ({ number }) => ({
':after': number > 0 && {
content: `"${number}"`,
color: '#ffffff',
background: '#a8a8a9',
fontSize: '10px',
verticalAlign: 'text-top',
padding: '1px 8px',
borderRadius: '4px',
marginLeft: '6px',
display: 'inline-block',
}
}), ({ active, color, number }) => active && ({
color,
opacity: 1,
':before': {
content: '""',
position: 'absolute',
background: color,
height: '3px',
width: '90%',
bottom: '0',
left: '5%',
borderTopLeftRadius: '4px',
borderTopRightRadius: '4px',
},
':after': number > 0 && {
content: `"${number}"`,
color: '#ffffff',
background: color,
fontSize: '10px',
verticalAlign: 'text-top',
padding: '1px 8px',
borderRadius: '4px',
marginLeft: '6px',
display: 'inline-block',
}
}));
const Tab = styled('button')({
position: 'relative',
userSelect: 'none',
cursor: 'pointer',
border: 0,
outline: 'none',
background: 'none',
height: 40,
width: 40,
borderRadius: '100%',
margin: '0 auto',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
':before': {
content: "''",
transition: 'all 150ms ease',
background: 'rgba(190, 197, 208, 0.25)',
borderRadius: '100%',
display: 'block',
height: 40,
width: 40,
position: 'absolute',
transform: 'scale(0)'
},
':hover:before': {
transform: 'scale(1)',
},
':active:before': {
background: 'rgba(190, 197, 208, 0.5)'
}
}, ({disabled}) => disabled && ({
background: 'none !important',
opacity: 0.35,
cursor: 'default',
':hover:before': {
transform: 'scale(0) !important',
},
':active:before': {
background: 'none !important'
}
}));
const SearchField = styled('div')({
float: 'left',
textAlign: 'left',
width: '50%',
boxShadow: '0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08)',
margin: '0 auto',
background: 'hsla(0,0%,100%,.125)',
borderRadius: '4px',
alignItems: 'center',
padding: 0,
height: '36px',
fontSize: '13px',
textDecoration: 'none',
transition: 'all 0.06s ease-in-out',
display: 'inline-flex',
':focus-within': {
background: '#fff'
}
});
const Message = styled('div')({
display: 'block',
textAlign: 'center',
marginTop: 24 * 5,
'p': {
paddingTop: 24,
userSelect: 'none',
display: 'block',
margin: 0
}
});
const LoaderContainer = styled('div')({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%'
});
const SearchInput = styled('input')({
flex: 1,
textAlign: 'left',
margin: '0 auto',
background: 'none',
padding: 0,
height: '36px',
color: '#fff',
fontSize: '13px',
textDecoration: 'none',
display: 'inline-flex',
border: '0',
outline: 'none',
':focus': {
color: '#202124'
}
});
const EnhancedSearchInput = withOnEnter(SearchInput);
const NotificationRow = styled('tr')({
position: 'relative',
display: 'flex',
alignItems: 'center',
textAlign: 'left',
width: '100%',
borderRadius: 4,
margin: '0 auto',
background: '#fff',
padding: '8px 16px',
transition: 'all 0.1s ease-in-out',
boxSizing: 'border-box',
':hover': {
background: '#f9f9f9',
// boxShadow: '0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08)',
zIndex: 10
}
});
const NotificationTab = styled(Tab)({
display: 'inline-flex',
margin: 0,
});
const Timestamp = styled('span')({
position: 'relative',
margin: 0,
marginLeft: 10,
fontSize: 11,
opacity: 0.5,
});
const NotificationTitle = styled('span')({
position: 'relative',
display: 'block'
}, ({img}) => img && ({
paddingLeft: 20,
'::before': {
content: '""',
position: 'absolute',
display: 'block',
background: `url(${img}) center center no-repeat`,
backgroundSize: 'cover',
left: 0,
height: 20,
width: 20,
}
}));
const Repository = styled('span')({
fontWeight: 500,
marginLeft: 10,
fontSize: 14
});
const PRIssue = styled(Repository)({
fontWeight: 400,
}, ({after}) => ({
':after': {
content: `"#${after}"`,
fontSize: 13,
opacity: .3,
marginLeft: 5
}
}));
const Table = styled('table')({
width: '96%',
minWidth: 970,
'td': {
display: 'inline-block'
}
});
const TableItem = styled('td')({
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}, ({width, flex}) => ({
width,
flex
}));
const SmallLink = styled('a')({
display: 'block',
marginRight: 10,
cursor: 'pointer',
fontSize: 10,
lineHeight: '20px',
fontWeight: 400,
textDecoration: 'underline',
transition: 'all 0.12s ease-in-out',
':hover': {
opacity: 0.75
}
});
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) {
case 'PullRequest':
return (
<Icon.PrMerged shrink={grow} />
);
case 'Issue':
return (
<Icon.IssueOpen shrink={grow} />
);
default:
return null;
}
}
export default function Scene ({
queuedCount,
stagedCount,
closedCount,
first,
last,
lastPage,
page,
notifications,
query,
activeStatus,
allNotificationsCount,
stagedTodayCount,
onChangePage,
onSetActiveStatus,
onClearQuery,
onLogout,
onSearch,
onMarkAsRead,
onFetchNotifications,
onRefreshNotifications,
onStageThread,
onRestoreThread,
isSearching,
isFetchingNotifications,
onClearCache,
fetchingNotificationsError,
activeFilter,
onSetActiveFilter,
}) {
const loading = isSearching || isFetchingNotifications;
const isFirstPage = page === 1;
const isLastPage = page === lastPage;
console.warn('before render in scene', notifications)
return (
<div style={{marginTop: 60}}>
<NavigationContainer className="container-gradient">
<div style={{
position: 'relative',
textAlign: 'right',
margin: '0 auto',
width: '92%'
}}>
<Logo
size={36}
style={{
float: 'left',
marginRight: 48,
cursor: 'pointer'
}}
onClick={() => {
onSetActiveStatus(Status.QUEUED);
onSetActiveFilter(Filters.PARTICIPATING);
}}
/>
<UnofficialReleaseTag>beta</UnofficialReleaseTag>
<SearchField>
<Icon.Search size={48} opacity={.45} />
<EnhancedSearchInput
disabled={loading}
type="text"
placeholder="Search for notifications"
onEnter={onSearch}
/>
{isSearching && <LoadingIcon white={true} size={48} />}
</SearchField>
<div style={{display: 'inline-block'}} className="button-container-alt">
<Link style={{
marginRight: 15,
background: 'none',
color: '#fff',
height: 36,
padding: '0 12px'
}} to={routes.HOME}>home</Link>
</div>
<div style={{display: 'inline-block'}} className="button-container-alt">
<a style={{
marginRight: 15,
background: 'none',
color: '#fff',
height: 36,
padding: '0 12px'
}} href="#" onClick={onLogout}>sign out</a>
</div>
</div>
</NavigationContainer>
<div style={{
display: 'flex',
flexDirection: 'row'
}}>
<div style={{
flex: '0 0 300px'
}}>
<Sidebar>
<FixedContainer>
<div style={{
width: 220,
padding: '0 14px',
margin: '0 11px 12px',
}}>
<h3 style={{
margin: 0
}}>
<Icon.Clock style={{
display: 'inline-block',
verticalAlign: 'middle',
marginRight: '5px',
top: '-3px',
}} />
{moment().format('h:mma')}
</h3>
<span style={{
display: 'block',
padding: '6px 0px',
fontSize: 15,
opacity: 0.7,
}}>{moment().format('dddd, MMMM Do')}</span>
<span style={{
display: 'block',
padding: '6px 0 8px',
fontSize: 12,
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"
onClick={() => onSetActiveFilter(Filters.ALL)}>
{activeFilter === Filters.ALL ? (
<Icon.InboxWhite shrink={.6} />
) : (
<Icon.Inbox shrink={.6} />
)}
all notifications
</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.BoltWhite shrink={.6} />
) : (
<Icon.Bolt shrink={.6} />
)}
your updates
</EnhancedSidebarLink>
<EnhancedSidebarLink
tooltip="Updates for issues and pull requests that you have commented on"
tooltipOffsetX={100}
active={activeFilter === Filters.COMMENT}
color="#00A0F5"
onClick={() => onSetActiveFilter(Filters.COMMENT)}>
{activeFilter === Filters.COMMENT ? (
<Icon.PeopleAltWhite shrink={.6} />
) : (
<Icon.PeopleAlt shrink={.6} />
)}
participating
</EnhancedSidebarLink>
<div style={{
padding: 14,
margin: 21,
background: '#f5f5f5',
borderRadius: 4,
height: 100,
fontSize: 12
}}>
bar chart, statistics, etc. will be here eventually
</div>
<div style={{
padding: 14,
margin: 21,
}}>
<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>
</div>
<div style={{
flex: 1
}}>
<GeneralOptionsContainer>
<EnhancedTab tooltip={!loading ? "Refresh your notifications" : null} disabled={loading}>
<Icon.Refresh
opacity={0.9}
onClick={!loading ? (() => onFetchNotifications()) : undefined}
/>
</EnhancedTab>
<EnhancedTab tooltip={!loading ? "Delete all of your notifications from the cache" : null} disabled={loading}>
<Icon.Trash
opacity={0.9}
onClick={!loading ? (() => {
const response = window.confirm('Are you sure you want to clear the cache?');
if (response) {
onClearCache();
}
}) : undefined}
/>
</EnhancedTab>
{query ? (
<React.Fragment>
<div style={{display: 'inline-block'}} className="button-container-alt">
<a style={{
background: 'none',
color: '#202124',
textTransform: 'inherit',
boxShadow: '0 0 0',
fontWeight: 400,
height: 36,
padding: '0 12px',
}}
>
Showing results for '{query}'
</a>
</div>
<EnhancedTab disabled={loading}>
<Icon.X
opacity={0.9}
onClick={!loading ? (() => onClearQuery()) : undefined}
/>
</EnhancedTab>
</React.Fragment>
) : null}
<div style={{float: 'right'}}>
<div style={{display: 'inline-block'}} className="button-container-alt">
<a style={{
marginRight: 15,
background: 'none',
color: '#202124',
textTransform: 'inherit',
boxShadow: '0 0 0',
fontWeight: 400,
height: 36,
padding: '0 12px',
}}>
{first}-{last} of about {allNotificationsCount}
</a>
</div>
<EnhancedTab disabled={loading || isFirstPage}>
<Icon.Prev
opacity={0.9}
onClick={!loading && !isFirstPage ? (() => onChangePage(page - 1)) : undefined}
/>
</EnhancedTab>
<EnhancedTab disabled={loading || isLastPage}>
<Icon.Next
opacity={0.9}
onClick={!loading && !isLastPage ? (() => onChangePage(page + 1)) : undefined}
/>
</EnhancedTab>
</div>
</GeneralOptionsContainer>
<GeneralOptionsContainer style={{paddingTop: 4}}>
<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);">
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);">
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);">
Resolved
</EnhancedNavTab>
</GeneralOptionsContainer>
<NotificationsContainer>
<Notifications>
{isFetchingNotifications ? (
<LoaderContainer>
<LoadingIcon />
</LoaderContainer>
) : notifications.length <= 0 ? (
<Message>
<p style={{
fontSize: 16,
fontWeight: 400,
}}>
No {activeStatus.toLowerCase()} notifications</p>
<p style={{
fontSize: 12,
fontWeight: 400,
color: '#5f6368'
}}>
<span role="img" aria-label="hooray">🎉</span> You're all set here for the moment</p>
</Message>
) : (
<Table>
<tbody style={{
display: 'flex',
flexDirection: 'column'
}}>
{notifications.map(n => (
<NotificationRow key={n.id}>
<TableItem>
<div style={{ float: 'left', marginTop: 2 }}>
{getPRIssueIcon(n.type, n.reasons)}
</div>
</TableItem>
<TableItem
style={{height: 36, cursor: 'pointer', userSelect: 'none'}}
width={400}
flex={.65}
onClick={() => {
window.open(n.url);
onStageThread(n.id, n.repository)
}}>
<NotificationTitle>
<PRIssue after={n.number}>{n.name}</PRIssue>
</NotificationTitle>
<Timestamp>
{getRelativeTime(n.updated_at)}
{n.isAuthor && (
<Icon.User
shrink={0.5}
style={{
display: 'inline-block',
top: -3
}}
/>
)}
</Timestamp>
</TableItem>
<TableItem width={100}>
<InlineBlockContainer>
{n.badges.map(badge => {
switch (badge) {
case Badges.HOT:
// lots of `reasons` within short time frame
return (
<EnhancedIconHot
tooltip="Lots of recent activity"
tooltipOffsetX={-15}
tooltipOffsetY={-10}
shrink={0.75}
/>
);
case Badges.OLD:
// old
return (
<EnhancedIconTimer
tooltip="Old pull request that needs your review"
tooltipOffsetX={-15}
tooltipOffsetY={-10}
shrink={0.75}
/>
);
case Badges.COMMENTS:
// lots of `reasons`
return (
<EnhancedIconConvo
tooltip="Very talkative thread"
tooltipOffsetX={-15}
tooltipOffsetY={-10}
shrink={0.75}
/>
);
default:
return null;
}
})}
</InlineBlockContainer>
</TableItem>
<TableItem width={250} flex={.35}>
<Repository
onClick={() => window.open(n.repositoryUrl)}
style={{cursor: 'pointer', userSelect: 'none'}}>
{n.repository}</Repository>
</TableItem>
<TableItem width={150} style={{textAlign: 'right'}}>
<EnhancedNotificationTab
tooltip={!loading ? "Score representing this notification's importance" : null}
tooltipOffsetX={-20}
>
{n.score}
</EnhancedNotificationTab>
{activeStatus === Status.QUEUED ? (
<EnhancedNotificationTab
tooltip={!loading ? "Mark as read" : null}
tooltipOffsetX={-10}
>
<Icon.Check
opacity={0.9}
onClick={!loading ? (() => onStageThread(n.id, n.repository)) : undefined}
/>
</EnhancedNotificationTab>
) : (
<EnhancedNotificationTab
tooltip={!loading ? "Revert back to unread" : null}
tooltipOffsetX={-10}
>
<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>
))}
</tbody>
</Table>
)}
</Notifications>
</NotificationsContainer>
</div>
</div>
</div>
);
}

View File

@ -11,7 +11,7 @@ import { routes } from '../../constants';
import { Filters } from '../../constants/filters';
import { Status } from '../../constants/status';
import { Reasons, Badges } from '../../constants/reasons';
import Scene from './SceneAlt';
import Scene from './Scene';
const PER_PAGE = 15;
@ -53,7 +53,6 @@ function scoreOf (notification) {
let prevReason = null;
for (let i = 0; i < reasons.length; i++) {
const reason = reasons[i].reason;
console.log(reason)
if (prevReason && reason === prevReason) {
const degradedScore = Math.ceil(scoreOfReason[reason] / 3);
score += Math.max(degradedScore, 2);
@ -122,8 +121,9 @@ class NotificationsPage extends React.Component {
}
componentDidMount () {
this.props.notificationsApi.fetchNotifications();
this.syncer = setInterval(() => {
console.warn('sync');
this.props.notificationsApi.fetchNotificationsSync();
}, 8 * 1000);
}
@ -172,14 +172,12 @@ class NotificationsPage extends React.Component {
}
enhancedOnStageThread = (thread_id, repository) => {
console.warn('staging thread', thread_id, 'in repo', repository);
this.props.storageApi.incrStat('stagedCount');
this.props.storageApi.incrStat(repository + '-stagedCount');
this.props.notificationsApi.stageThread(thread_id);
}
restoreThread = thread_id => {
console.warn('restoring thread');
this.props.notificationsApi.restoreThread(thread_id);
}
@ -191,6 +189,7 @@ class NotificationsPage extends React.Component {
const {
fetchNotifications,
markAsRead,
markAllAsStaged,
clearCache,
notifications,
loading: isFetchingNotifications,
@ -210,6 +209,16 @@ class NotificationsPage extends React.Component {
))
);
break;
case Filters.ASSIGNED:
filterMethod = n => (
n.reasons.some(({ reason }) => reason === 'assign')
);
break;
case Filters.REVIEW_REQUESTED:
filterMethod = n => (
n.reasons.some(({ reason }) => reason === 'review_requested')
);
break;
case Filters.COMMENT:
filterMethod = n => (
n.reasons.some(({ reason }) => reason === 'comment')
@ -290,6 +299,7 @@ class NotificationsPage extends React.Component {
onClearQuery={this.onClearQuery}
onFetchNotifications={fetchNotifications}
onMarkAsRead={markAsRead}
onMarkAllAsStaged={markAllAsStaged}
onClearCache={clearCache}
onStageThread={this.enhancedOnStageThread}
onRestoreThread={this.restoreThread}

View File

@ -1,6 +1,6 @@
import React from 'react';
import {AuthConsumer} from './Auth';
import {StorageProvider} from './Storage';
import {StorageProvider, LOCAL_STORAGE_PREFIX} from './Storage';
import {Status} from '../constants/status';
const BASE_GITHUB_API_URL = 'https://api.github.com';
@ -121,7 +121,7 @@ class NotificationsProvider extends React.Component {
fetchNotifications = (page = 1, optimizePolling = true) => {
if (!this.props.token) {
console.error('Unauthenitcated, aborting request.')
console.error('Unauthenitcated, aborting request.');
return false;
}
@ -133,7 +133,7 @@ class NotificationsProvider extends React.Component {
processNotificationsChunk = (nextPage, notificationsChunk) => {
return new Promise((resolve, reject) => {
console.log('chunk', notificationsChunk)
console.log('chunk', notificationsChunk);
let everythingUpdated = true;
if (notificationsChunk.length === 0) {
@ -187,7 +187,6 @@ class NotificationsProvider extends React.Component {
: Promise.reject();
})
.then(() => {
console.warn('removing', thread_id);
this.props.removeItemFromStorage(thread_id);
this.props.refreshNotifications();
return Promise.resolve();
@ -208,7 +207,6 @@ class NotificationsProvider extends React.Component {
requestClearCache = () => {
return new Promise((resolve, reject) => {
console.warn('clearing cache');
this.props.clearStorageCache();
this.props.refreshNotifications();
this.last_modified = null;
@ -225,7 +223,6 @@ class NotificationsProvider extends React.Component {
requestStageThread = thread_id => {
return new Promise((resolve, reject) => {
console.warn('staging thread', thread_id);
const cached_n = this.props.getItemFromStorage(thread_id);
if (cached_n) {
const newValue = {
@ -241,9 +238,28 @@ class NotificationsProvider extends React.Component {
});
}
requestStageAll = () => {
return new Promise((resolve, reject) => {
Object.keys(localStorage).forEach(nKey => {
// Only update the notification items in the cache.
// Don't get the statistics or anything else caught in there.
if (nKey.includes(LOCAL_STORAGE_PREFIX)) {
let cached_n = null;
cached_n = JSON.parse(window.localStorage.getItem(nKey));
const newValue = {
...cached_n,
status: Status.STAGED
};
window.localStorage.setItem(nKey, JSON.stringify(newValue));
}
});
this.props.refreshNotifications();
return resolve();
});
}
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 = {
@ -259,6 +275,13 @@ class NotificationsProvider extends React.Component {
});
}
markAllAsStaged = () => {
this.setState({ loading: true });
return this.requestStageAll()
.catch(error => this.setState({ error }))
.finally(() => this.setState({ loading: false }));
}
stageThread = thread_id => {
this.setState({ loading: true });
return this.requestStageThread(thread_id)
@ -282,7 +305,6 @@ class NotificationsProvider extends React.Component {
if (prevReason) {
reasons = prevReason.concat(newReason);
console.warn('MULTIPLE REASONS', reasons)
} else {
reasons = [newReason];
}
@ -311,6 +333,7 @@ class NotificationsProvider extends React.Component {
fetchNotifications: this.fetchNotifications,
fetchNotificationsSync: this.requestFetchNotifications,
markAsRead: this.markAsRead,
markAllAsStaged: this.markAllAsStaged,
clearCache: this.clearCache,
stageThread: this.stageThread,
restoreThread: this.restoreThread,

View File

@ -3,8 +3,8 @@ import moment from 'moment';
import {Status} from '../constants/status';
import {Reasons} from '../constants/reasons';
const LOCAL_STORAGE_PREFIX = '__meteorite_noti_cache__';
const LOCAL_STORAGE_STATISTIC_PREFIX = '__meteorite_statistic_cache__';
export const LOCAL_STORAGE_PREFIX = '__meteorite_noti_cache__';
export const LOCAL_STORAGE_STATISTIC_PREFIX = '__meteorite_statistic_cache__';
const getMockReasons = n => {
const reasons = Object.values(Reasons);

View File

@ -112,3 +112,12 @@
50% {background-position: 60% 50%}
100% {background-position: 0% 50%}
}
@media only screen and (max-width: 1400px) {
#section {
flex-direction: column !important;
}
#item-text {
width: 600px;
}
}