Many tickets and new readme
BIN
.github/icon.png
vendored
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
.github/template.png
vendored
Normal file
After Width: | Height: | Size: 57 KiB |
@ -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.
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
|
4
src/components/Icon/svg/bolt-alt.svg
Normal 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 |
1
src/components/Icon/svg/bubbles.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><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 |
1
src/components/Icon/svg/cloudoff.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path fill="#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 |
1
src/components/Icon/svg/eye-white.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path 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 |
1
src/components/Icon/svg/eye.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 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 |
1
src/components/Icon/svg/sync.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 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 |
1
src/components/Icon/svg/tag-white.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path 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 |
1
src/components/Icon/svg/tag.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="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 |
1
src/components/Icon/svg/warn.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>
|
After Width: | Height: | Size: 188 B |
@ -1,5 +1,6 @@
|
||||
export const Filters = {
|
||||
ALL: 'all',
|
||||
ASSIGNED: 'assign',
|
||||
REVIEW_REQUESTED: 'review_requested',
|
||||
PARTICIPATING: 'participating',
|
||||
SUBSCRIBED: 'subscribed',
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|