├── .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 | Logo 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 | --------------------------------------------------------------------------------