Skip to content

Commit

Permalink
feat(filter): Filter notifications based on owner/repository (#192)
Browse files Browse the repository at this point in the history
Co-authored-by: Laxman <notlmn@outlook.com>
  • Loading branch information
jroehl and notlmn committed Mar 29, 2020
1 parent 9f4c947 commit 1be1db3
Show file tree
Hide file tree
Showing 20 changed files with 602 additions and 49 deletions.
Binary file added media/screenshot-filter.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 8 additions & 3 deletions package.json
Expand Up @@ -4,11 +4,16 @@
"lint": "run-p lint:*",
"lint:js": "xo",
"lint:css": "stylelint source/**/*.css",
"lint-web-ext": "web-ext lint --source-dir ./distribution/",
"lint-fix": "run-p 'lint:* -- --fix'",
"test": "run-s lint:* test:* build",
"test": "run-s lint:* test:* build lint-web-ext",
"test:js": "ava",
"build": "webpack --mode=production",
"watch": "webpack --mode=development --watch",
"web-ext:firefox": "web-ext run --source-dir ./distribution/",
"web-ext:chrome": "web-ext run --target chromium --source-dir ./distribution/",
"dev:firefox": "npm-run-all -p watch web-ext:firefox",
"dev:chrome": "npm-run-all -p watch web-ext:chrome",
"prerelease:version": "VERSION=$(utc-version); echo $VERSION; dot-json distribution/manifest.json version $VERSION",
"prerelease:source-url": "if [ ! -z \"${TRAVIS_REPO_SLUG}\" ]; then echo https://github.com/$TRAVIS_REPO_SLUG/tree/\"${TRAVIS_TAG:-$TRAVIS_COMMIT}\" > distribution/SOURCE_URL; fi",
"release": "npm-run-all build prerelease:* release:*",
Expand All @@ -17,7 +22,7 @@
},
"dependencies": {
"delay": "^4.3.0",
"webext-options-sync": "^1.0.0-8",
"webext-options-sync": "^1.2.0",
"webextension-polyfill": "^0.4.0"
},
"devDependencies": {
Expand All @@ -37,7 +42,7 @@
"stylelint-config-xo": "^0.15.0",
"terser-webpack-plugin": "^1.4.1",
"utc-version": "^2.0.1",
"web-ext": "^3.1.1",
"web-ext": "^3.2.0",
"web-ext-submit": "^3.1.1",
"webpack": "^4.38.0",
"webpack-cli": "^3.3.6",
Expand Down
19 changes: 14 additions & 5 deletions readme.md
Expand Up @@ -19,6 +19,7 @@ Checks for new GitHub notifications every minute, shows the number of notificati

- [Notification count in the toolbar icon.](#notification-count)
- [Desktop notifications.](#desktop-notifications)
- [Filter notifications](#filtering-notifications) from repositories you wish to see.
- [GitHub Enterprise support.](#github-enterprise-support)
- Click the toolbar icon to go to the GitHub notifications page.
- Option to show only unread count for issues you're participating in.
Expand All @@ -28,28 +29,30 @@ Checks for new GitHub notifications every minute, shows the number of notificati

## Screenshots

### Notification count
### Notification Count

![Screenshot of extension should notification count](media/screenshot.png)

### Options

![Options page for Notifier for GitHub](media/screenshot-options.png)


## Permissions

The extension requests a couple of optional permissions. It works as intended even if you disallow these. Some features work only when you grant these permissions as mentioned below.

### Tabs permission
### Tabs Permission

When you click on the extension icon, the GitHub notifications page is opened in a new tab. The `tabs` permission lets us switch to an existing notifications tab if you already have one opened instead of opening a new one each time you click it.

This permission also lets us update the notification count immediately after opening a notification. You can find both of these options under the "Tab handling" section in the extension's options page.

### Notifications permission
### Notifications Permission

If you want to receive desktop notifications for public repositories, you can enable them on extension options page. You will then be asked for the `notifications` permission.

### Repos permission
### Repos Permission

If you want to receive (useful) desktop notifications for any private repositories you have, you will have to create a GitHub personal access token that has access to the `repo` scope as well. This is due to GitHub's current permission scheme, as the only way we can read anything about your private repos is if we have full control over repositories.

Expand All @@ -58,12 +61,18 @@ If you're concerned with your security in this manner, please feel free to ignor

## Configuration

### Desktop notifications
### Desktop Notifications

![Notification from Notifier for GitHub extension](media/screenshot-notification.png)

You can opt-in to receive desktop notifications for new notifications on GitHub. The extension checks for new notifications every minute, and displays notifications that arrived after the last check if there are any. Clicking on the notification opens it on GitHub.

### Filtering Notifications

![Filtering Notifications](media/screenshot-filter.png)

If you have [desktop notifications](#desktop-notifications) enabled as mentioned above, you can also filter which repositories you wish to receive these notifications from. You can do this by only selecting the repositories (that grouped by user/organization) in the options menu.

### GitHub Enterprise support

By default, the extension works for the public [GitHub](https://github.com) site. If you want the extension to show notifications from a GitHub Enterprise server, you have to configure the extension to use the API URL for your GitHub Enterprise server (like `https://github.yourco.com/`).
Expand Down
32 changes: 23 additions & 9 deletions source/lib/api.js
@@ -1,4 +1,5 @@
import optionsStorage from '../options-storage';
import {parseLinkHeader} from '../util';

export async function getHostname() {
const {rootUrl} = await optionsStorage.getAll();
Expand Down Expand Up @@ -81,9 +82,10 @@ export async function makeApiRequest(endpoint, params) {
}
}

export async function getNotificationResponse({maxItems = 100, lastModified = ''} = {}) {
export async function getNotificationResponse({page = 1, maxItems = 100, lastModified = ''}) {
const {onlyParticipating} = await optionsStorage.getAll();
const params = {
page,
per_page: maxItems // eslint-disable-line camelcase
};

Expand All @@ -98,9 +100,22 @@ export async function getNotificationResponse({maxItems = 100, lastModified = ''
return makeApiRequest('/notifications', params);
}

export async function getNotifications({maxItems, lastModified} = {}) {
const {json: notifications} = await getNotificationResponse({maxItems, lastModified});
return notifications || [];
export async function getNotifications({page, maxItems, lastModified, notifications = []}) {
const {headers, json} = await getNotificationResponse({page, maxItems, lastModified});
notifications = [...notifications, ...json];

const {next} = parseLinkHeader(headers.get('Link'));
if (!next) {
return notifications;
}

const {searchParams} = new URL(next);
return getNotifications({
page: searchParams.get('page'),
maxItems: searchParams.get('per_page'),
lastModified,
notifications
});
}

export async function getNotificationCount() {
Expand All @@ -118,13 +133,12 @@ export async function getNotificationCount() {
};
}

const lastlink = linkHeader.split(', ').find(link => {
return link.endsWith('rel="last"');
});
const {last} = parseLinkHeader(linkHeader);
const {searchParams} = new URL(last);

// We get notification count by asking the API to give us only one notificaion
// We get notification count by asking the API to give us only one notification
// for each page, then the last page number gives us the count
const count = Number(lastlink.slice(lastlink.lastIndexOf('page=') + 5, lastlink.lastIndexOf('>')));
const count = Number(searchParams.get('page'));

return {
count,
Expand Down
16 changes: 14 additions & 2 deletions source/lib/notifications-service.js
@@ -1,5 +1,7 @@
import delay from 'delay';
import optionsStorage from '../options-storage';
import repositoriesStorage from '../repositories-storage';
import {parseFullName} from '../util';
import {makeApiRequest, getNotifications, getTabUrl, getHostname} from './api';
import {getNotificationReasonText} from './defaults';
import {openTab} from './tabs-service';
Expand Down Expand Up @@ -122,8 +124,18 @@ export async function playNotificationSound() {
}

export async function checkNotifications(lastModified) {
const notifications = await getNotifications({lastModified});
const {showDesktopNotif, playNotifSound} = await optionsStorage.getAll();
let notifications = await getNotifications({lastModified});
const {showDesktopNotif, playNotifSound, filterNotifications} = await optionsStorage.getAll();

if (filterNotifications) {
const repositories = await repositoriesStorage.getAll();
/* eslint-disable camelcase */
notifications = notifications.filter(({repository: {full_name}}) => {
const {owner, repository} = parseFullName(full_name);
return Boolean(repositories[owner] && repositories[owner][repository]);
});
/* eslint-enable camelcase */
}

if (playNotifSound && notifications.length > 1) {
await playNotificationSound();
Expand Down
47 changes: 47 additions & 0 deletions source/lib/repositories-service.js
@@ -0,0 +1,47 @@
import {parseLinkHeader, parseFullName} from '../util';
import repositoriesStorage from '../repositories-storage';
import {makeApiRequest} from './api';

export async function getRepositories(
repos = [],
params = {
page: '1',
per_page: '100' // eslint-disable-line camelcase
}
) {
const {headers, json} = await makeApiRequest('/user/subscriptions', params);
repos = [...repos, ...json];

const {next} = parseLinkHeader(headers.get('Link'));
if (!next) {
return repos;
}

const {searchParams} = new URL(next);
return getRepositories(repos, {
page: searchParams.get('page'),
per_page: searchParams.get('per_page') // eslint-disable-line camelcase
});
}

export async function listRepositories(update) {
const stored = await repositoriesStorage.getAll();

let tree = stored;
if (update || !tree || Object.keys(tree).length <= 0) {
const fetched = await getRepositories();
/* eslint-disable camelcase */
tree = fetched.reduce((tree, {full_name}) => {
const {owner, repository} = parseFullName(full_name);
return Object.assign({}, tree, {
[owner]: Object.assign(tree[owner] || {}, {
[repository]: Boolean(stored && stored[owner] && stored[owner][repository])
})
});
}, {});
/* eslint-enable camelcase */
await repositoriesStorage.set(tree);
}

return tree;
}
13 changes: 13 additions & 0 deletions source/lib/user-service.js
@@ -0,0 +1,13 @@
import {makeApiRequest} from './api';
import localStore from './local-store';

export async function getUser(update) {
let user = await localStore.get('user');
if (update || !user) {
const {json} = await makeApiRequest('/user');
await localStore.set('user', json);
user = json;
}

return user;
}
3 changes: 1 addition & 2 deletions source/manifest.json
Expand Up @@ -33,7 +33,6 @@
"default_icon": "icon-toolbar.png"
},
"options_ui": {
"page": "options.html",
"chrome_style": true
"page": "options.html"
}
}
3 changes: 2 additions & 1 deletion source/options-storage.js
Expand Up @@ -8,7 +8,8 @@ const optionsStorage = new OptionsSync({
showDesktopNotif: false,
onlyParticipating: false,
reuseTabs: false,
updateCountOnNavigation: false
updateCountOnNavigation: false,
filterNotifications: false
},
migrations: [
OptionsSync.migrations.removeUnused
Expand Down
73 changes: 73 additions & 0 deletions source/options.css
Expand Up @@ -16,6 +16,8 @@ html.is-edgium {
body {
font-family: system-ui, sans-serif;
font-size: 14px;
margin: 0;
padding: 16px;
}

p {
Expand Down Expand Up @@ -50,10 +52,23 @@ label {
flex-wrap: wrap;
}

summary label {
display: inline;
}

label input[type='checkbox'] {
margin-right: 0.5em;
}

/* Workaround for missing indeterminate icon bug in chrome extension */
input[type='checkbox']:indeterminate::after {
content: '\2212';
font-size: 16px;
top: -5px;
font-weight: bold;
position: absolute;
}

input {
font-family: inherit;
}
Expand All @@ -80,3 +95,61 @@ input[type='text'][name='token'] {
font-size: 12px;
width: 35ch; /* Show only a part of the token */
}

.hidden {
display: none;
}

#error-message {
color: var(--github-red);
}

#repositories-form {
margin: 0.5em 0 0 1.75em;
}

.repo-wrapper,
.repo-wrapper ul {
margin: 0.25em 0 0.5em;
}

.repo-wrapper ul > li {
list-style-type: none;
}

.repo-wrapper summary {
display: flex;
align-items: center;
}

.repo-wrapper label {
margin: 0;
}

.loader {
display: none;
width: 0.8em;
height: 0.8em;
}

.loader::after {
content: ' ';
display: block;
width: 0.7em;
height: 0.7em;
margin: 0 0.1em;
border-radius: 50%;
border: 2px solid var(--github-red);
border-color: var(--github-red) transparent;
animation: spin 1.2s linear infinite;
}

.loading > .loader {
display: inline-block;
}

@keyframes spin {
to {
transform: rotate(360deg);
}
}
19 changes: 19 additions & 0 deletions source/options.html
Expand Up @@ -57,6 +57,25 @@ <h3>Tab Handling</h3>
Update notification count after opening a notification
</label>
</section>

<hr>

<section>
<h3>Filter Notifications</h3>
<label>
<input type="checkbox" name="filterNotifications">
Filter desktop notifications you receive by owner or repository
</label>
</section>
</form>

<form id="repositories-form">
<p class="small">
<span>Select the repositories you want to receive notifications for.</span>
<span id="reload-repositories" class="loading"> <a href="#null">Reload repositories</a> <span class="loader"></span></span>
</p>
<p id="error-message" class="small hidden"></p>
<div class="repo-wrapper"></div>
</form>

<script src="browser-polyfill.min.js"></script>
Expand Down

0 comments on commit 1be1db3

Please sign in to comment.