├── .editorconfig
├── .gitignore
├── .vscodeignore
├── changelog.md
├── license
├── package.json
├── readme.md
├── resources
├── logo-128x128.png
├── logo.afphoto
└── logo.png
├── src
├── commands.ts
├── context.ts
├── index.ts
├── secrets.ts
├── state.ts
├── statusbar.ts
├── types.ts
└── utils.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 |
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_size = 2
8 | indent_style = space
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.err
3 | *.log
4 | ._*
5 | .cache
6 | .fseventsd
7 | .DocumentRevisions*
8 | .DS_Store
9 | .TemporaryItems
10 | .Trashes
11 | Thumbs.db
12 |
13 | dist
14 | node_modules
15 | package-lock.json
16 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | resources/**/*.afdesign
2 | resources/**/*.afphoto
3 | resources/**/*.gif
4 | resources/**/*.mp4
5 | resources/logo.png
6 |
7 | src
8 | node_modules
9 |
--------------------------------------------------------------------------------
/changelog.md:
--------------------------------------------------------------------------------
1 | ### Version 3.0.2
2 | - Delaying activation until the editor has started up, for slightly faster startup
3 |
4 | ### Version 3.0.1
5 | - Minor internal improvements
6 |
7 | ### Version 3.0.0
8 | - Rewitten: more modern code, no third-party dependencies, 99.5% smaller bundle
9 | - New command: "githubNotificationsBell.setToken", replacing the less secure setting for setting the personal access token
10 | - New setting: "githubNotificationsBell.protocol", for setting a custom protocol to use when quering GitHub
11 | - Added support for pagination, useful for people with more than 50 notifications
12 |
13 | ### Version 2.3.1
14 | - Fixed a regression when querying GitHub's API
15 |
16 | ### Version 2.3.0
17 | - Added support for using a custom GitHub domain
18 |
19 | ### Version 2.2.0
20 | - Added support for the "GITHUB_NOTIFICATIONS_TOKEN" environment variable
21 |
22 | ### Version 2.1.5
23 | - Improved `alignment` setting definition
24 |
25 | ### Version 2.1.4
26 | - Replaced `open` with `vscode.open`
27 |
28 | ### Version 2.1.3
29 | - Readme: using hi-res logo
30 |
31 | ### Version 2.1.2
32 | - Outputting modern code (es2017, faster)
33 | - Using "Debug Launcher" for debugging
34 |
35 | ### Version 2.1.1
36 | - Using the `mark-github` icon by default
37 | - Updated readme
38 |
39 | ### Version 2.1.0
40 | - Refresh when focused after opening in browser
41 |
42 | ### Version 2.0.0
43 | - Removed special support for "participating" notifications
44 | - Added a counter
45 |
46 | ### Version 1.2.1
47 | - Bundling with webpack
48 |
49 | ### Version 1.2.0
50 | - Removed some unused code
51 | - Using the global state in order to reduce API usage
52 |
53 | ### Version 1.1.1
54 | - Updated readme
55 |
56 | ### Version 1.1.0
57 | - Increased default refresh interval to 300s
58 | - Added support for changing the icon
59 |
60 | ### Version 1.0.5
61 | - Updated readme
62 |
63 | ### Version 1.0.3
64 | - Renamed a command
65 |
66 | ### Version 1.0.2
67 | - Showing an information message when explicitly refreshing it
68 |
69 | ### Version 1.0.1
70 | - Updated readme
71 |
72 | ### Version 1.0.0
73 | - Initial release
74 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017-present Fabio Spampinato
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a
6 | copy of this software and associated documentation files (the "Software"),
7 | to deal in the Software without restriction, including without limitation
8 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
9 | and/or sell copies of the Software, and to permit persons to whom the
10 | Software is furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21 | DEALINGS IN THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vscode-github-notifications-bell",
3 | "displayName": "GitHub Notifications",
4 | "publisher": "fabiospampinato",
5 | "repository": "github:fabiospampinato/vscode-github-notifications-bell",
6 | "description": "A secure, customizable, statusbar icon that notifies you about notifications on GitHub.",
7 | "icon": "resources/logo-128x128.png",
8 | "version": "3.0.2",
9 | "main": "dist/index.js",
10 | "activationEvents": [
11 | "onStartupFinished"
12 | ],
13 | "contributes": {
14 | "configuration": {
15 | "type": "object",
16 | "title": "GitHub Notifications Bell - Configuration",
17 | "properties": {
18 | "githubNotificationsBell.refreshInterval": {
19 | "type": "number",
20 | "description": "Amount of seconds to wait before each refresh",
21 | "default": 300
22 | },
23 | "githubNotificationsBell.alignment": {
24 | "type": "string",
25 | "description": "Bell's position in the statusbar (left/right)",
26 | "default": "right",
27 | "enum": [
28 | "left",
29 | "right"
30 | ]
31 | },
32 | "githubNotificationsBell.icon": {
33 | "type": "string",
34 | "description": "The icon to use in the statusbar",
35 | "default": "mark-github"
36 | },
37 | "githubNotificationsBell.color": {
38 | "type": "string",
39 | "description": "Bell's color when there are some notifications",
40 | "default": ""
41 | },
42 | "githubNotificationsBell.hideIfNone": {
43 | "type": "boolean",
44 | "description": "Hide the bell if there are no notifications",
45 | "default": true
46 | },
47 | "githubNotificationsBell.showNumberOfNotifications": {
48 | "type": "boolean",
49 | "description": "Show the number of notifications alongside the bell icon",
50 | "default": true
51 | },
52 | "githubNotificationsBell.protocol": {
53 | "type": "string",
54 | "description": "The protocol to use when quering GitHub",
55 | "default": "https"
56 | },
57 | "githubNotificationsBell.domain": {
58 | "type": "string",
59 | "description": "The Github domain to query against. Github Enterprise may use a different domain",
60 | "default": "github.com"
61 | }
62 | }
63 | },
64 | "commands": [
65 | {
66 | "command": "githubNotificationsBell.openInBrowser",
67 | "title": "GitHub Notifications: Open in Browser"
68 | },
69 | {
70 | "command": "githubNotificationsBell.refresh",
71 | "title": "GitHub Notifications: Refresh"
72 | },
73 | {
74 | "command": "githubNotificationsBell.setToken",
75 | "title": "GitHub Notifications: Set Personal Access Token"
76 | }
77 | ]
78 | },
79 | "scripts": {
80 | "bundle:dev": "tsex bundle --external vscode --format cjs --platform node --no-declare",
81 | "bundle:prod": "tsex bundle --external vscode --format cjs --platform node --minify",
82 | "clean": "tsex clean",
83 | "compile": "tsex compile",
84 | "debug": "code --extensionDevelopmentPath $PWD --inspect-extensions 9222",
85 | "package": "vsce package",
86 | "prepublishOnly": "scex -bs clean bundle:prod",
87 | "vscode:prepublish": "scex -bs clean bundle:prod",
88 | "dev": "scex -bs bundle:dev debug",
89 | "prod": "scex -bs bundle:prod debug"
90 | },
91 | "categories": [
92 | "Other"
93 | ],
94 | "engines": {
95 | "vscode": "^1.87.0"
96 | },
97 | "keywords": [
98 | "vscode",
99 | "vsc",
100 | "extension",
101 | "github",
102 | "notifications"
103 | ],
104 | "dependencies": {
105 | "vscode-extras": "^1.6.1"
106 | },
107 | "devDependencies": {
108 | "@types/vscode": "^1.87.0",
109 | "esbuild": "0.20.1",
110 | "scex": "^1.1.0",
111 | "tsex": "^3.2.0",
112 | "typescript": "^5.4.2"
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # GitHub Notifications
2 |
3 |
4 |
5 |
6 |
7 | A secure, customizable, statusbar icon that notifies you about notifications on GitHub.
8 |
9 | You can customize it to your likings, choosing when to show it and which icon/color/label to use.
10 |
11 | ## Install
12 |
13 | Follow the instructions in the [Marketplace](https://marketplace.visualstudio.com/items?itemName=fabiospampinato.vscode-github-notifications-bell), or run the following in the command palette:
14 |
15 | ```shell
16 | ext install fabiospampinato.vscode-github-notifications-bell
17 | ```
18 |
19 | ## Usage
20 |
21 | It adds 3 commands to the command palette:
22 |
23 | ```js
24 | 'GitHub Notifications: Open in Browser' // Open the notifications page in the browser
25 | 'GitHub Notifications: Refresh' // Refresh the notifications
26 | 'GitHub Notifications: Set Personal Access Token' // Set the personal access token
27 | ```
28 |
29 | ## Secrets
30 |
31 | This extension needs a GitHub Personal Access Token, to create it go [here](https://github.com/settings/tokens), click "Generate new token" and be sure to select the "notifications" scope, then click "Generate token".
32 |
33 | To tell the extension about your token you should run the `GitHub Notifications: Set Personal Access Token` from the command palette.
34 |
35 | ## Settings
36 |
37 | ```js
38 | {
39 | "githubNotificationsBell.refreshInterval": 300, // Amount of seconds to wait before each refresh
40 | "githubNotificationsBell.alignment": "right", // Bell's position in the statusbar (left/right)
41 | "githubNotificationsBell.icon": "mark-github", // The icon to use in the statusbar
42 | "githubNotificationsBell.color": "", // Bell's color when there are some notifications
43 | "githubNotificationsBell.hideIfNone": true, // Hide the bell if there are no notifications
44 | "githubNotificationsBell.showNumberOfNotifications": true // Show the number of notifications alongside the bell icon
45 | "githubNotificationsBell.protocol": "https" // The protocol to use when quering GitHub
46 | "githubNotificationsBell.domain": "github.com" // The Github domain to query against. Github Enterprise may use a different domain
47 | }
48 | ```
49 |
50 | ## Hints
51 |
52 | - **Icon**: [here](https://code.visualstudio.com/api/references/icons-in-labels#icon-listing) you can browse a list of supported icons.
53 |
54 | ## License
55 |
56 | MIT © Fabio Spampinato
57 |
--------------------------------------------------------------------------------
/resources/logo-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fabiospampinato/vscode-github-notifications-bell/d08757063658582dff89176b9cedca238ba31489/resources/logo-128x128.png
--------------------------------------------------------------------------------
/resources/logo.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fabiospampinato/vscode-github-notifications-bell/d08757063658582dff89176b9cedca238ba31489/resources/logo.afphoto
--------------------------------------------------------------------------------
/resources/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fabiospampinato/vscode-github-notifications-bell/d08757063658582dff89176b9cedca238ba31489/resources/logo.png
--------------------------------------------------------------------------------
/src/commands.ts:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | import {alert, openInExternal} from 'vscode-extras';
5 | import Context from './context';
6 | import Secrets from './secrets';
7 | import State from './state';
8 | import Statusbar from './statusbar';
9 | import {getOptions} from './utils';
10 |
11 | /* MAIN */
12 |
13 | const openInBrowser = (): void => {
14 |
15 | const options = getOptions ();
16 | const url = `${options.protocol}://${options.domain}/notifications`;
17 |
18 | openInExternal ( url );
19 |
20 | };
21 |
22 | const refresh = async ( showNotification: boolean = true ): Promise => {
23 |
24 | await update ( true );
25 |
26 | if ( showNotification ) {
27 |
28 | alert.info ( `GitHub Notifications refreshed. ${State.getCounter ()} Notifications.` );
29 |
30 | }
31 |
32 | };
33 |
34 | const setToken = async (): Promise => {
35 |
36 | await Secrets.updateToken ();
37 |
38 | };
39 |
40 | const update = async ( force?: boolean ): Promise => {
41 |
42 | await State.refresh ( force );
43 | await Statusbar.refresh ( Context.statusbar );
44 |
45 | };
46 |
47 | /* EXPORT */
48 |
49 | export {openInBrowser, refresh, setToken, update};
50 |
--------------------------------------------------------------------------------
/src/context.ts:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | import vscode from 'vscode';
5 | import Statusbar from './statusbar';
6 |
7 | /* MAIN */
8 |
9 | const Context = {
10 | token: undefined,
11 | secrets: undefined,
12 | store: undefined,
13 | statusbar: Statusbar.create ()
14 | };
15 |
16 | /* EXPORT */
17 |
18 | export default Context;
19 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | import vscode from 'vscode';
5 | import Context from './context';
6 | import * as Commands from './commands';
7 | import Secrets from './secrets';
8 | import Statusbar from './statusbar';
9 |
10 | /* MAIN */
11 |
12 | const activate = async ( context: vscode.ExtensionContext ): Promise => {
13 |
14 | /* INIT */
15 |
16 | Context.secrets = context.secrets;
17 | Context.store = context.globalState;
18 |
19 | await Secrets.initToken ();
20 |
21 | Statusbar.refresh ( Context.statusbar );
22 | Commands.update ( false );
23 |
24 | /* COMMANDS */
25 |
26 | vscode.commands.registerCommand ( 'githubNotificationsBell.openInBrowser', Commands.openInBrowser );
27 | vscode.commands.registerCommand ( 'githubNotificationsBell.refresh', Commands.refresh );
28 | vscode.commands.registerCommand ( 'githubNotificationsBell.setToken', Commands.setToken );
29 |
30 | /* SETTINGS CHANGE */
31 |
32 | vscode.workspace.onDidChangeConfiguration ( () => Commands.update ( true ) );
33 |
34 | setInterval ( () => Commands.update ( false ), 30_000 );
35 |
36 | /* SECRETS CHANGE */
37 |
38 | Context.secrets?.onDidChange ( async () => {
39 |
40 | Secrets.initToken ();
41 | Commands.update ( true );
42 |
43 | });
44 |
45 | };
46 |
47 | /* EXPORT */
48 |
49 | export {activate};
50 |
--------------------------------------------------------------------------------
/src/secrets.ts:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | import {prompt} from 'vscode-extras';
5 | import Context from './context';
6 |
7 | /* MAIN */
8 |
9 | const Secrets = {
10 |
11 | /* API */
12 |
13 | getToken: async (): Promise => {
14 |
15 | return Context.secrets?.get ( 'token' );
16 |
17 | },
18 |
19 | setToken: async ( token: string ): Promise => {
20 |
21 | Context.token = token;
22 |
23 | return Context.secrets?.store ( 'token', token );
24 |
25 | },
26 |
27 | initToken: async (): Promise => {
28 |
29 | const token = await Secrets.getToken ();
30 |
31 | if ( token ) {
32 |
33 | Context.token = token;
34 |
35 | } else {
36 |
37 | await Secrets.updateToken ();
38 |
39 | }
40 |
41 | },
42 |
43 | updateToken: async (): Promise => {
44 |
45 | const token = await prompt.password ( 'GitHub Notifications - Personal Access Token' );
46 |
47 | if ( !token ) return;
48 |
49 | return Secrets.setToken ( token );
50 |
51 | }
52 |
53 | };
54 |
55 | /* EXPORT */
56 |
57 | export default Secrets;
58 |
--------------------------------------------------------------------------------
/src/state.ts:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | import Context from './context';
5 | import {getOptions} from './utils';
6 |
7 | /* HELPERS */
8 |
9 | const KEY_COUNTER = 'githubNotificationsBell.counter';
10 | const KEY_DATE = 'githubNotificationsBell.date';
11 |
12 | /* MAIN */
13 |
14 | const State = {
15 |
16 | /* API */
17 |
18 | getCounter: (): number => {
19 |
20 | return Context.store?.get ( KEY_COUNTER, 0 ) || 0;
21 |
22 | },
23 |
24 | setCounter: ( counter: number ): void => {
25 |
26 | Context.store?.update ( KEY_COUNTER, counter );
27 |
28 | },
29 |
30 | getDate: (): number => {
31 |
32 | return Context.store?.get ( KEY_DATE, 0 ) || 0;
33 |
34 | },
35 |
36 | setDate: ( date: number ): void => {
37 |
38 | Context.store?.update ( KEY_DATE, date );
39 |
40 | },
41 |
42 | fetch: async ( page: number = 1 ): Promise => {
43 |
44 | try {
45 |
46 | const options = getOptions ();
47 |
48 | if ( !Context.token ) return 0;
49 |
50 | const headers = {
51 | 'Authorization': `token ${Context.token}`,
52 | 'User-Agent': 'vscode-github-notifications-bell'
53 | };
54 |
55 | const url = `${options.protocol}://api.${options.domain}/notifications?page=${page}`;
56 | const response = await fetch ( url, { headers } );
57 | const result = await response.json ();
58 | const resultsPerPage = 50;
59 | const counter = result.length || 0;
60 | const isLastPage = ( counter !== resultsPerPage );
61 |
62 | return isLastPage ? counter : counter + await State.fetch ( page + 1 );
63 |
64 | } catch ( error ) {
65 |
66 | console.error ( error );
67 |
68 | return 0;
69 |
70 | }
71 |
72 | },
73 |
74 | refresh: async ( force?: boolean ): Promise => {
75 |
76 | const options = getOptions ();
77 | const isRefreshable = force || ( Date.now () - State.getDate () ) >= ( options.refreshInterval * 1000 );
78 |
79 | if ( !isRefreshable ) return;
80 |
81 | State.setDate ( Date.now () );
82 |
83 | const counter = await State.fetch ();
84 |
85 | State.setCounter ( counter );
86 |
87 | }
88 |
89 | };
90 |
91 | /* EXPORT */
92 |
93 | export default State;
94 |
--------------------------------------------------------------------------------
/src/statusbar.ts:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | import vscode from 'vscode';
5 | import Context from './context';
6 | import State from './state';
7 | import {getOptions, once} from './utils';
8 |
9 | /* MAIN */
10 |
11 | const Statusbar = {
12 |
13 | /* API */
14 |
15 | create: once ((): vscode.StatusBarItem => {
16 |
17 | const options = getOptions ();
18 | const alignment = ( options.alignment === 'left' ) ? vscode.StatusBarAlignment.Left : vscode.StatusBarAlignment.Right;
19 | const priority = -Infinity;
20 | const item = vscode.window.createStatusBarItem ( alignment, priority );
21 |
22 | return item;
23 |
24 | }),
25 |
26 | refresh: ( item: vscode.StatusBarItem ): void => {
27 |
28 | const options = getOptions ();
29 | const enabled = !!Context.token;
30 | const counter = State.getCounter ();
31 | const visible = !enabled || counter || !options.hideIfNone;
32 |
33 | item.command = enabled ? 'githubNotificationsBell.openInBrowser' : 'githubNotificationsBell.setToken';
34 | item.text = `$(${options.icon})${counter && options.showNumberOfNotifications ? ` ${counter}` : ''}`;
35 | item.color = enabled ? options.color : '#ff2200';
36 | item.tooltip = `${counter} Notifications`;
37 | item[visible ? 'show' : 'hide']();
38 |
39 | }
40 |
41 | };
42 |
43 | /* EXPORT */
44 |
45 | export default Statusbar;
46 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 |
2 | /* MAIN */
3 |
4 | type Options = {
5 | refreshInterval: number,
6 | alignment: string,
7 | icon: string,
8 | color: string,
9 | hideIfNone: boolean,
10 | showNumberOfNotifications: boolean,
11 | protocol: string,
12 | domain: string
13 | };
14 |
15 | /* EXPORT */
16 |
17 | export type {Options};
18 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | import {getConfig} from 'vscode-extras';
5 | import type {Options} from './types';
6 |
7 | /* MAIN */
8 |
9 | const getOptions = (): Options => {
10 |
11 | const config = getConfig ( 'githubNotificationsBell' );
12 | const refreshInterval = isNumber ( config?.refreshInterval ) ? config.refreshInterval : 300;
13 | const alignment = isString ( config?.alignment ) ? config.alignment : 'right';
14 | const icon = isString ( config?.icon ) ? config.icon : 'mark-github';
15 | const color = isString ( config?.color ) ? config.color : '';
16 | const hideIfNone = isBoolean ( config?.hideIfNone ) ? config.hideIfNone : true;
17 | const showNumberOfNotifications = isBoolean ( config?.showNumberOfNotifications ) ? config.showNumberOfNotifications : true;
18 | const protocol = isString ( config?.protocol ) ? config.protocol : 'https';
19 | const domain = isString ( config?.domain ) ? config.domain : 'github.com';
20 |
21 | return {refreshInterval, alignment, icon, color, hideIfNone, showNumberOfNotifications, protocol, domain};
22 |
23 | };
24 |
25 | const isBoolean = ( value: unknown ): value is boolean => {
26 |
27 | return typeof value === 'boolean';
28 |
29 | };
30 |
31 | const isNumber = ( value: unknown ): value is number => {
32 |
33 | return typeof value === 'number';
34 |
35 | };
36 |
37 | const isString = ( value: unknown ): value is string => {
38 |
39 | return typeof value === 'string';
40 |
41 | };
42 |
43 | const once = ( fn: () => T ): (() => T) => {
44 |
45 | let inited = false;
46 | let result: T;
47 |
48 | return (): T => {
49 |
50 | result = ( inited ? result : fn () );
51 | inited = true;
52 |
53 | return result;
54 |
55 | };
56 |
57 | };
58 |
59 | /* EXPORT */
60 |
61 | export {getOptions, isBoolean, isNumber, isString, once};
62 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsex/tsconfig.json",
3 | "compilerOptions": {
4 | "noPropertyAccessFromIndexSignature": false
5 | }
6 | }
7 |
--------------------------------------------------------------------------------