├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.yml ├── .vscode ├── extensions.json └── settings.json ├── .yarnrc ├── LICENSE ├── README.md ├── binding.gyp ├── docs ├── index.md └── releases.md ├── jest.json ├── lib ├── index.ts ├── native-module.ts ├── notification-callback.ts ├── notification-event-type.ts ├── notification-options.ts ├── notification-permission.ts ├── notification-settings-url.ts └── notification-support.ts ├── package.json ├── sample-app ├── index.html ├── main.ts ├── package.json ├── preload.ts ├── renderer.ts ├── tsconfig.json ├── webpack.main.config.js └── yarn.lock ├── script ├── download-node-lib-win-arm64.ps1 └── upload.js ├── src ├── mac │ ├── GHDesktopNotificationsManager.h │ ├── GHDesktopNotificationsManager.m │ ├── Utils.h │ ├── Utils.mm │ └── main_mac.mm └── win │ ├── DesktopNotification.cpp │ ├── DesktopNotification.h │ ├── DesktopNotificationsActionCenterActivator.h │ ├── DesktopNotificationsManager.cpp │ ├── DesktopNotificationsManager.h │ ├── main_win.cc │ ├── utils.cpp │ └── utils.h ├── tsconfig.json ├── vendor └── yarn-1.16.0.js └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - 'v*' 9 | pull_request: 10 | 11 | jobs: 12 | build: 13 | name: ${{ matrix.friendlyName }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | node: [16.13.0] 19 | os: [windows-2019, macos-latest] 20 | include: 21 | - os: windows-2019 22 | friendlyName: Windows 23 | - os: macos-latest 24 | friendlyName: macOS 25 | timeout-minutes: 10 26 | steps: 27 | - uses: actions/checkout@v2 28 | - name: Use Node.js ${{ matrix.node-version }} 29 | uses: actions/setup-node@v2.1.2 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | 33 | # This step can be removed as soon as official Windows arm64 builds are published: 34 | # https://github.com/nodejs/build/issues/2450#issuecomment-705853342 35 | - run: | 36 | $NodeVersion = (node --version) -replace '^.' 37 | $NodeFallbackVersion = "16.13.0" 38 | & .\script\download-node-lib-win-arm64.ps1 $NodeVersion $NodeFallbackVersion 39 | if: ${{ matrix.os == 'windows-2019' }} 40 | name: Install Windows arm64 node.lib 41 | 42 | - name: Install and build dependencies 43 | run: yarn 44 | - name: Lint 45 | run: yarn check-prettier 46 | - name: Build 47 | run: yarn build 48 | - name: Prebuild x64 49 | run: npm run prebuild-napi-x64 50 | - name: Prebuild arm64 51 | run: npm run prebuild-napi-arm64 52 | - name: Publish 53 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 54 | run: yarn upload 55 | env: 56 | GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # generated TS files 61 | dist/ 62 | 63 | # prebuild native modules for publishing 64 | prebuilds/ 65 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ignore everything unrelated to the source code 2 | .vscode 3 | .prettierrc.yml 4 | benchmarks/ 5 | docs/ 6 | lib/ 7 | script/ 8 | test/ 9 | sample-app/ 10 | .gitattributes 11 | tsconfig.json 12 | .yarnrc 13 | vendor/ 14 | yarn-error.log 15 | *.tgz 16 | build 17 | jest.json 18 | .node-version 19 | .github 20 | dist/test 21 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .prettierrc.yml 3 | benchmarks/ 4 | .gitattributes 5 | .yarnrc 6 | vendor/ 7 | yarn-error.log 8 | build 9 | dist 10 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | trailingComma: es5 3 | semi: false 4 | proseWrap: always 5 | endOfLine: auto 6 | arrowParens: avoid 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "eg2.tslint", 4 | "samverschueren.final-newline", 5 | "DmitryDorofeev.empty-indent", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "search.exclude": { 4 | "**/node_modules": true, 5 | "**/dist": true, 6 | "**/out": true, 7 | ".awcache": true 8 | }, 9 | "files.exclude": { 10 | "**/.git": true, 11 | "**/.svn": true, 12 | "**/.hg": true, 13 | "**/.DS_Store": true, 14 | "**/node_modules": true, 15 | "**/build": true, 16 | "**/dist": true 17 | }, 18 | "editor.tabSize": 2, 19 | "prettier.semi": false, 20 | "prettier.singleQuote": true, 21 | "prettier.trailingComma": "es5", 22 | "editor.formatOnSave": true, 23 | "files.associations": { 24 | "string": "cpp", 25 | "array": "cpp", 26 | "atomic": "cpp", 27 | "bit": "cpp", 28 | "*.tcc": "cpp", 29 | "cctype": "cpp", 30 | "clocale": "cpp", 31 | "cmath": "cpp", 32 | "compare": "cpp", 33 | "concepts": "cpp", 34 | "cstdarg": "cpp", 35 | "cstddef": "cpp", 36 | "cstdint": "cpp", 37 | "cstdio": "cpp", 38 | "cstdlib": "cpp", 39 | "cstring": "cpp", 40 | "cwchar": "cpp", 41 | "cwctype": "cpp", 42 | "deque": "cpp", 43 | "unordered_map": "cpp", 44 | "vector": "cpp", 45 | "exception": "cpp", 46 | "algorithm": "cpp", 47 | "functional": "cpp", 48 | "iterator": "cpp", 49 | "memory": "cpp", 50 | "memory_resource": "cpp", 51 | "numeric": "cpp", 52 | "optional": "cpp", 53 | "random": "cpp", 54 | "string_view": "cpp", 55 | "system_error": "cpp", 56 | "tuple": "cpp", 57 | "type_traits": "cpp", 58 | "utility": "cpp", 59 | "initializer_list": "cpp", 60 | "iosfwd": "cpp", 61 | "iostream": "cpp", 62 | "istream": "cpp", 63 | "limits": "cpp", 64 | "new": "cpp", 65 | "ostream": "cpp", 66 | "ranges": "cpp", 67 | "sstream": "cpp", 68 | "stdexcept": "cpp", 69 | "streambuf": "cpp", 70 | "cinttypes": "cpp", 71 | "typeinfo": "cpp", 72 | "chrono": "cpp", 73 | "codecvt": "cpp", 74 | "condition_variable": "cpp", 75 | "ctime": "cpp", 76 | "map": "cpp", 77 | "ratio": "cpp", 78 | "fstream": "cpp", 79 | "iomanip": "cpp", 80 | "mutex": "cpp", 81 | "stop_token": "cpp", 82 | "thread": "cpp", 83 | "__locale": "cpp", 84 | "locale": "cpp", 85 | "xstring": "cpp", 86 | "charconv": "cpp", 87 | "format": "cpp", 88 | "forward_list": "cpp", 89 | "ios": "cpp", 90 | "list": "cpp", 91 | "shared_mutex": "cpp", 92 | "xfacet": "cpp", 93 | "xhash": "cpp", 94 | "xiosbase": "cpp", 95 | "xlocale": "cpp", 96 | "xlocbuf": "cpp", 97 | "xlocinfo": "cpp", 98 | "xlocmes": "cpp", 99 | "xlocmon": "cpp", 100 | "xlocnum": "cpp", 101 | "xloctime": "cpp", 102 | "xmemory": "cpp", 103 | "xstddef": "cpp", 104 | "xtr1common": "cpp", 105 | "xtree": "cpp", 106 | "xutility": "cpp" 107 | } 108 | } -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | yarn-path "./vendor/yarn-1.16.0.js" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 GitHub Desktop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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 FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # desktop-notifications 2 | 3 | ## A simple and opinionated library for working with OS notifications 4 | 5 | ## Goals 6 | 7 | - zero dependencies 8 | - good support for Windows notifications 9 | - leverage TypeScript declarations wherever possible 10 | 11 | **Note:** This is currently in preview, with support for features that GitHub 12 | Desktop uses. 13 | 14 | ## Install 15 | 16 | ```shellsession 17 | $ npm install --save desktop-notifications 18 | # or 19 | $ yarn add desktop-notifications 20 | ``` 21 | 22 | ## But Why? 23 | 24 | The current set of options for interacting with notifications, especially on 25 | Windows, have some limitations that meant we couldn't use them easily in GitHub 26 | Desktop: 27 | 28 | - [`electron`](https://www.electronjs.org/) doesn't support Windows 29 | notifications when those are hidden away in the Action Center, because it 30 | doesn't have a COM activator that could leverage CLSID-based activation. More 31 | details about this can be found in 32 | [electron/electron#29461](https://github.com/electron/electron/issues/29461). 33 | - [`node-notifier`](https://www.npmjs.com/package/node-notifier) relies on 34 | [`snoretoast`](https://github.com/KDE/snoretoast) to handle notifications on 35 | Windows, and the way it's used is only able to detect one event with each 36 | notification, and also requires the app to use the same CLSID that is 37 | [hardcoded](https://github.com/KDE/snoretoast/blob/17f88b2c757d54581bb7d5aa4d0d4462c3e75a98/CMakeLists.txt#L5) 38 | in snoretoast. 39 | - [`electron-windows-notifications`](https://github.com/felixrieseberg/electron-windows-notifications) 40 | has **many** dependencies around [`NodeRT`](https://github.com/NodeRT/NodeRT) 41 | which, as of today, also require some 42 | [manual steps](https://stackoverflow.com/a/54591996/673745) in order to build 43 | them. 44 | 45 | After exploring all these options, we decided to write our own library to do the 46 | stuff we require using no dependencies at all and having all the features we 47 | need. 48 | 49 | ## Building and running the sample app 50 | 51 | In order to get the sample app up and running you need to: 52 | 53 | 1. Build `desktop-notifications`. 54 | 2. Build the sample app. 55 | 3. (macOS only) Sign the Electron binary, so that macOS will allow the sample 56 | app to display notifications. 57 | 4. Start the sample app. 58 | 59 | These are the commands to make that happen: 60 | 61 | ``` 62 | $ yarn install 63 | $ yarn build 64 | $ cd sample-app 65 | $ yarn install 66 | $ yarn build 67 | $ /usr/bin/codesign --deep --force --sign - --timestamp\=none node_modules/electron/dist/Electron.app/Contents/MacOS/Electron # macOS only 68 | $ yarn start 69 | ``` 70 | 71 | ## Documentation 72 | 73 | See the documentation under the 74 | [`docs`](https://github.com/desktop/desktop-notifications/tree/master/docs) 75 | folder. 76 | 77 | ## Supported versions 78 | 79 | Each release of `desktop-notifications` includes prebuilt binaries based on 80 | [N-API](https://nodejs.org/api/n-api.html), with support for different versions 81 | of Node and Electron. Please refer to the 82 | [N-API version matrix](https://nodejs.org/api/n-api.html#node-api-version-matrix) 83 | and the release documentation for [Node](https://github.com/nodejs/Release) and 84 | [Electron](https://electronjs.org/docs/tutorial/support) to see what is 85 | supported currently. 86 | 87 | ## Contributing 88 | 89 | Read the 90 | [Setup](https://github.com/desktop/desktop-notifications/blob/master/docs/index.md#setup) 91 | section to ensure your development environment is setup for what you need. 92 | 93 | This project isn't about implementing a 1-1 replication of any other 94 | notifications API, but implementing just enough for whatever usage GitHub 95 | Desktop needs. 96 | 97 | If you want to see something supported, open an issue to start a discussion 98 | about it. 99 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [ 3 | { 4 | 'target_name': 'desktop-notifications', 5 | 'cflags!': [ '-fno-exceptions' ], 6 | 'cflags_cc!': [ '-fno-exceptions' ], 7 | 'msvs_settings': { 8 | 'VCCLCompilerTool': { 'ExceptionHandling': 1 }, 9 | }, 10 | 'include_dirs': [ 11 | ' { 25 | console.log('Hello world!') 26 | } 27 | 28 | // Then show it! 29 | notification.show() 30 | 31 | // ... 32 | 33 | // Finally, clean up any resources used by the notifications environment 34 | terminateNotifications() 35 | ``` 36 | 37 | ## Setup 38 | 39 | ```shellsession 40 | $ git clone https://github.com/desktop/desktop-notifications 41 | $ cd desktop-notifications 42 | $ yarn 43 | ``` 44 | 45 | As this project builds a native module, you'll need these dependencies along 46 | with a recent version of Node: 47 | 48 | - [Python 2.7](https://www.python.org/downloads/windows/) 49 | - _Let Python install into the default suggested path (`c:\Python27`), 50 | otherwise you'll have to configure node-gyp manually with the path which is 51 | annoying._ 52 | - _Ensure the **Add python.exe to Path** option is selected._ 53 | - One of Visual Studio 2019, Visual C++ Build Tools or Visual Studio 2019 54 | - [Visual C++ Build Tools](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools) 55 | - _Run `npm config set msvs_version 2019` to tell node to use this 56 | toolchain._ 57 | - [Visual Studio 2019](https://www.visualstudio.com/vs/community/) 58 | - _Ensure you select the **Desktop development with C++** feature as that is 59 | required by Node.js for installing native modules._ 60 | - _Run `npm config set msvs_version 2019` to tell node to use this 61 | toolchain._ 62 | - **IMPORTANT:** Make sure to install Windows 10 SDK. 63 | -------------------------------------------------------------------------------- /docs/releases.md: -------------------------------------------------------------------------------- 1 | # Releases 2 | 3 | ## Release/Publishing 4 | 5 | Before running the commands in 'Publishing to NPM', create a new release branch 6 | of the form `releases/x.x.x` 7 | 8 | After running commands in 'Publishing to NPM', the release branch should be 9 | pushed. Now, you need to get it reviewed and merged. 10 | 11 | After that, don't forget publish the release on the repo. 12 | 13 | - Go to https://github.com/desktop/desktop-notifications/releases 14 | - Click click `Draft a New Release` 15 | - Fill in form 16 | - Hit `Publish release` 17 | 18 | ## Publishing to NPM 19 | 20 | Releases are done to NPM, and are currently limited to the core team. 21 | 22 | ```sh 23 | # to ensure everything is up-to-date and tests pass 24 | npm ci 25 | npm test 26 | 27 | # you might need to do a different sort of version bump here 28 | npm version minor 29 | 30 | # this will also run the test suite and fail if any errors found 31 | # this will also run `git push --follow-tags` at the end 32 | npm publish 33 | ``` 34 | -------------------------------------------------------------------------------- /jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "testMatch": ["**/dist/**/*-test.js"], 3 | "testTimeout": 50000 4 | } 5 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | initializeNotifications, 3 | showNotification, 4 | closeNotification, 5 | terminateNotifications, 6 | getNotificationsPermission, 7 | requestNotificationsPermission, 8 | } from './native-module' 9 | export { 10 | supportsNotifications, 11 | supportsNotificationsPermissionRequest, 12 | } from './notification-support' 13 | export { getNotificationSettingsUrl } from './notification-settings-url' 14 | export { onNotificationEvent } from './notification-callback' 15 | -------------------------------------------------------------------------------- /lib/native-module.ts: -------------------------------------------------------------------------------- 1 | import { supportsNotifications } from './notification-support' 2 | import { notificationCallback } from './notification-callback' 3 | import { DesktopNotificationPermission } from './notification-permission' 4 | import { INotificationOptions } from './notification-options' 5 | import { v4 as uuidv4 } from 'uuid' 6 | 7 | // The native binary will be loaded lazily to avoid any possible crash at start 8 | // time, which are harder to trace. 9 | let _nativeModule: any | null | undefined = undefined 10 | 11 | function getNativeModule(): any | null { 12 | if (_nativeModule !== undefined) { 13 | return _nativeModule 14 | } 15 | 16 | _nativeModule = supportsNotifications() 17 | ? require('../build/Release/desktop-notifications.node') 18 | : null 19 | 20 | return _nativeModule 21 | } 22 | 23 | /** Initializes the desktop-notifications system. */ 24 | export const initializeNotifications: ( 25 | opts: INotificationOptions 26 | ) => void = opts => 27 | getNativeModule()?.initializeNotifications(notificationCallback, opts) 28 | 29 | /** Terminates the desktop-notifications system. */ 30 | export const terminateNotifications: () => void = () => 31 | getNativeModule()?.terminateNotifications() 32 | 33 | /** Gets the current state of the notifications permission. */ 34 | export const getNotificationsPermission: () => Promise< 35 | DesktopNotificationPermission 36 | > = () => getNativeModule()?.getNotificationsPermission() 37 | 38 | /** Requests the user to grant permission to display notifications. */ 39 | export const requestNotificationsPermission: () => Promise = () => 40 | getNativeModule()?.requestNotificationsPermission() 41 | 42 | /** 43 | * Displays a system notification. 44 | * @param title Title of the notification 45 | * @param body Body of the notification 46 | * @param userInfo (Optional) An object with any information that needs to be 47 | * passed to the notification callback when the user clicks on the notification. 48 | * @returns The ID of the notification displayed. This ID can be used to close 49 | * the notification. 50 | */ 51 | export const showNotification: ( 52 | title: string, 53 | body: string, 54 | userInfo?: Record 55 | ) => Promise = async (...args) => { 56 | const id = uuidv4() 57 | try { 58 | await getNativeModule()?.showNotification(id, ...args) 59 | } catch (e) { 60 | return null 61 | } 62 | return id 63 | } 64 | 65 | /** Closes the notification with the given ID. */ 66 | export const closeNotification: (id: string) => void = (...args) => 67 | getNativeModule()?.closeNotification(...args) 68 | -------------------------------------------------------------------------------- /lib/notification-callback.ts: -------------------------------------------------------------------------------- 1 | import { DesktopNotificationEvent } from './notification-event-type' 2 | 3 | /** 4 | * Callback for notification events. The specific type of userInfo can be chosen 5 | * as long as it's a Record. 6 | */ 7 | export type NotificationCallback< 8 | T extends Record = Record 9 | > = (event: DesktopNotificationEvent, id: string, userInfo: T) => void 10 | 11 | let globalNotificationCallback: NotificationCallback | null = null 12 | 13 | /** 14 | * This is used internally to handle notification events sent from the native 15 | * side. 16 | */ 17 | export const notificationCallback: NotificationCallback = (...args) => 18 | globalNotificationCallback?.(...args) 19 | 20 | /** Sets a handler for notification events. */ 21 | export const onNotificationEvent = < 22 | T extends Record = Record 23 | >( 24 | callback: NotificationCallback | null 25 | ) => { 26 | globalNotificationCallback = callback as NotificationCallback 27 | } 28 | -------------------------------------------------------------------------------- /lib/notification-event-type.ts: -------------------------------------------------------------------------------- 1 | export type DesktopNotificationEvent = 'click' 2 | -------------------------------------------------------------------------------- /lib/notification-options.ts: -------------------------------------------------------------------------------- 1 | export interface INotificationOptions { 2 | /** CLSID used by Windows to report notification events */ 3 | readonly toastActivatorClsid?: string 4 | } 5 | -------------------------------------------------------------------------------- /lib/notification-permission.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Different types of notification permissions: 3 | * - "default": The user has not yet made a choice regarding whether to allow 4 | * or deny notifications. On Windows, this is the same as "granted". 5 | * - "granted": The user has granted permission to display notifications. 6 | * - "denied": The user has denied permission to display notifications. 7 | */ 8 | export type DesktopNotificationPermission = 'default' | 'granted' | 'denied' 9 | -------------------------------------------------------------------------------- /lib/notification-settings-url.ts: -------------------------------------------------------------------------------- 1 | import { supportsNotifications } from './notification-support' 2 | 3 | /** 4 | * Returns the special URL that leads to the Notifications settings in the 5 | * current OS. On Windows, this is only available on versions 10+. 6 | */ 7 | export function getNotificationSettingsUrl() { 8 | if (!supportsNotifications()) { 9 | return null 10 | } 11 | 12 | return process.platform === 'darwin' 13 | ? 'x-apple.systempreferences:com.apple.preference.notifications' 14 | : 'ms-settings:notifications' 15 | } 16 | -------------------------------------------------------------------------------- /lib/notification-support.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os' 2 | 3 | /** 4 | * Whether or not the current system supports these notifications. As of today, 5 | * only macOS and Windows 10+ are supported. 6 | */ 7 | export function supportsNotifications() { 8 | if (process.platform === 'darwin') { 9 | return supportsDarwinNotifications() 10 | } 11 | 12 | if (process.platform === 'win32') { 13 | return supportsWindowsNotifications() 14 | } 15 | 16 | return false 17 | } 18 | 19 | function supportsDarwinNotifications() { 20 | const versionComponents = os.release().split('.') 21 | const majorVersion = parseInt(versionComponents[0], 10) 22 | 23 | // Only macOS 10.14 and newer are supported. Since os.release() gives us the 24 | // Darwin kernel version, it should be a major version of 18 or higher, according 25 | // to https://en.wikipedia.org/wiki/Darwin_%28operating_system%29#Release_history 26 | return majorVersion >= 18 27 | } 28 | 29 | function supportsWindowsNotifications() { 30 | // The Windows Notification APIs we use are only available on Windows 10+, and 31 | // some of them are limited before the Creators Update (build 15063). 32 | // See: https://docs.microsoft.com/en-us/uwp/api/windows.ui.notifications.toastnotification.tag?view=winrt-22621#remarks 33 | const CreatorsUpdateBuildNumber = 15063 34 | 35 | const versionComponents = os.release().split('.') 36 | const majorVersion = parseInt(versionComponents[0], 10) 37 | const buildNumber = 38 | versionComponents.length >= 3 39 | ? parseInt(versionComponents[2], 10) 40 | : CreatorsUpdateBuildNumber 41 | 42 | // Only Windows 10 (15063) and newer are supported 43 | return ( 44 | majorVersion > 10 || 45 | (majorVersion === 10 && buildNumber >= CreatorsUpdateBuildNumber) 46 | ) 47 | } 48 | 49 | /** 50 | * Whether or not the current system supports asking the user for permission to 51 | * display notifications. As of today, only macOS supports this. 52 | */ 53 | export function supportsNotificationsPermissionRequest() { 54 | return process.platform === 'darwin' && supportsDarwinNotifications() 55 | } 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "desktop-notifications", 3 | "version": "0.2.4", 4 | "description": "A simple and opinionated library for handling Windows notifications", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "scripts": { 8 | "install": "prebuild-install || node-gyp rebuild", 9 | "build": "node-gyp rebuild && tsc", 10 | "pretest": "yarn build", 11 | "test": "jest -c jest.json", 12 | "prepublishOnly": "yarn build", 13 | "postpublish": "git push --follow-tags", 14 | "prettify": "yarn prettier --write \"./**/*.{ts,tsx,js,json,jsx,scss,html,yaml,yml}\"", 15 | "check-prettier": "prettier --check \"./**/*.{ts,tsx,js,json,jsx,scss,html,yaml,yml}\"", 16 | "prebuild-napi-x64": "prebuild -t 4 -r napi -a x64 --strip", 17 | "prebuild-napi-ia32": "prebuild -t 4 -r napi -a ia32 --strip", 18 | "prebuild-napi-arm64": "prebuild -t 4 -r napi -a arm64 --strip", 19 | "prebuild-all": "yarn prebuild-napi-x64 && yarn prebuild-napi-ia32 && yarn prebuild-napi-arm64", 20 | "upload": "node ./script/upload.js" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/desktop/desktop-notifications.git" 25 | }, 26 | "keywords": [], 27 | "author": "", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/desktop/desktop-notifications/issues" 31 | }, 32 | "homepage": "https://github.com/desktop/desktop-notifications#readme", 33 | "devDependencies": { 34 | "@types/benchmark": "^1.0.31", 35 | "@types/jest": "^26.0.13", 36 | "@types/node": "^12.0.0", 37 | "@types/uuid": "^8.3.4", 38 | "benchmark": "^2.1.4", 39 | "jest": "^26.4.2", 40 | "prebuild": "^11.0.2", 41 | "prettier": "^2.0.5", 42 | "ts-node": "^9.0.0", 43 | "typescript": "^3.9.0" 44 | }, 45 | "dependencies": { 46 | "node-addon-api": "^5.0.0", 47 | "prebuild-install": "^7.0.1", 48 | "uuid": "^8.3.2" 49 | }, 50 | "binary": { 51 | "napi_versions": [ 52 | 4 53 | ] 54 | }, 55 | "config": { 56 | "runtime": "napi", 57 | "target": 4 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /sample-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 14 | Desktop Notifications 15 | 16 | 17 |

Desktop Notifications

18 |

This is a sample app to test the package desktop-notifications.

19 | 20 | 23 | 24 | 25 | Notifications Settings 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /sample-app/main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain } from 'electron' 2 | import path from 'path' 3 | import * as url from 'url' 4 | import { 5 | getNotificationsPermission, 6 | initializeNotifications, 7 | showNotification, 8 | onNotificationEvent, 9 | requestNotificationsPermission, 10 | getNotificationSettingsUrl, 11 | terminateNotifications, 12 | } from 'desktop-notifications' 13 | 14 | const createWindow = () => { 15 | const win = new BrowserWindow({ 16 | width: 800, 17 | height: 600, 18 | webPreferences: { 19 | preload: path.join(__dirname, 'preload.js'), 20 | nodeIntegration: true, 21 | contextIsolation: false, 22 | }, 23 | }) 24 | 25 | win.loadURL( 26 | url.format({ 27 | pathname: path.join(__dirname, '../index.html'), 28 | protocol: 'file:', 29 | slashes: true, 30 | }) 31 | ) 32 | win.webContents.openDevTools() 33 | win.on('closed', () => { 34 | terminateNotifications() 35 | }) 36 | return win 37 | } 38 | 39 | app.setAppUserModelId('com.squirrel.GitHubDesktop.GitHubDesktop') 40 | 41 | app.whenReady().then(() => { 42 | const window = createWindow() 43 | 44 | initializeNotifications({ 45 | toastActivatorClsid: '{27D44D0C-A542-5B90-BCDB-AC3126048BA2}', 46 | }) 47 | 48 | onNotificationEvent((event, id, userInfo) => { 49 | console.log(`[MAIN] Notification event: ${event} ${id}`, userInfo) 50 | window.webContents.send('notification-event', event, id, userInfo) 51 | }) 52 | 53 | ipcMain.handle( 54 | 'show-notification', 55 | (_, title: string, body: string, userInfo: Record) => { 56 | return showNotification(title, body, userInfo) 57 | } 58 | ) 59 | 60 | ipcMain.handle('get-notifications-permission', getNotificationsPermission) 61 | 62 | ipcMain.handle( 63 | 'request-notifications-permission', 64 | requestNotificationsPermission 65 | ) 66 | 67 | ipcMain.handle('get-notification-settings-url', getNotificationSettingsUrl) 68 | }) 69 | -------------------------------------------------------------------------------- /sample-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "desktop-notifications-sample-app", 3 | "version": "1.0.0", 4 | "description": "Sample app for desktop-notifications package", 5 | "main": "./dist/main.js", 6 | "license": "MIT", 7 | "devDependencies": { 8 | "awesome-node-loader": "^1.1.1", 9 | "electron": "^18.0.1", 10 | "ts-loader": "^9.2.8", 11 | "typescript": "^4.6.3", 12 | "webpack": "^5.71.0", 13 | "webpack-cli": "^4.9.2", 14 | "webpack-dev-server": "^4.7.4" 15 | }, 16 | "scripts": { 17 | "build": "tsc", 18 | "start": "cross-env NODE_ENV=development webpack --config webpack.main.config.js --mode development && electron ." 19 | }, 20 | "dependencies": { 21 | "cross-env": "^7.0.3", 22 | "desktop-notifications": "../", 23 | "quick-lru": "^3.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sample-app/preload.ts: -------------------------------------------------------------------------------- 1 | global.exports = {} 2 | -------------------------------------------------------------------------------- /sample-app/renderer.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron' 2 | import QuickLRU from 'quick-lru' 3 | 4 | const notificationCallbacks = new QuickLRU void>({ 5 | maxSize: 200, 6 | }) 7 | 8 | ipcRenderer.on('notification-event', (event, id, userInfo) => { 9 | console.log(`[RENDERER] Notification event: ${event} ${id}`, userInfo) 10 | 11 | const callback = notificationCallbacks.get(id) 12 | callback?.() 13 | notificationCallbacks.delete(id) 14 | }) 15 | 16 | setupClickEventListener('showNotificationButton', showNotification) 17 | setupClickEventListener('checkPermissionsButton', checkNotificationsPermission) 18 | setupClickEventListener( 19 | 'requestPermissionsButton', 20 | requestNotificationsPermission 21 | ) 22 | updateNotificationsSettingsLink() 23 | 24 | function setupClickEventListener(id: string, onclick: () => void) { 25 | const element = document.getElementById(id) 26 | if (element === null) { 27 | throw new Error(`Could not find notification with id ${id}`) 28 | } 29 | element.addEventListener('click', onclick) 30 | } 31 | 32 | async function updateNotificationsSettingsLink() { 33 | const notificationsSettingsLink = document.getElementById( 34 | 'notificationsSettingsLink' 35 | ) as HTMLLinkElement 36 | notificationsSettingsLink.href = await ipcRenderer.invoke( 37 | 'get-notification-settings-url' 38 | ) 39 | } 40 | 41 | async function showNotification() { 42 | const notificationID = await ipcRenderer.invoke( 43 | 'show-notification', 44 | 'Test notification', 45 | 'This notification is a test. Hello! 👋', 46 | { 47 | myDict: { foo: 'bar' }, 48 | myString: 'Hello world!', 49 | myBool: true, 50 | myArray: ['one', 2, 'three'], 51 | type: 'pr-review-submit-fake', 52 | myNumber: 42, 53 | } 54 | ) 55 | 56 | console.log('Shown notification with ID:', notificationID) 57 | 58 | notificationCallbacks.set(notificationID, () => { 59 | console.log(`[RENDERER] Notification clicked: ${notificationID}`) 60 | }) 61 | } 62 | 63 | async function checkNotificationsPermission() { 64 | console.log( 65 | 'checkPermissions', 66 | await ipcRenderer.invoke('get-notifications-permission') 67 | ) 68 | } 69 | 70 | async function requestNotificationsPermission() { 71 | console.log( 72 | 'requestPermissions', 73 | await ipcRenderer.invoke('request-notifications-permission') 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /sample-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es2020", 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true, 8 | "allowUnreachableCode": false, 9 | "allowUnusedLabels": false, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noUnusedLocals": true, 13 | "sourceMap": true, 14 | "jsx": "react", 15 | "strict": true, 16 | "outDir": "./dist", 17 | "useUnknownInCatchVariables": false 18 | }, 19 | "include": ["**/*.ts", "**/*.tsx", "**/*.d.tsx"], 20 | "exclude": ["node_modules", "dist", "out"], 21 | "compileOnSave": false 22 | } 23 | -------------------------------------------------------------------------------- /sample-app/webpack.main.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | resolve: { 5 | extensions: ['.tsx', '.ts', '.js'], 6 | }, 7 | devtool: 'source-map', 8 | entry: './main.ts', 9 | target: 'electron-main', 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(js|ts|tsx)$/, 14 | exclude: /node_modules/, 15 | loader: 'ts-loader', 16 | }, 17 | { 18 | test: /\.node$/, 19 | loader: 'awesome-node-loader', 20 | }, 21 | ], 22 | }, 23 | output: { 24 | path: path.resolve(__dirname, './dist'), 25 | filename: '[name].js', 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /script/download-node-lib-win-arm64.ps1: -------------------------------------------------------------------------------- 1 | # This script can be removed as soon as official Windows arm64 builds are published: 2 | # https://github.com/nodejs/build/issues/2450#issuecomment-705853342 3 | 4 | $nodeVersion = $args[0] 5 | $fallbackVersion = $args[1] 6 | 7 | If ($null -eq $nodeVersion -Or $null -eq $fallbackVersion) { 8 | Write-Error "No NodeJS version given as argument to this file. Run it like download-nodejs-win-arm64.ps1 NODE_VERSION NODE_FALLBACK_VERSION" 9 | exit 1 10 | } 11 | 12 | $url = "https://unofficial-builds.nodejs.org/download/release/v$nodeVersion/win-arm64/node.lib" 13 | $fallbackUrl = "https://unofficial-builds.nodejs.org/download/release/v$fallbackVersion/win-arm64/node.lib" 14 | 15 | # Always write to the $nodeVersion cache folder, even if we're using the fallbackVersion 16 | $cacheFolder = "$env:TEMP\prebuild\napi\$nodeVersion\arm64" 17 | 18 | If (!(Test-Path $cacheFolder)) { 19 | New-Item -ItemType Directory -Force -Path $cacheFolder 20 | } 21 | 22 | $output = "$cacheFolder\node.lib" 23 | $start_time = Get-Date 24 | 25 | Try { 26 | Invoke-WebRequest -Uri $url -OutFile $output 27 | $downloadedNodeVersion = $nodeVersion 28 | } Catch { 29 | If ($_.Exception.Response -And $_.Exception.Response.StatusCode -eq "NotFound") { 30 | Write-Output "No arm64 node.lib found for Node Windows $nodeVersion, trying fallback version $fallbackVersion..." 31 | Invoke-WebRequest -Uri $fallbackUrl -OutFile $output 32 | $downloadedNodeVersion = $fallbackVersion 33 | } 34 | } 35 | 36 | Write-Output "Downloaded arm64 NodeJS lib v$downloadedNodeVersion to $output in $((Get-Date).Subtract($start_time).Seconds) second(s)" 37 | -------------------------------------------------------------------------------- /script/upload.js: -------------------------------------------------------------------------------- 1 | // to ensure that env not in the CI server log 2 | 3 | const path = require('path') 4 | const { spawnSync } = require('child_process') 5 | 6 | spawnSync( 7 | path.join( 8 | __dirname, 9 | '../node_modules/.bin/prebuild' + 10 | (process.platform === 'win32' ? '.cmd' : '') 11 | ), 12 | ['--upload-all', process.env.GITHUB_AUTH_TOKEN], 13 | { stdio: 'inherit' } 14 | ) 15 | -------------------------------------------------------------------------------- /src/mac/GHDesktopNotificationsManager.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | NS_ASSUME_NONNULL_BEGIN 4 | 5 | typedef void (^GHNotificationCompletionHandler)( 6 | NSString *event, 7 | NSString *identifier, 8 | NSDictionary *userInfo, 9 | dispatch_block_t completionHandler 10 | ); 11 | 12 | API_AVAILABLE(macos(10.14)) 13 | @interface GHDesktopNotificationsManager : NSObject 14 | 15 | @property(nonatomic, nullable) GHNotificationCompletionHandler completionHandler; 16 | 17 | - (void)getNotificationSettingsWithCompletionHandler:(void(^)(NSString *permission))completionHandler; 18 | - (void)requestAuthorizationWithCompletionHandler:(void(^)(BOOL granted, NSError * _Nullable error))completionHandler; 19 | 20 | - (void)showNotificationWithIdentifier:(NSString *)identifier 21 | title:(NSString *)title 22 | body:(NSString *)body 23 | userInfo:(NSDictionary * _Nullable)userInfo 24 | completionHandler:(void(^)(NSError * _Nullable error))completionHandler; 25 | - (void)removePendingNotificationRequestsWithIdentifiers:(NSArray *)identifiers; 26 | 27 | @end 28 | 29 | NS_ASSUME_NONNULL_END 30 | -------------------------------------------------------------------------------- /src/mac/GHDesktopNotificationsManager.m: -------------------------------------------------------------------------------- 1 | #import "GHDesktopNotificationsManager.h" 2 | 3 | #import 4 | 5 | @interface GHDesktopNotificationsManager () 6 | 7 | @end 8 | 9 | @implementation GHDesktopNotificationsManager 10 | 11 | - (instancetype)init 12 | { 13 | if ((self = [super init])) 14 | { 15 | [self configureNotificationsDelegate]; 16 | } 17 | return self; 18 | } 19 | 20 | - (void)configureNotificationsDelegate 21 | { 22 | UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; 23 | center.delegate = self; 24 | } 25 | 26 | - (void)getNotificationSettingsWithCompletionHandler:(void(^)(NSString *permission))completionHandler 27 | { 28 | UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; 29 | [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) { 30 | NSString *permission = @"default"; 31 | 32 | if (settings.authorizationStatus == UNAuthorizationStatusNotDetermined) 33 | { 34 | permission = @"default"; 35 | } 36 | else if (settings.authorizationStatus == UNAuthorizationStatusDenied) 37 | { 38 | permission = @"denied"; 39 | } 40 | else 41 | { 42 | permission = @"granted"; 43 | } 44 | 45 | completionHandler(permission); 46 | }]; 47 | } 48 | 49 | - (void)requestAuthorizationWithCompletionHandler:(void(^)(BOOL granted, NSError * _Nullable error))completionHandler 50 | { 51 | UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; 52 | [center 53 | requestAuthorizationWithOptions:UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert 54 | completionHandler:completionHandler]; 55 | } 56 | 57 | - (void)showNotificationWithIdentifier:(NSString *)identifier 58 | title:(NSString *)title 59 | body:(NSString *)body 60 | userInfo:(NSDictionary * _Nullable)userInfo 61 | completionHandler:(void(^)(NSError * _Nullable error))completionHandler 62 | { 63 | UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init]; 64 | content.title = [NSString localizedUserNotificationStringForKey:title arguments:nil]; 65 | content.body = [NSString localizedUserNotificationStringForKey:body arguments:nil]; 66 | content.sound = [UNNotificationSound defaultSound]; 67 | 68 | if (userInfo != nil) 69 | { 70 | content.userInfo = userInfo; 71 | } 72 | 73 | // Create the request object. 74 | UNNotificationRequest* request = [UNNotificationRequest 75 | requestWithIdentifier:identifier content:content trigger:nil]; 76 | 77 | UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; 78 | 79 | [center 80 | requestAuthorizationWithOptions:UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert 81 | completionHandler:^(BOOL granted, NSError *error) { 82 | 83 | if (error != nil || !granted) 84 | { 85 | NSLog(@"Permission to display notification wasn't granted: %@", error); 86 | completionHandler(error ?: [NSError errorWithDomain:@"GHDesktopNotificationManagerError" code:-1 userInfo:nil]); 87 | return; 88 | } 89 | 90 | [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { 91 | if (error != nil) { 92 | NSLog(@"Error posting notification: %@", error.localizedDescription); 93 | completionHandler(error); 94 | return; 95 | } 96 | 97 | completionHandler(nil); 98 | }]; 99 | }]; 100 | } 101 | 102 | - (void)removePendingNotificationRequestsWithIdentifiers:(NSArray *)identifiers 103 | { 104 | UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; 105 | [center removePendingNotificationRequestsWithIdentifiers:identifiers]; 106 | } 107 | 108 | #pragma mark - UNUserNotificationCenterDelegate methods 109 | 110 | - (void)userNotificationCenter:(UNUserNotificationCenter *)center 111 | didReceiveNotificationResponse:(UNNotificationResponse *)response 112 | withCompletionHandler:(void (^)(void))completionHandler 113 | { 114 | if (self.completionHandler == nil) 115 | { 116 | completionHandler(); 117 | return; 118 | } 119 | 120 | UNNotificationRequest *request = response.notification.request; 121 | self.completionHandler(@"click", request.identifier, request.content.userInfo, completionHandler); 122 | } 123 | 124 | - (void)userNotificationCenter:(UNUserNotificationCenter *)center 125 | willPresentNotification:(UNNotification *)notification 126 | withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler 127 | { 128 | // This will make sure the notification is displayed even when the app is focused 129 | completionHandler(UNNotificationPresentationOptionAlert 130 | | UNNotificationPresentationOptionBadge 131 | | UNNotificationPresentationOptionSound); 132 | } 133 | 134 | @end 135 | -------------------------------------------------------------------------------- /src/mac/Utils.h: -------------------------------------------------------------------------------- 1 | #include 2 | #import 3 | 4 | Napi::Value getNapiValueFromNSObject(const Napi::Env &env, id value); 5 | id getNSObjectFromNapiValue(const Napi::Env &env, const Napi::Value &value); 6 | -------------------------------------------------------------------------------- /src/mac/Utils.mm: -------------------------------------------------------------------------------- 1 | #import "Utils.h" 2 | 3 | Napi::Object getNapiObjectFromDictionary(const Napi::Env &env, NSDictionary *d) 4 | { 5 | Napi::Object obj = Napi::Object::New(env); 6 | for (NSString *key in d) { 7 | id value = d[key]; 8 | obj.Set(Napi::String::New(env, key.UTF8String), getNapiValueFromNSObject(env, value)); 9 | } 10 | return obj; 11 | } 12 | 13 | Napi::Array getNapiArrayFromArray(const Napi::Env &env, NSArray *a) 14 | { 15 | Napi::Array result = Napi::Array::New(env); 16 | for (NSUInteger i = 0; i < a.count; i++) { 17 | id value = a[i]; 18 | result.Set(i, getNapiValueFromNSObject(env, value)); 19 | } 20 | return result; 21 | } 22 | 23 | Napi::Value getNapiValueFromNSObject(const Napi::Env &env, id value) 24 | { 25 | if (value == nil) 26 | { 27 | return env.Undefined(); 28 | } 29 | 30 | if ([value isKindOfClass:[NSString class]]) 31 | { 32 | NSString *string = (NSString *)value; 33 | return Napi::String::New(env, string.UTF8String); 34 | } 35 | else if ([value isKindOfClass:[NSNumber class]]) 36 | { 37 | NSNumber *number = value; 38 | 39 | // Booleans wrapped in NSNumber require special treatment 40 | CFTypeID boolID = CFBooleanGetTypeID(); // the type ID of CFBoolean 41 | CFTypeID numID = CFGetTypeID((__bridge CFTypeRef)(number)); // the type ID of number 42 | if (numID == boolID) 43 | { 44 | return Napi::Boolean::New(env, [number boolValue]); 45 | } 46 | 47 | CFNumberType numberType = CFNumberGetType((CFNumberRef)value); 48 | if (numberType == kCFNumberFloatType || numberType == kCFNumberDoubleType) 49 | { 50 | return Napi::Number::New(env, number.doubleValue); 51 | } 52 | else if (numberType == kCFNumberSInt64Type || numberType == kCFNumberLongLongType) 53 | { 54 | return Napi::Number::New(env, number.longLongValue); 55 | } 56 | else if (numberType == kCFNumberSInt32Type || numberType == kCFNumberIntType) 57 | { 58 | return Napi::Number::New(env, number.intValue); 59 | } 60 | else if (numberType == kCFNumberSInt16Type || numberType == kCFNumberShortType) 61 | { 62 | return Napi::Number::New(env, number.shortValue); 63 | } 64 | else if (numberType == kCFNumberSInt8Type || numberType == kCFNumberCharType) 65 | { 66 | return Napi::Number::New(env, number.charValue); 67 | } 68 | else if (numberType == kCFNumberCFIndexType || numberType == kCFNumberNSIntegerType) 69 | { 70 | return Napi::Number::New(env, number.integerValue); 71 | } 72 | else 73 | { 74 | return Napi::Number::New(env, number.longValue); 75 | } 76 | } 77 | else if ([value isKindOfClass:[NSDictionary class]]) 78 | { 79 | return getNapiObjectFromDictionary(env, value); 80 | } 81 | else if ([value isKindOfClass:[NSArray class]]) 82 | { 83 | return getNapiArrayFromArray(env, value); 84 | } 85 | 86 | return env.Undefined(); 87 | } 88 | 89 | id getNSObjectFromNapiValue(const Napi::Env &env, const Napi::Value &value) 90 | { 91 | if (value.IsString()) 92 | { 93 | return [NSString stringWithUTF8String:value.As().Utf8Value().c_str()]; 94 | } 95 | else if (value.IsNumber()) 96 | { 97 | return @(value.As().DoubleValue()); 98 | } 99 | else if (value.IsBoolean()) 100 | { 101 | return [NSNumber numberWithBool:value.As().Value()]; 102 | } 103 | else if (value.IsNull()) 104 | { 105 | return [NSNull null]; 106 | } 107 | else if (value.IsObject()) 108 | { 109 | Napi::Object obj = value.As(); 110 | NSMutableDictionary *d = [NSMutableDictionary dictionary]; 111 | for (auto it = obj.begin(); it != obj.end(); ++it) { 112 | auto entry = *it; 113 | NSString *key = [NSString stringWithUTF8String:entry.first.As().Utf8Value().c_str()]; 114 | d[key] = getNSObjectFromNapiValue(env, entry.second); 115 | } 116 | return d; 117 | } 118 | else if (value.IsArray()) 119 | { 120 | Napi::Array arr = value.As(); 121 | NSMutableArray *a = [NSMutableArray array]; 122 | for (uint32_t i = 0; i < arr.Length(); i++) { 123 | a[i] = getNSObjectFromNapiValue(env, arr[i]); 124 | } 125 | return a; 126 | } 127 | 128 | return nil; 129 | } 130 | -------------------------------------------------------------------------------- /src/mac/main_mac.mm: -------------------------------------------------------------------------------- 1 | #include 2 | #include "uv.h" 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #import 11 | 12 | #import "GHDesktopNotificationsManager.h" 13 | #import "Utils.h" 14 | 15 | using namespace Napi; 16 | 17 | // Disable API availability warning here. Availability will be handled when 18 | // the instance is created. 19 | #pragma clang diagnostic push 20 | #pragma clang diagnostic ignored "-Wunguarded-availability-new" 21 | GHDesktopNotificationsManager *desktopNotificationsManager = nil; 22 | #pragma clang diagnostic pop 23 | 24 | Napi::ThreadSafeFunction notificationsCallback; 25 | 26 | // Dummy value to pass into function parameter for ThreadSafeFunction. 27 | Napi::Value NoOp(const Napi::CallbackInfo &info) { 28 | return info.Env().Undefined(); 29 | } 30 | 31 | namespace 32 | { 33 | Napi::ThreadSafeFunction notificationCallback; 34 | 35 | Napi::Value initializeNotifications(const Napi::CallbackInfo &info) 36 | { 37 | const Napi::Env &env = info.Env(); 38 | 39 | if (desktopNotificationsManager) 40 | { 41 | return env.Undefined(); 42 | } 43 | 44 | // There is a second argument, which is an object with options, but it is 45 | // not used on macOS. 46 | if (info.Length() < 1) 47 | { 48 | Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException(); 49 | return env.Undefined(); 50 | } 51 | 52 | if (!info[0].IsFunction()) 53 | { 54 | Napi::TypeError::New(env, "Callback must be a function.").ThrowAsJavaScriptException(); 55 | return env.Undefined(); 56 | } 57 | 58 | // Create the desktop notifications manager only if it's a supported macOS version 59 | if (@available(macOS 10.14, *)) 60 | { 61 | desktopNotificationsManager = [GHDesktopNotificationsManager new]; 62 | } 63 | else 64 | { 65 | return env.Undefined(); 66 | } 67 | 68 | auto callback = info[0].As(); 69 | notificationsCallback = Napi::ThreadSafeFunction::New(callback.Env(), callback, "Notification Callback", 0, 1); 70 | 71 | desktopNotificationsManager.completionHandler = ^(NSString *event, NSString *identifier, NSDictionary *userInfo, dispatch_block_t completionHandler) { 72 | auto cb = [event, identifier, userInfo, completionHandler](Napi::Env env, Napi::Function jsCallback) 73 | { 74 | jsCallback.Call({ 75 | Napi::String::New(env, event.UTF8String), 76 | Napi::String::New(env, identifier.UTF8String), 77 | getNapiValueFromNSObject(env, userInfo), 78 | }); 79 | 80 | // Invoke the OS completion handler 81 | completionHandler(); 82 | }; 83 | 84 | notificationsCallback.BlockingCall(cb); 85 | }; 86 | 87 | return info.Env().Undefined(); 88 | } 89 | 90 | Napi::Value showNotification(const Napi::CallbackInfo &info) 91 | { 92 | const Napi::Env &env = info.Env(); 93 | 94 | if (desktopNotificationsManager == nil) 95 | { 96 | return env.Undefined(); 97 | } 98 | 99 | if (info.Length() < 3) 100 | { 101 | Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException(); 102 | return env.Undefined(); 103 | } 104 | 105 | if (!info[0].IsString()) 106 | { 107 | Napi::TypeError::New(env, "A string was expected for the first argument, but wasn't received.").ThrowAsJavaScriptException(); 108 | return env.Undefined(); 109 | } 110 | 111 | if (!info[1].IsString()) 112 | { 113 | Napi::TypeError::New(env, "A string was expected for the second argument, but wasn't received.").ThrowAsJavaScriptException(); 114 | return env.Undefined(); 115 | } 116 | 117 | if (!info[2].IsString()) 118 | { 119 | Napi::TypeError::New(env, "A string was expected for the third argument, but wasn't received.").ThrowAsJavaScriptException(); 120 | return env.Undefined(); 121 | } 122 | 123 | NSString *notificationID = [NSString stringWithCString:info[0].As().Utf8Value().c_str() 124 | encoding:[NSString defaultCStringEncoding]]; 125 | auto titleUTF16 = info[1].As().Utf16Value(); 126 | NSString *title = [NSString stringWithCharacters:(const unichar *)titleUTF16.c_str() 127 | length:titleUTF16.size()]; 128 | auto bodyUTF16 = info[2].As().Utf16Value(); 129 | NSString *body = [NSString stringWithCharacters:(const unichar *)bodyUTF16.c_str() 130 | length:bodyUTF16.size()]; 131 | 132 | NSDictionary *userInfo = nil; 133 | if (info.Length() > 3 && info[3].IsObject()) 134 | { 135 | auto userInfoObject = info[3].As(); 136 | userInfo = getNSObjectFromNapiValue(env, userInfoObject); 137 | } 138 | 139 | Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env); 140 | Napi::ThreadSafeFunction ts_fn = Napi::ThreadSafeFunction::New( 141 | env, Napi::Function::New(env, NoOp), "showNotificationCallback", 0, 1); 142 | __block Napi::ThreadSafeFunction tsfn = ts_fn; 143 | 144 | [desktopNotificationsManager showNotificationWithIdentifier:notificationID 145 | title:title 146 | body:body 147 | userInfo:userInfo 148 | completionHandler:^(NSError *error) { 149 | 150 | if (error != nil) { 151 | auto callback = [deferred](Napi::Env env, Napi::Function js_cb) { 152 | deferred.Reject(env.Undefined()); 153 | }; 154 | tsfn.BlockingCall(callback); 155 | tsfn.Release(); 156 | return; 157 | } 158 | 159 | auto callback = [deferred](Napi::Env env, Napi::Function js_cb) { 160 | deferred.Resolve(env.Undefined()); 161 | }; 162 | tsfn.BlockingCall(callback); 163 | tsfn.Release(); 164 | }]; 165 | 166 | return deferred.Promise(); 167 | } 168 | 169 | Napi::Value terminateNotifications(const Napi::CallbackInfo &info) 170 | { 171 | const Napi::Env &env = info.Env(); 172 | desktopNotificationsManager = nil; 173 | return env.Undefined(); 174 | } 175 | 176 | Napi::Value closeNotification(const Napi::CallbackInfo &info) 177 | { 178 | const Napi::Env &env = info.Env(); 179 | 180 | if (desktopNotificationsManager == nil) 181 | { 182 | return env.Undefined(); 183 | } 184 | 185 | if (info.Length() < 1) 186 | { 187 | Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException(); 188 | return env.Undefined(); 189 | } 190 | 191 | if (!info[0].IsString()) 192 | { 193 | Napi::TypeError::New(env, "A string was expected for the first argument, but wasn't received.").ThrowAsJavaScriptException(); 194 | return env.Undefined(); 195 | } 196 | 197 | NSString *notificationID = [NSString stringWithCString:info[0].As().Utf8Value().c_str() 198 | encoding:[NSString defaultCStringEncoding]]; 199 | 200 | [desktopNotificationsManager removePendingNotificationRequestsWithIdentifiers:@[notificationID]]; 201 | 202 | return env.Undefined(); 203 | } 204 | 205 | Napi::Value getNotificationsPermission(const Napi::CallbackInfo &info) 206 | { 207 | const Napi::Env &env = info.Env(); 208 | 209 | if (desktopNotificationsManager == nil) 210 | { 211 | return env.Undefined(); 212 | } 213 | 214 | Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env); 215 | Napi::ThreadSafeFunction ts_fn = Napi::ThreadSafeFunction::New( 216 | env, Napi::Function::New(env, NoOp), "getNotificationsPermissionCallback", 0, 1); 217 | 218 | __block Napi::ThreadSafeFunction tsfn = ts_fn; 219 | [desktopNotificationsManager getNotificationSettingsWithCompletionHandler:^(NSString *permission) { 220 | auto callback = [permission, deferred](Napi::Env env, Napi::Function js_cb) { 221 | deferred.Resolve(Napi::String::New(env, permission.UTF8String)); 222 | }; 223 | tsfn.BlockingCall(callback); 224 | tsfn.Release(); 225 | }]; 226 | 227 | return deferred.Promise(); 228 | } 229 | 230 | Napi::Value requestNotificationsPermission(const Napi::CallbackInfo &info) 231 | { 232 | const Napi::Env &env = info.Env(); 233 | 234 | if (desktopNotificationsManager == nil) 235 | { 236 | return env.Undefined(); 237 | } 238 | 239 | Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env); 240 | Napi::ThreadSafeFunction ts_fn = Napi::ThreadSafeFunction::New( 241 | env, Napi::Function::New(env, NoOp), "requestNotificationsPermissionCallback", 0, 1); 242 | 243 | __block Napi::ThreadSafeFunction tsfn = ts_fn; 244 | [desktopNotificationsManager 245 | requestAuthorizationWithCompletionHandler:^(BOOL granted, NSError *error) { 246 | 247 | if (error != nil) { 248 | NSLog(@"Error requesting permission %@", error); 249 | } 250 | 251 | auto callback = [granted, deferred](Napi::Env env, Napi::Function js_cb) { 252 | deferred.Resolve(Napi::Boolean::New(env, granted)); 253 | }; 254 | tsfn.BlockingCall(callback); 255 | tsfn.Release(); 256 | }]; 257 | 258 | return deferred.Promise(); 259 | } 260 | 261 | Napi::Object Init(Napi::Env env, Napi::Object exports) 262 | { 263 | exports.Set(Napi::String::New(env, "initializeNotifications"), Napi::Function::New(env, initializeNotifications)); 264 | exports.Set(Napi::String::New(env, "terminateNotifications"), Napi::Function::New(env, terminateNotifications)); 265 | exports.Set(Napi::String::New(env, "showNotification"), Napi::Function::New(env, showNotification)); 266 | exports.Set(Napi::String::New(env, "closeNotification"), Napi::Function::New(env, closeNotification)); 267 | exports.Set(Napi::String::New(env, "getNotificationsPermission"), Napi::Function::New(env, getNotificationsPermission)); 268 | exports.Set(Napi::String::New(env, "requestNotificationsPermission"), Napi::Function::New(env, requestNotificationsPermission)); 269 | 270 | return exports; 271 | } 272 | } 273 | 274 | NODE_API_MODULE(desktopNotificationsNativeModule, Init); 275 | -------------------------------------------------------------------------------- /src/win/DesktopNotification.cpp: -------------------------------------------------------------------------------- 1 | #include "DesktopNotificationsManager.h" 2 | #include "DesktopNotification.h" 3 | #include "Utils.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | const std::wstring kLaunchAttribute = L"launch"; 13 | 14 | using namespace ABI::Windows::UI::Notifications; 15 | using namespace Windows::Foundation; 16 | using namespace Wrappers; 17 | 18 | DesktopNotification::DesktopNotification(const std::wstring &id, 19 | const std::wstring &appID, 20 | const std::wstring &title, 21 | const std::wstring &body, 22 | const std::wstring &userInfo) 23 | : m_title(title), 24 | m_body(body), 25 | m_userInfo(userInfo), 26 | m_appID(appID), 27 | m_id(id) 28 | { 29 | } 30 | 31 | // Set the values of each of the text nodes 32 | HRESULT DesktopNotification::setTextValues() 33 | { 34 | ComPtr nodeList; 35 | DN_RETURN_ON_ERROR( 36 | m_toastXml->GetElementsByTagName(HStringReference(L"text").Get(), &nodeList)); 37 | // create the title 38 | ComPtr textNode; 39 | DN_RETURN_ON_ERROR(nodeList->Item(0, &textNode)); 40 | DN_RETURN_ON_ERROR( 41 | setNodeValueString(HStringReference(m_title.c_str()).Get(), textNode.Get())); 42 | DN_RETURN_ON_ERROR(nodeList->Item(1, &textNode)); 43 | return setNodeValueString(HStringReference(m_body.c_str()).Get(), textNode.Get()); 44 | } 45 | 46 | HRESULT DesktopNotification::startListeningEvents(DesktopNotificationsManager *desktopNotificationsManager) 47 | { 48 | ComPtr toast = m_notification; 49 | 50 | // TODO: Register the event handlers if we need more control over them. For 51 | // now, just using the events from the activator is enough. 52 | // DN_RETURN_ON_ERROR(toast->add_Activated(desktopNotificationsManager, &m_activatedToken)); 53 | // DN_RETURN_ON_ERROR(toast->add_Dismissed(desktopNotificationsManager, &m_dismissedToken)); 54 | // DN_RETURN_ON_ERROR(toast->add_Failed(desktopNotificationsManager, &m_failedToken)); 55 | 56 | return S_OK; 57 | } 58 | 59 | HRESULT DesktopNotification::setNodeValueString(const HSTRING &inputString, IXmlNode *node) 60 | { 61 | ComPtr inputText; 62 | DN_RETURN_ON_ERROR(m_toastXml->CreateTextNode(inputString, &inputText)); 63 | 64 | ComPtr inputTextNode; 65 | DN_RETURN_ON_ERROR(inputText.As(&inputTextNode)); 66 | 67 | ComPtr pAppendedChild; 68 | return node->AppendChild(inputTextNode.Get(), &pAppendedChild); 69 | } 70 | 71 | HRESULT DesktopNotification::addAttribute(const std::wstring &name, IXmlNamedNodeMap *attributeMap) 72 | { 73 | ComPtr srcAttribute; 74 | HRESULT hr = 75 | m_toastXml->CreateAttribute(HStringReference(name.c_str()).Get(), &srcAttribute); 76 | 77 | if (SUCCEEDED(hr)) 78 | { 79 | ComPtr node; 80 | hr = srcAttribute.As(&node); 81 | if (SUCCEEDED(hr)) 82 | { 83 | ComPtr pNode; 84 | hr = attributeMap->SetNamedItem(node.Get(), &pNode); 85 | } 86 | } 87 | return hr; 88 | } 89 | 90 | HRESULT DesktopNotification::addAttribute(const std::wstring &name, IXmlNamedNodeMap *attributeMap, 91 | const std::wstring &value) 92 | { 93 | ComPtr srcAttribute; 94 | DN_RETURN_ON_ERROR( 95 | m_toastXml->CreateAttribute(HStringReference(name.c_str()).Get(), &srcAttribute)); 96 | 97 | ComPtr node; 98 | DN_RETURN_ON_ERROR(srcAttribute.As(&node)); 99 | 100 | ComPtr pNode; 101 | DN_RETURN_ON_ERROR(attributeMap->SetNamedItem(node.Get(), &pNode)); 102 | return setNodeValueString(HStringReference(value.c_str()).Get(), node.Get()); 103 | } 104 | 105 | void DesktopNotification::printXML() 106 | { 107 | ComPtr s; 108 | ComPtr ss(m_toastXml); 109 | ss.As(&s); 110 | HSTRING string; 111 | s->GetXml(&string); 112 | PCWSTR str = WindowsGetStringRawBuffer(string, nullptr); 113 | DN_LOG_DEBUG(L"------------------------\n\t\t\t" << str << L"\n\t\t" << L"------------------------"); 114 | } 115 | 116 | // Create and display the toast 117 | HRESULT DesktopNotification::createToast(ComPtr toastManager, 118 | DesktopNotificationsManager *desktopNotificationsManager) 119 | { 120 | DN_RETURN_ON_ERROR(toastManager->GetTemplateContent( 121 | ToastTemplateType_ToastImageAndText02, &m_toastXml)); 122 | ComPtr rootList; 123 | DN_RETURN_ON_ERROR( 124 | m_toastXml->GetElementsByTagName(HStringReference(L"toast").Get(), &rootList)); 125 | 126 | ComPtr root; 127 | DN_RETURN_ON_ERROR(rootList->Item(0, &root)); 128 | ComPtr rootAttributes; 129 | DN_RETURN_ON_ERROR(root->get_Attributes(&rootAttributes)); 130 | 131 | const auto data = Utils::formatLaunchArgs(m_id, m_userInfo); 132 | DN_RETURN_ON_ERROR(addAttribute(kLaunchAttribute, rootAttributes.Get(), data)); 133 | DN_RETURN_ON_ERROR(setTextValues()); 134 | 135 | // printXML(); 136 | 137 | DN_RETURN_ON_ERROR(toastManager->CreateToastNotifierWithId( 138 | HStringReference(m_appID.c_str()).Get(), &m_notifier)); 139 | 140 | ComPtr factory; 141 | DN_RETURN_ON_ERROR(GetActivationFactory( 142 | HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(), 143 | &factory)); 144 | DN_RETURN_ON_ERROR(factory->CreateToastNotification(m_toastXml.Get(), &m_notification)); 145 | 146 | ComPtr toastV2; 147 | if (SUCCEEDED(m_notification.As(&toastV2))) 148 | { 149 | DN_RETURN_ON_ERROR(toastV2->put_Tag(HStringReference(m_id.c_str()).Get())); 150 | DN_RETURN_ON_ERROR(toastV2->put_Group(HStringReference(DN_GROUP_NAME).Get())); 151 | } 152 | 153 | std::wstring error; 154 | NotificationSetting setting = NotificationSetting_Enabled; 155 | if (!DN_CHECK_RESULT(m_notifier->get_Setting(&setting))) 156 | { 157 | DN_LOG_ERROR("Failed to retreive NotificationSettings ensure your appId is registered"); 158 | } 159 | switch (setting) 160 | { 161 | case NotificationSetting_Enabled: 162 | DN_RETURN_ON_ERROR(startListeningEvents(desktopNotificationsManager)); 163 | break; 164 | case NotificationSetting_DisabledForApplication: 165 | error = L"DisabledForApplication"; 166 | break; 167 | case NotificationSetting_DisabledForUser: 168 | error = L"DisabledForUser"; 169 | break; 170 | case NotificationSetting_DisabledByGroupPolicy: 171 | error = L"DisabledByGroupPolicy"; 172 | break; 173 | case NotificationSetting_DisabledByManifest: 174 | error = L"DisabledByManifest"; 175 | break; 176 | } 177 | if (!error.empty()) 178 | { 179 | std::wstringstream err; 180 | err << L"Notifications are disabled\n" 181 | << L"Reason: " << error << L" Please make sure that the app id is set correctly.\n" 182 | << L"Command Line: " << GetCommandLineW(); 183 | DN_LOG_ERROR(err.str()); 184 | } 185 | return m_notifier->Show(m_notification.Get()); 186 | } 187 | 188 | std::wstring DesktopNotification::getLaunchArgsFromToast(IToastNotification *toast) 189 | { 190 | IXmlDocument *xmlDoc = nullptr; 191 | toast->get_Content(&xmlDoc); 192 | if (xmlDoc == nullptr) 193 | { 194 | DN_LOG_ERROR(L"Could not get xml document from toast"); 195 | return L""; 196 | } 197 | 198 | // Get "launch" attribute and split it to obtain the notification ID 199 | HSTRING launchArgs; 200 | IXmlElement *rootElement = nullptr; 201 | xmlDoc->get_DocumentElement(&rootElement); 202 | rootElement->GetAttribute(HStringReference(kLaunchAttribute.c_str()).Get(), &launchArgs); 203 | 204 | if (launchArgs == nullptr) 205 | { 206 | DN_LOG_ERROR(L"Could not get launch attribute from toast"); 207 | return L""; 208 | } 209 | 210 | return WindowsGetStringRawBuffer(launchArgs, nullptr); 211 | } 212 | 213 | std::string DesktopNotification::getNotificationIDFromToast(IToastNotification *toast) 214 | { 215 | std::wstring launchArgs = getLaunchArgsFromToast(toast); 216 | if (launchArgs == L"") 217 | { 218 | DN_LOG_ERROR(L"Could not get launch arguments from toast"); 219 | return ""; 220 | } 221 | 222 | return Utils::parseNotificationID(launchArgs); 223 | } 224 | 225 | std::wstring DesktopNotification::getUserInfoFromToast(IToastNotification *toast) 226 | { 227 | std::wstring launchArgs = getLaunchArgsFromToast(toast); 228 | if (launchArgs == L"") 229 | { 230 | DN_LOG_ERROR(L"Could not get launch arguments from toast"); 231 | return L""; 232 | } 233 | 234 | return Utils::parseUserInfo(launchArgs); 235 | } 236 | -------------------------------------------------------------------------------- /src/win/DesktopNotification.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "DesktopNotificationsManager.h" 3 | #include 4 | 5 | class DesktopNotificationsManager; 6 | 7 | class DesktopNotification 8 | { 9 | 10 | public: 11 | explicit DesktopNotification::DesktopNotification(const std::wstring &id, 12 | const std::wstring &appID, 13 | const std::wstring &title, 14 | const std::wstring &body, 15 | const std::wstring &userInfo); 16 | 17 | std::wstring getID() 18 | { 19 | return m_id; 20 | } 21 | 22 | // Create and display the toast 23 | HRESULT createToast(Microsoft::WRL::ComPtr toastManager, 24 | DesktopNotificationsManager *desktopNotificationsManager); 25 | 26 | static std::string getNotificationIDFromToast(ABI::Windows::UI::Notifications::IToastNotification *toast); 27 | static std::wstring getUserInfoFromToast(ABI::Windows::UI::Notifications::IToastNotification *toast); 28 | 29 | private: 30 | std::wstring m_appID; 31 | 32 | std::wstring m_title; 33 | std::wstring m_body; 34 | std::wstring m_userInfo; 35 | std::wstring m_id; 36 | EventRegistrationToken m_activatedToken, m_dismissedToken, m_failedToken; 37 | 38 | Microsoft::WRL::ComPtr m_toastXml; 39 | Microsoft::WRL::ComPtr m_notifier; 40 | Microsoft::WRL::ComPtr m_notification; 41 | 42 | // Set the values of each of the text nodes 43 | HRESULT setTextValues(); 44 | HRESULT startListeningEvents(DesktopNotificationsManager *desktopNotificationsManager); 45 | HRESULT setNodeValueString(const HSTRING &inputString, ABI::Windows::Data::Xml::Dom::IXmlNode *node); 46 | HRESULT addAttribute(const std::wstring &name, ABI::Windows::Data::Xml::Dom::IXmlNamedNodeMap *attributeMap); 47 | HRESULT addAttribute(const std::wstring &name, ABI::Windows::Data::Xml::Dom::IXmlNamedNodeMap *attributeMap, 48 | const std::wstring &value); 49 | void printXML(); 50 | 51 | static std::wstring getLaunchArgsFromToast(ABI::Windows::UI::Notifications::IToastNotification *toast); 52 | }; 53 | -------------------------------------------------------------------------------- /src/win/DesktopNotificationsActionCenterActivator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "DesktopNotificationsManager.h" 9 | #include "Utils.h" 10 | 11 | #define DN_WSTRINGIFY(X) L##X 12 | #define DN_STRINGIFY(X) DN_WSTRINGIFY(X) 13 | 14 | typedef struct NOTIFICATION_USER_INPUT_DATA 15 | { 16 | LPCWSTR Key; 17 | LPCWSTR Value; 18 | } NOTIFICATION_USER_INPUT_DATA; 19 | 20 | MIDL_INTERFACE("53E31837-6600-4A81-9395-75CFFE746F94") 21 | INotificationActivationCallback : public IUnknown 22 | { 23 | public: 24 | virtual HRESULT STDMETHODCALLTYPE Activate( 25 | __RPC__in_string LPCWSTR appUserModelId, __RPC__in_opt_string LPCWSTR invokedArgs, 26 | __RPC__in_ecount_full_opt(count) const NOTIFICATION_USER_INPUT_DATA *data, 27 | ULONG count) = 0; 28 | }; 29 | 30 | // The COM server which implements the callback notifcation from Action Center 31 | class DesktopNotificationsActionCenterActivator 32 | : public Microsoft::WRL::RuntimeClass< 33 | Microsoft::WRL::RuntimeClassFlags, 34 | INotificationActivationCallback> 35 | { 36 | public: 37 | DesktopNotificationsActionCenterActivator() {} 38 | virtual HRESULT STDMETHODCALLTYPE Activate(__RPC__in_string LPCWSTR appUserModelId, 39 | __RPC__in_opt_string LPCWSTR invokedArgs, 40 | __RPC__in_ecount_full_opt(count) 41 | const NOTIFICATION_USER_INPUT_DATA *data, 42 | ULONG count) override 43 | { 44 | if (!desktopNotificationsManager) 45 | { 46 | DN_LOG_ERROR("Cannot handle notification activator: notifications not initialized."); 47 | return S_OK; 48 | } 49 | 50 | desktopNotificationsManager->handleActivatorEvent(invokedArgs); 51 | 52 | return S_OK; 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /src/win/DesktopNotificationsManager.cpp: -------------------------------------------------------------------------------- 1 | #include "DesktopNotificationsManager.h" 2 | #include "DesktopNotification.h" 3 | #include "Utils.h" 4 | #include "DesktopNotificationsActionCenterActivator.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | using namespace Microsoft::WRL; 11 | using namespace Microsoft::WRL::Details; 12 | using namespace ABI::Windows::UI; 13 | using namespace ABI::Windows::UI::Notifications; 14 | using namespace ABI::Windows::Data::Xml::Dom; 15 | using namespace Windows::Foundation; 16 | using namespace Wrappers; 17 | 18 | DesktopNotificationsManager::DesktopNotificationsManager(const std::wstring &toastActivatorClsid, 19 | Napi::Function &callback) 20 | : m_ref(0), 21 | m_toastActivatorClsid(toastActivatorClsid), 22 | m_callback(Napi::ThreadSafeFunction::New(callback.Env(), callback, "Notification Callback", 0, 1)) 23 | { 24 | { 25 | HRESULT hr = Windows::Foundation::Initialize(RO_INIT_MULTITHREADED); 26 | if (!SUCCEEDED(hr)) 27 | { 28 | DN_LOG_ERROR(L"Failed to initialize with RO_INIT_MULTITHREADED: " << hr); 29 | } 30 | } 31 | { 32 | HRESULT hr = GetActivationFactory( 33 | HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager) 34 | .Get(), 35 | &m_toastManager); 36 | if (!SUCCEEDED(hr)) 37 | { 38 | DN_LOG_ERROR(L"Failed to register com Factory, please make sure you " 39 | L"correctly initialized with RO_INIT_MULTITHREADED: " 40 | << hr); 41 | } 42 | } 43 | 44 | if (char *envAppID = std::getenv("DN_APP_ID")) 45 | { 46 | DN_LOG_INFO(L"Using custom App User Model ID '" << envAppID << "'"); 47 | 48 | HRESULT hr = SetCurrentProcessExplicitAppUserModelID(Utils::utf8ToWideChar(std::string(envAppID))); 49 | if (!SUCCEEDED(hr)) 50 | { 51 | DN_LOG_ERROR(L"DesktopNotificationsManager: Failed to set AUMID"); 52 | return; 53 | } 54 | } 55 | 56 | { 57 | PWSTR appID; 58 | HRESULT hr = GetCurrentProcessExplicitAppUserModelID(&appID); 59 | if (!SUCCEEDED(hr)) 60 | { 61 | DN_LOG_ERROR(L"Couldn't retrieve AUMID"); 62 | return; 63 | } 64 | else 65 | { 66 | m_appID = std::wstring(appID); 67 | CoTaskMemFree(appID); 68 | } 69 | } 70 | 71 | RegisterClassObjects(m_toastActivatorClsid); 72 | } 73 | 74 | void DesktopNotificationsManager::SignalObjectCountZero() 75 | { 76 | // Do nothing 77 | } 78 | 79 | HRESULT DesktopNotificationsManager::RegisterClassObjects(const std::wstring &toastActivatorClsid) 80 | { 81 | // Create an out-of-proc COM module with caching disabled. The supplied 82 | // method is invoked when the last instance object of the module is released. 83 | auto &module = Module::Create( 84 | this, &DesktopNotificationsManager::SignalObjectCountZero); 85 | 86 | // Usually COM module classes statically define their CLSID at compile time 87 | // through the use of various macros, and WRL::Module internals takes care of 88 | // creating the class objects and registering them. However, we need to 89 | // register the same object with different CLSIDs depending on a runtime 90 | // setting, so we handle that logic here. 91 | 92 | ComPtr factory; 93 | unsigned int flags = ModuleType::OutOfProcDisableCaching; 94 | 95 | HRESULT hr = CreateClassFactory< 96 | SimpleClassFactory>( 97 | &flags, nullptr, __uuidof(IClassFactory), &factory); 98 | if (FAILED(hr)) 99 | { 100 | DN_LOG_ERROR("Failed to create Factory for Action Center activator; hr: " << hr); 101 | return hr; 102 | } 103 | 104 | ComPtr class_factory; 105 | hr = factory.As(&class_factory); 106 | if (FAILED(hr)) 107 | { 108 | DN_LOG_ERROR("Failed to create IClassFactory for Action Center activator; hr: " << hr); 109 | return hr; 110 | } 111 | 112 | CLSID activatorClsid; 113 | CLSIDFromString(toastActivatorClsid.c_str(), &activatorClsid); 114 | 115 | // All pointers in this array are unowned. Do not release them. 116 | IClassFactory *class_factories[] = {class_factory.Get()}; 117 | IID class_ids[] = {activatorClsid}; 118 | 119 | hr = module.RegisterCOMObject(nullptr, class_ids, class_factories, m_comCookies, 120 | std::extent()); 121 | if (FAILED(hr)) 122 | { 123 | DN_LOG_ERROR("Failed to register Action Center activator; hr: " << hr); 124 | } 125 | else 126 | { 127 | module.IncrementObjectCount(); 128 | } 129 | 130 | return hr; 131 | } 132 | 133 | DesktopNotificationsManager::~DesktopNotificationsManager() 134 | { 135 | m_callback.Release(); 136 | m_desktopNotifications.clear(); 137 | 138 | UnregisterClassObjects(); 139 | } 140 | 141 | HRESULT DesktopNotificationsManager::UnregisterClassObjects() 142 | { 143 | auto &module = Module::GetModule(); 144 | 145 | module.DecrementObjectCount(); 146 | 147 | HRESULT hr = module.UnregisterCOMObject(nullptr, m_comCookies, 148 | std::extent()); 149 | if (FAILED(hr)) 150 | { 151 | DN_LOG_ERROR("Failed to unregister Action Center activator; hr: " << hr); 152 | } 153 | 154 | return hr; 155 | } 156 | 157 | const std::string DesktopNotificationsManager::getCurrentPermission() 158 | { 159 | Microsoft::WRL::ComPtr notifier; 160 | if (!DN_CHECK_RESULT(m_toastManager->CreateToastNotifierWithId( 161 | HStringReference(m_appID.c_str()).Get(), ¬ifier))) 162 | { 163 | DN_LOG_ERROR("Failed to create a ToastNotifier to ensure your appId is registered"); 164 | return "default"; 165 | } 166 | 167 | NotificationSetting setting = NotificationSetting_Enabled; 168 | if (!DN_CHECK_RESULT(notifier->get_Setting(&setting))) 169 | { 170 | DN_LOG_ERROR("Failed to retreive NotificationSettings to ensure your appId is registered"); 171 | return "default"; 172 | } 173 | 174 | return (setting == NotificationSetting_Enabled ? "granted" : "denied"); 175 | } 176 | 177 | ComPtr DesktopNotificationsManager::getHistory() 178 | { 179 | ComPtr toastStatics2; 180 | if (DN_CHECK_RESULT(m_toastManager.As(&toastStatics2))) 181 | { 182 | ComPtr nativeHistory; 183 | DN_CHECK_RESULT(toastStatics2->get_History(&nativeHistory)); 184 | return nativeHistory; 185 | } 186 | return {}; 187 | } 188 | 189 | HRESULT DesktopNotificationsManager::displayToast(const std::wstring &id, 190 | const std::wstring &title, 191 | const std::wstring &body, 192 | const std::wstring &userInfo) 193 | { 194 | std::shared_ptr d = std::make_shared(id, m_appID, title, body, userInfo); 195 | m_desktopNotifications.push_back(d); 196 | return d->createToast(m_toastManager, this); 197 | } 198 | 199 | bool DesktopNotificationsManager::closeToast(const std::wstring &id) 200 | { 201 | // Iterate through m_desktopNotifications looking for a notification with 202 | // the given id, close that notification and remove it from the list. 203 | for (auto it = m_desktopNotifications.begin(); it != m_desktopNotifications.end(); ++it) 204 | { 205 | auto notification = *it; 206 | if (notification->getID() == id) 207 | { 208 | m_desktopNotifications.erase(it); 209 | return closeNotification(notification); 210 | } 211 | } 212 | 213 | return false; 214 | } 215 | 216 | void DesktopNotificationsManager::handleActivatorEvent(const std::wstring &launchArgs) 217 | { 218 | const auto notificationID = Utils::parseNotificationID(launchArgs); 219 | const auto userInfo = Utils::parseUserInfo(launchArgs); 220 | invokeJSCallback("click", notificationID, userInfo); 221 | } 222 | 223 | bool DesktopNotificationsManager::closeNotification(std::shared_ptr d) 224 | { 225 | if (auto history = getHistory()) 226 | { 227 | if (DN_CHECK_RESULT(history->RemoveGroupedTagWithId( 228 | HStringReference(d->getID().c_str()).Get(), HStringReference(DN_GROUP_NAME).Get(), 229 | HStringReference(m_appID.c_str()).Get()))) 230 | { 231 | return true; 232 | } 233 | } 234 | 235 | DN_LOG_ERROR("Notification " << d->getID() << " does not exist"); 236 | return false; 237 | } 238 | 239 | // DesktopToastActivatedEventHandler 240 | IFACEMETHODIMP DesktopNotificationsManager::Invoke(_In_ IToastNotification *sender, 241 | _In_ IInspectable *args) 242 | { 243 | IToastActivatedEventArgs *buttonReply = nullptr; 244 | args->QueryInterface(&buttonReply); 245 | if (buttonReply == nullptr) 246 | { 247 | DN_LOG_ERROR(L"args is not a IToastActivatedEventArgs"); 248 | return S_OK; 249 | } 250 | 251 | const auto notificationID = DesktopNotification::getNotificationIDFromToast(sender); 252 | const auto userInfo = DesktopNotification::getUserInfoFromToast(sender); 253 | invokeJSCallback("click", notificationID, userInfo); 254 | 255 | return S_OK; 256 | } 257 | 258 | // DesktopToastDismissedEventHandler 259 | IFACEMETHODIMP DesktopNotificationsManager::Invoke(_In_ IToastNotification *sender, 260 | _In_ IToastDismissedEventArgs *e) 261 | { 262 | const auto notificationID = DesktopNotification::getNotificationIDFromToast(sender); 263 | if (notificationID == "") 264 | { 265 | DN_LOG_ERROR(L"Could not get notification ID from toast"); 266 | return S_OK; 267 | } 268 | const auto userInfo = DesktopNotification::getUserInfoFromToast(sender); 269 | 270 | ToastDismissalReason tdr; 271 | HRESULT hr = e->get_Reason(&tdr); 272 | if (SUCCEEDED(hr)) 273 | { 274 | switch (tdr) 275 | { 276 | case ToastDismissalReason_ApplicationHidden: 277 | invokeJSCallback("hidden", notificationID, userInfo); 278 | break; 279 | case ToastDismissalReason_UserCanceled: 280 | invokeJSCallback("dismissed", notificationID, userInfo); 281 | break; 282 | case ToastDismissalReason_TimedOut: 283 | invokeJSCallback("timedout", notificationID, userInfo); 284 | break; 285 | } 286 | } 287 | 288 | return S_OK; 289 | } 290 | 291 | // DesktopToastFailedEventHandler 292 | IFACEMETHODIMP DesktopNotificationsManager::Invoke(_In_ IToastNotification *sender, 293 | _In_ IToastFailedEventArgs *e) 294 | { 295 | const auto notificationID = DesktopNotification::getNotificationIDFromToast(sender); 296 | if (notificationID == "") 297 | { 298 | DN_LOG_ERROR(L"Could not get notification ID from toast"); 299 | return S_OK; 300 | } 301 | const auto userInfo = DesktopNotification::getUserInfoFromToast(sender); 302 | 303 | DN_LOG_ERROR(L"The toast encountered an error."); 304 | invokeJSCallback("error", notificationID, userInfo); 305 | return S_OK; 306 | } 307 | 308 | void DesktopNotificationsManager::invokeJSCallback(const std::string &eventName, 309 | const std::string ¬ificationID, 310 | const std::wstring &userInfo) 311 | { 312 | auto cb = [=](Napi::Env env, Napi::Function jsCallback) 313 | { 314 | Napi::Value userInfoObject = env.Undefined(); 315 | if (userInfo != L"") 316 | { 317 | Napi::String userInfoString = Napi::String::New(env, (const char16_t *)userInfo.c_str()); 318 | userInfoObject = Utils::JSONParse(env, userInfoString); 319 | } 320 | jsCallback.Call({ 321 | Napi::String::New(env, eventName), 322 | Napi::String::New(env, notificationID), 323 | userInfoObject, 324 | }); 325 | }; 326 | 327 | m_callback.BlockingCall(cb); 328 | } 329 | 330 | std::shared_ptr desktopNotificationsManager = nullptr; 331 | -------------------------------------------------------------------------------- /src/win/DesktopNotificationsManager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | // Windows Header Files: 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | 23 | #include 24 | 25 | #include "DesktopNotification.h" 26 | 27 | using namespace Microsoft::WRL; 28 | using namespace ABI::Windows::Data::Xml::Dom; 29 | 30 | typedef ABI::Windows::Foundation::ITypedEventHandler< 31 | ABI::Windows::UI::Notifications::ToastNotification *, ::IInspectable *> 32 | DesktopToastActivatedEventHandler; 33 | typedef ABI::Windows::Foundation::ITypedEventHandler< 34 | ABI::Windows::UI::Notifications::ToastNotification *, 35 | ABI::Windows::UI::Notifications::ToastDismissedEventArgs *> 36 | DesktopToastDismissedEventHandler; 37 | typedef ABI::Windows::Foundation::ITypedEventHandler< 38 | ABI::Windows::UI::Notifications::ToastNotification *, 39 | ABI::Windows::UI::Notifications::ToastFailedEventArgs *> 40 | DesktopToastFailedEventHandler; 41 | 42 | class DesktopNotificationsManager : public Microsoft::WRL::Implements 45 | { 46 | friend class DesktopNotification; 47 | 48 | public: 49 | DesktopNotificationsManager(const std::wstring &toastActivatorClsid, 50 | Napi::Function &callback); 51 | ~DesktopNotificationsManager(); 52 | 53 | const std::string getCurrentPermission(); 54 | 55 | HRESULT displayToast(const std::wstring &id, 56 | const std::wstring &title, 57 | const std::wstring &body, 58 | const std::wstring &userInfo); 59 | bool closeToast(const std::wstring &id); 60 | 61 | void handleActivatorEvent(const std::wstring &launchArgs); 62 | 63 | // DesktopToastActivatedEventHandler 64 | IFACEMETHODIMP Invoke(_In_ ABI::Windows::UI::Notifications::IToastNotification *sender, 65 | _In_ IInspectable *args); 66 | 67 | // DesktopToastDismissedEventHandler 68 | IFACEMETHODIMP Invoke(_In_ ABI::Windows::UI::Notifications::IToastNotification *sender, 69 | _In_ ABI::Windows::UI::Notifications::IToastDismissedEventArgs *e); 70 | 71 | // DesktopToastFailedEventHandler 72 | IFACEMETHODIMP Invoke(_In_ ABI::Windows::UI::Notifications::IToastNotification *sender, 73 | _In_ ABI::Windows::UI::Notifications::IToastFailedEventArgs *e); 74 | 75 | // IUnknown 76 | IFACEMETHODIMP_(ULONG) 77 | AddRef() 78 | { 79 | return InterlockedIncrement(&m_ref); 80 | } 81 | 82 | IFACEMETHODIMP_(ULONG) 83 | Release() 84 | { 85 | ULONG l = InterlockedDecrement(&m_ref); 86 | if (l == 0) 87 | { 88 | delete this; 89 | } 90 | return l; 91 | } 92 | 93 | IFACEMETHODIMP QueryInterface(_In_ REFIID riid, _COM_Outptr_ void **ppv) 94 | { 95 | if (IsEqualIID(riid, IID_IUnknown)) 96 | { 97 | *ppv = static_cast(static_cast(this)); 98 | } 99 | else if (IsEqualIID(riid, __uuidof(DesktopToastActivatedEventHandler))) 100 | { 101 | *ppv = static_cast(this); 102 | } 103 | else if (IsEqualIID(riid, __uuidof(DesktopToastDismissedEventHandler))) 104 | { 105 | *ppv = static_cast(this); 106 | } 107 | else if (IsEqualIID(riid, __uuidof(DesktopToastFailedEventHandler))) 108 | { 109 | *ppv = static_cast(this); 110 | } 111 | else 112 | { 113 | *ppv = nullptr; 114 | } 115 | 116 | if (*ppv) 117 | { 118 | reinterpret_cast(*ppv)->AddRef(); 119 | return S_OK; 120 | } 121 | 122 | return E_NOINTERFACE; 123 | } 124 | 125 | private: 126 | Microsoft::WRL::ComPtr getHistory(); 127 | 128 | Microsoft::WRL::ComPtr m_toastManager; 129 | 130 | std::vector> m_desktopNotifications; 131 | 132 | void SignalObjectCountZero(); 133 | HRESULT RegisterClassObjects(const std::wstring &toastActivatorClsid); 134 | HRESULT UnregisterClassObjects(); 135 | bool closeNotification(std::shared_ptr d); 136 | void DesktopNotificationsManager::invokeJSCallback(const std::string &eventName, 137 | const std::string ¬ificationID, 138 | const std::wstring &userInfo); 139 | 140 | // Identifiers of registered class objects. Used for unregistration. 141 | DWORD m_comCookies[1] = {0}; 142 | const std::wstring m_toastActivatorClsid; 143 | Napi::ThreadSafeFunction m_callback; 144 | std::wstring m_appID; 145 | ULONG m_ref; 146 | }; 147 | 148 | extern std::shared_ptr desktopNotificationsManager; 149 | -------------------------------------------------------------------------------- /src/win/main_win.cc: -------------------------------------------------------------------------------- 1 | // Windows.h strict mode 2 | #define STRICT 3 | 4 | #include 5 | #include "uv.h" 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "DesktopNotificationsManager.h" 15 | #include "Utils.h" 16 | 17 | using namespace Napi; 18 | 19 | namespace 20 | { 21 | Napi::Value initializeNotifications(const Napi::CallbackInfo &info) 22 | { 23 | Napi::Env &env = info.Env(); 24 | 25 | if (desktopNotificationsManager) 26 | { 27 | return env.Undefined(); 28 | } 29 | 30 | if (info.Length() < 2) 31 | { 32 | Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException(); 33 | return env.Undefined(); 34 | } 35 | 36 | if (!info[0].IsFunction()) 37 | { 38 | Napi::TypeError::New(env, "Callback must be a function.").ThrowAsJavaScriptException(); 39 | return env.Undefined(); 40 | } 41 | 42 | if (!info[1].IsObject()) 43 | { 44 | Napi::TypeError::New(env, "An object was expected for the second argument, but wasn't received.").ThrowAsJavaScriptException(); 45 | return env.Undefined(); 46 | } 47 | 48 | auto callback = info[0].As(); 49 | 50 | Napi::Object options = info[1].As(); 51 | if (!options.Has("toastActivatorClsid")) 52 | { 53 | Napi::TypeError::New(env, "The options object must have the \"toastActivatorClsid\" property.").ThrowAsJavaScriptException(); 54 | return env.Undefined(); 55 | } 56 | 57 | auto toastActivatorClsid = Utils::utf8ToWideChar(options.Get("toastActivatorClsid").As()); 58 | 59 | desktopNotificationsManager = std::make_shared(toastActivatorClsid, callback); 60 | 61 | return info.Env().Undefined(); 62 | } 63 | 64 | Napi::Value terminateNotifications(const Napi::CallbackInfo &info) 65 | { 66 | desktopNotificationsManager = nullptr; 67 | return info.Env().Undefined(); 68 | } 69 | 70 | Napi::Value showNotification(const Napi::CallbackInfo &info) 71 | { 72 | Napi::Env &env = info.Env(); 73 | 74 | if (!desktopNotificationsManager) 75 | { 76 | DN_LOG_ERROR("Cannot show notification: notifications not initialized."); 77 | return env.Undefined(); 78 | } 79 | 80 | if (info.Length() < 3) 81 | { 82 | Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException(); 83 | return env.Undefined(); 84 | } 85 | 86 | if (!info[0].IsString()) 87 | { 88 | Napi::TypeError::New(env, "A string was expected for the first argument, but wasn't received.").ThrowAsJavaScriptException(); 89 | return env.Undefined(); 90 | } 91 | 92 | if (!info[1].IsString()) 93 | { 94 | Napi::TypeError::New(env, "A string was expected for the second argument, but wasn't received.").ThrowAsJavaScriptException(); 95 | return env.Undefined(); 96 | } 97 | 98 | if (!info[2].IsString()) 99 | { 100 | Napi::TypeError::New(env, "A string was expected for the third argument, but wasn't received.").ThrowAsJavaScriptException(); 101 | return env.Undefined(); 102 | } 103 | 104 | auto id = Utils::utf8ToWideChar(info[0].As()); 105 | auto title = Utils::utf8ToWideChar(info[1].As()); 106 | auto body = Utils::utf8ToWideChar(info[2].As()); 107 | auto userInfo = L""; 108 | 109 | if (info[3].IsObject()) 110 | { 111 | auto userInfoObject = info[3].As(); 112 | auto userInfoString = Utils::JSONStringify(env, userInfoObject); 113 | userInfo = Utils::utf8ToWideChar(userInfoString); 114 | } 115 | 116 | desktopNotificationsManager->displayToast(id, title, body, userInfo); 117 | 118 | Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env); 119 | deferred.Resolve(env.Undefined()); 120 | return deferred.Promise(); 121 | } 122 | 123 | Napi::Value closeNotification(const Napi::CallbackInfo &info) 124 | { 125 | Napi::Env &env = info.Env(); 126 | 127 | if (!desktopNotificationsManager) 128 | { 129 | DN_LOG_ERROR("Cannot close notification: notifications not initialized."); 130 | return env.Undefined(); 131 | } 132 | 133 | if (info.Length() < 1) 134 | { 135 | Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException(); 136 | return env.Undefined(); 137 | } 138 | 139 | if (!info[0].IsString()) 140 | { 141 | Napi::TypeError::New(env, "A string was expected for the first argument, but wasn't received.").ThrowAsJavaScriptException(); 142 | return env.Undefined(); 143 | } 144 | 145 | auto id = Utils::utf8ToWideChar(info[0].As()); 146 | 147 | desktopNotificationsManager->closeToast(id); 148 | 149 | return env.Undefined(); 150 | } 151 | 152 | Napi::Value getNotificationsPermission(const Napi::CallbackInfo &info) 153 | { 154 | Napi::Env &env = info.Env(); 155 | 156 | Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env); 157 | 158 | if (!desktopNotificationsManager) 159 | { 160 | DN_LOG_ERROR("Cannot show notification: notifications not initialized."); 161 | deferred.Resolve(Napi::String::New(env, "default")); 162 | } 163 | else 164 | { 165 | const std::string permission = desktopNotificationsManager->getCurrentPermission(); 166 | deferred.Resolve(Napi::String::New(env, permission)); 167 | } 168 | 169 | return deferred.Promise(); 170 | } 171 | 172 | Napi::Value requestNotificationsPermission(const Napi::CallbackInfo &info) 173 | { 174 | Napi::Env &env = info.Env(); 175 | 176 | // Do nothing. There is no way of requesting permission on Windows. 177 | Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env); 178 | deferred.Resolve(Napi::Boolean::New(env, true)); 179 | return deferred.Promise(); 180 | } 181 | 182 | Napi::Object Init(Napi::Env env, Napi::Object exports) 183 | { 184 | exports.Set(Napi::String::New(env, "initializeNotifications"), Napi::Function::New(env, initializeNotifications)); 185 | exports.Set(Napi::String::New(env, "terminateNotifications"), Napi::Function::New(env, terminateNotifications)); 186 | exports.Set(Napi::String::New(env, "showNotification"), Napi::Function::New(env, showNotification)); 187 | exports.Set(Napi::String::New(env, "closeNotification"), Napi::Function::New(env, closeNotification)); 188 | exports.Set(Napi::String::New(env, "getNotificationsPermission"), Napi::Function::New(env, getNotificationsPermission)); 189 | exports.Set(Napi::String::New(env, "requestNotificationsPermission"), Napi::Function::New(env, requestNotificationsPermission)); 190 | 191 | return exports; 192 | } 193 | } 194 | 195 | NODE_API_MODULE(desktopNotificationsNativeModule, Init); 196 | -------------------------------------------------------------------------------- /src/win/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "Utils.h" 2 | #include "DesktopNotificationsManager.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | using namespace Microsoft::WRL; 10 | 11 | namespace Utils 12 | { 13 | LPWSTR Utils::utf8ToWideChar(std::string utf8) 14 | { 15 | int wide_char_length = MultiByteToWideChar(CP_UTF8, 16 | 0, 17 | utf8.c_str(), 18 | -1, 19 | nullptr, 20 | 0); 21 | if (wide_char_length == 0) 22 | { 23 | return nullptr; 24 | } 25 | 26 | LPWSTR result = new WCHAR[wide_char_length]; 27 | if (MultiByteToWideChar(CP_UTF8, 28 | 0, 29 | utf8.c_str(), 30 | -1, 31 | result, 32 | wide_char_length) == 0) 33 | { 34 | delete[] result; 35 | return nullptr; 36 | } 37 | 38 | return result; 39 | } 40 | 41 | std::string Utils::wideCharToUTF8(std::wstring wstr) 42 | { 43 | if (wstr.empty()) 44 | { 45 | return std::string(); 46 | } 47 | int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), NULL, 0, NULL, NULL); 48 | std::string strTo(size_needed, 0); 49 | WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &strTo[0], size_needed, NULL, NULL); 50 | return strTo; 51 | } 52 | 53 | std::unordered_map splitData(const std::wstring &data) 54 | { 55 | std::unordered_map out; 56 | size_t start = 0; 57 | for (size_t end = data.find(L";", start); end != std::wstring::npos; 58 | start = end + 1, end = data.find(L";", start)) 59 | { 60 | if (start == end) 61 | { 62 | end = data.size(); 63 | } 64 | const std::wstring tmp(data.data() + start, end - start); 65 | const auto pos = tmp.find(L"="); 66 | if (pos > 0) 67 | { 68 | out[tmp.substr(0, pos)] = tmp.substr(pos + 1); 69 | } 70 | } 71 | return out; 72 | } 73 | 74 | std::wstring formatLaunchArgs(const std::wstring ¬ificationID, const std::wstring &userInfo) 75 | { 76 | std::wstringstream out; 77 | out << notificationID << L";" << userInfo; 78 | return out.str(); 79 | } 80 | 81 | std::string parseNotificationID(const std::wstring &launchArgs) 82 | { 83 | std::wstringstream out; 84 | size_t end = launchArgs.find(L";"); 85 | if (end == std::wstring::npos) 86 | { 87 | return ""; 88 | } 89 | 90 | return wideCharToUTF8(launchArgs.substr(0, end)); 91 | } 92 | 93 | std::wstring parseUserInfo(const std::wstring &launchArgs) 94 | { 95 | std::wstringstream out; 96 | size_t start = launchArgs.find(L";"); 97 | if (start == std::wstring::npos) 98 | { 99 | return L""; 100 | } 101 | 102 | return launchArgs.substr(start + 1); 103 | } 104 | 105 | std::wstring formatWinError(unsigned long errorCode) 106 | { 107 | wchar_t *error = nullptr; 108 | size_t len = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, 109 | nullptr, errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), 110 | reinterpret_cast(&error), 0, nullptr); 111 | const auto out = std::wstring(error, len); 112 | LocalFree(error); 113 | return out; 114 | } 115 | 116 | Napi::String JSONStringify(const Napi::Env &env, const Napi::Object &object) 117 | { 118 | Napi::Object json = env.Global().Get("JSON").As(); 119 | Napi::Function stringify = json.Get("stringify").As(); 120 | Napi::String result = Napi::String::New(env, ""); 121 | 122 | try 123 | { 124 | result = stringify.Call(json, {object}).As(); 125 | } 126 | catch (...) 127 | { 128 | DN_LOG_ERROR("Failed to stringify JSON object"); 129 | } 130 | 131 | return result; 132 | } 133 | 134 | Napi::Value JSONParse(const Napi::Env &env, const Napi::String &string) 135 | { 136 | Napi::Object json = env.Global().Get("JSON").As(); 137 | Napi::Function parse = json.Get("parse").As(); 138 | Napi::Value result = env.Undefined(); 139 | 140 | try 141 | { 142 | result = parse.Call(json, {string}).As(); 143 | } 144 | catch (...) 145 | { 146 | DN_LOG_ERROR("Failed to parse JSON"); 147 | } 148 | 149 | return result; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/win/utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define DN_LOG(stream, level, msg) stream << L"[desktop-notifications] [" << level << L"] " << msg << std::endl 10 | 11 | #define DN_LOG_VERBOSE(msg) DN_LOG(std::wcout, "verbose", msg) 12 | #define DN_LOG_INFO(msg) DN_LOG(std::wcout, "info", msg) 13 | #define DN_LOG_DEBUG(msg) DN_LOG(std::wcout, "debug", msg) 14 | #define DN_LOG_WARN(msg) DN_LOG(std::wcout, "warning", msg) 15 | #define DN_LOG_ERROR(msg) DN_LOG(std::wcerr, "error", msg) 16 | 17 | namespace Utils 18 | { 19 | LPWSTR utf8ToWideChar(std::string utf8); 20 | std::string wideCharToUTF8(std::wstring wstr); 21 | 22 | std::unordered_map splitData(const std::wstring &data); 23 | 24 | // Formats the launch args like this: ; 25 | std::wstring formatLaunchArgs(const std::wstring ¬ificationID, const std::wstring &userInfo); 26 | std::string parseNotificationID(const std::wstring &launchArgs); 27 | std::wstring parseUserInfo(const std::wstring &launchArgs); 28 | 29 | inline bool checkResult(const char *file, const long line, const char *func, const HRESULT &hr) 30 | { 31 | if (FAILED(hr)) 32 | { 33 | DN_LOG_ERROR(file << line << func << L":\n\t\t\t" << hr); 34 | return false; 35 | } 36 | return true; 37 | } 38 | 39 | std::wstring formatWinError(unsigned long errorCode); 40 | 41 | Napi::String JSONStringify(const Napi::Env &env, const Napi::Object &object); 42 | Napi::Value JSONParse(const Napi::Env &env, const Napi::String &string); 43 | }; 44 | 45 | #define DN_GROUP_NAME L"desktop-notifications" 46 | 47 | #define DN_CHECK_RESULT(hr) Utils::checkResult(__FILE__, __LINE__, __FUNCSIG__, hr) 48 | 49 | #define DN_RETURN_ON_ERROR(hr) \ 50 | do \ 51 | { \ 52 | HRESULT _tmp = hr; \ 53 | if (!DN_CHECK_RESULT(_tmp)) \ 54 | { \ 55 | return _tmp; \ 56 | } \ 57 | } while (false) 58 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "noImplicitReturns": true, 8 | "noImplicitThis": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "noUnusedLocals": true, 11 | "strict": true, 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "declaration": true, 15 | "types": ["jest", "node"], 16 | "typeRoots": ["./node_modules/@types"] 17 | }, 18 | "exclude": ["node_modules", "build"], 19 | "include": ["lib/**/*.ts", "test/**/*.ts"], 20 | "compileOnSave": false 21 | } 22 | --------------------------------------------------------------------------------