Repository: Nickersoft/push.js
Branch: master
Commit: f799843c9bc5
Files: 39
Total size: 90.8 KB
Directory structure:
gitextract_6ktotom7/
├── .babelrc
├── .browserslistrc
├── .codeclimate.yml
├── .csslintrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.yml
├── .flowconfig
├── .gitignore
├── .npmignore
├── .nvmrc
├── .prettierrc
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── bin/
│ └── push.js
├── bower.json
├── index.d.ts
├── package.json
├── rollup.config.js
├── src/
│ ├── agents/
│ │ ├── AbstractAgent.js
│ │ ├── DesktopAgent.js
│ │ ├── MSAgent.js
│ │ ├── MobileChromeAgent.js
│ │ ├── MobileFirefoxAgent.js
│ │ ├── WebKitAgent.js
│ │ └── index.js
│ ├── index.js
│ ├── push/
│ │ ├── Messages.js
│ │ ├── Permission.js
│ │ ├── Push.js
│ │ ├── Util.js
│ │ └── index.js
│ ├── serviceWorker.js
│ └── types.js
└── tests/
├── browsers.conf.js
├── karma.conf.js
└── push.tests.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
],
"@babel/preset-flow"
],
"plugins": [
"@babel/plugin-transform-flow-strip-types",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-syntax-import-meta",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-json-strings",
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
"@babel/plugin-proposal-function-sent",
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-throw-expressions"
]
}
================================================
FILE: .browserslistrc
================================================
Firefox >= 22
Chrome >= 5
Safari >= 6
Opera >= 25
Android >= 4.4
Blackberry >= 10
OperaMobile >= 37
FirefoxAndroid >= 47
Samsung >= 4
Explorer >= 9
================================================
FILE: .codeclimate.yml
================================================
---
engines:
csslint:
enabled: true
duplication:
enabled: true
config:
languages:
- ruby
- javascript
- python
- php
eslint:
enabled: true
fixme:
enabled: true
ratings:
paths:
- "**.css"
- "**.inc"
- "**.js"
- "**.jsx"
- "**.module"
- "**.php"
- "**.py"
- "**.rb"
exclude_paths:
- node_modules/
- tests/
- bin/
================================================
FILE: .csslintrc
================================================
--exclude-exts=.min.css
--ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes
================================================
FILE: .editorconfig
================================================
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
tab_width = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
================================================
FILE: .eslintignore
================================================
**/*{.,-}min.js
================================================
FILE: .eslintrc.yml
================================================
---
parserOptions:
sourceType: module
ecmaFeatures:
jsx: true
env:
amd: true
browser: true
es6: true
jquery: true
node: true
# http://eslint.org/docs/rules/
rules:
# Possible Errors
no-await-in-loop: off
no-cond-assign: error
no-console: off
no-constant-condition: error
no-control-regex: error
no-debugger: error
no-dupe-args: error
no-dupe-keys: error
no-duplicate-case: error
no-empty-character-class: error
no-empty: error
no-ex-assign: error
no-extra-boolean-cast: error
no-extra-parens: off
no-extra-semi: error
no-func-assign: error
no-inner-declarations:
- error
- functions
no-invalid-regexp: error
no-irregular-whitespace: error
no-negated-in-lhs: error
no-obj-calls: error
no-prototype-builtins: off
no-regex-spaces: error
no-sparse-arrays: error
no-template-curly-in-string: off
no-unexpected-multiline: error
no-unreachable: error
no-unsafe-finally: off
no-unsafe-negation: off
use-isnan: error
valid-jsdoc: off
valid-typeof: error
# Best Practices
accessor-pairs: error
array-callback-return: off
block-scoped-var: off
class-methods-use-this: off
complexity:
- error
- 6
consistent-return: off
curly: off
default-case: off
dot-location: off
dot-notation: off
eqeqeq: error
guard-for-in: error
no-alert: error
no-caller: error
no-case-declarations: error
no-div-regex: error
no-else-return: off
no-empty-function: off
no-empty-pattern: error
no-eq-null: error
no-eval: error
no-extend-native: error
no-extra-bind: error
no-extra-label: off
no-fallthrough: error
no-floating-decimal: off
no-global-assign: off
no-implicit-coercion: off
no-implied-eval: error
no-invalid-this: off
no-iterator: error
no-labels:
- error
- allowLoop: true
allowSwitch: true
no-lone-blocks: error
no-loop-func: error
no-magic-number: off
no-multi-spaces: off
no-multi-str: off
no-native-reassign: error
no-new-func: error
no-new-wrappers: error
no-new: error
no-octal-escape: error
no-octal: error
no-param-reassign: off
no-proto: error
no-redeclare: error
no-restricted-properties: off
no-return-assign: error
no-return-await: off
no-script-url: error
no-self-assign: off
no-self-compare: error
no-sequences: off
no-throw-literal: off
no-unmodified-loop-condition: off
no-unused-expressions: error
no-unused-labels: off
no-useless-call: error
no-useless-concat: error
no-useless-escape: off
no-useless-return: off
no-void: error
no-warning-comments: off
no-with: error
prefer-promise-reject-errors: off
radix: error
require-await: off
vars-on-top: off
wrap-iife: error
yoda: off
# Strict
strict: off
# Variables
init-declarations: off
no-catch-shadow: error
no-delete-var: error
no-label-var: error
no-restricted-globals: off
no-shadow-restricted-names: error
no-shadow: off
no-undef-init: error
no-undef: off
no-undefined: off
no-unused-vars: off
no-use-before-define: off
# Node.js and CommonJS
callback-return: error
global-require: error
handle-callback-err: error
no-mixed-requires: off
no-new-require: off
no-path-concat: error
no-process-env: off
no-process-exit: error
no-restricted-modules: off
no-sync: off
# Stylistic Issues
array-bracket-spacing: off
block-spacing: off
brace-style: off
camelcase: off
capitalized-comments: off
comma-dangle:
- error
- never
comma-spacing: off
comma-style: off
computed-property-spacing: off
consistent-this: off
eol-last: off
func-call-spacing: off
func-name-matching: off
func-names: off
func-style: off
id-length: off
id-match: off
indent: off
jsx-quotes: off
key-spacing: off
keyword-spacing: off
line-comment-position: off
linebreak-style: off
lines-around-comment: off
lines-around-directive: off
max-depth: off
max-len: off
max-nested-callbacks: off
max-params: off
max-statements-per-line: off
max-statements:
- error
- 30
multiline-ternary: off
new-cap: off
new-parens: off
newline-after-var: off
newline-before-return: off
newline-per-chained-call: off
no-array-constructor: off
no-bitwise: off
no-continue: off
no-inline-comments: off
no-lonely-if: off
no-mixed-operators: off
no-mixed-spaces-and-tabs: off
no-multi-assign: off
no-multiple-empty-lines: off
no-negated-condition: off
no-nested-ternary: off
no-new-object: off
no-plusplus: off
no-restricted-syntax: off
no-spaced-func: off
no-tabs: off
no-ternary: off
no-trailing-spaces: off
no-underscore-dangle: off
no-unneeded-ternary: off
object-curly-newline: off
object-curly-spacing: off
object-property-newline: off
one-var-declaration-per-line: off
one-var: off
operator-assignment: off
operator-linebreak: off
padded-blocks: off
quote-props: off
quotes: off
require-jsdoc: off
semi-spacing: off
semi: off
sort-keys: off
sort-vars: off
space-before-blocks: off
space-before-function-paren: off
space-in-parens: off
space-infix-ops: off
space-unary-ops: off
spaced-comment: off
template-tag-spacing: off
unicode-bom: off
wrap-regex: off
# ECMAScript 6
arrow-body-style: off
arrow-parens: off
arrow-spacing: off
constructor-super: off
generator-star-spacing: off
no-class-assign: off
no-confusing-arrow: off
no-const-assign: off
no-dupe-class-members: off
no-duplicate-imports: off
no-new-symbol: off
no-restricted-imports: off
no-this-before-super: off
no-useless-computed-key: off
no-useless-constructor: off
no-useless-rename: off
no-var: off
object-shorthand: off
prefer-arrow-callback: off
prefer-const: off
prefer-destructuring: off
prefer-numeric-literals: off
prefer-rest-params: off
prefer-reflect: off
prefer-spread: off
prefer-template: off
require-yield: off
rest-spread-spacing: off
sort-imports: off
symbol-description: off
template-curly-spacing: off
yield-star-spacing: off
================================================
FILE: .flowconfig
================================================
[ignore]
.*/node_modules/.*
[include]
.*/src/.*
[options]
# Add emojis to status messages because emojis are cool 😎
emoji=true
# Enabled strict argument types
experimental.strict_type_args=true
# Only run flow on the following file extensions
module.file_ext=.js
module.name_mapper='^push$' -> '<PROJECT_ROOT>/src/push/'
module.name_mapper='^types$' -> '<PROJECT_ROOT>/src/types.js'
module.name_mapper='^agents$' -> '<PROJECT_ROOT>/src/agents'
[version]
^0.57.3
================================================
FILE: .gitignore
================================================
# Created by https://www.gitignore.io
### OSX ###
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Node ###
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
dist
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
.idea
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
# Sensitive or high-churn files:
.idea/dataSources/
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
================================================
FILE: .npmignore
================================================
.*
================================================
FILE: .nvmrc
================================================
v8.12
================================================
FILE: .prettierrc
================================================
singleQuote: true
semi: true
useTabs: false
tabWidth: 4
printWidth: 80
================================================
FILE: .travis.yml
================================================
env:
global:
- CC_TEST_REPORTER_ID=71e04b5c32896d7201c79e2db2af1a89fc375f668392802961019875e71b179f
- GIT_COMMITTED_AT=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then git log -1 --pretty=format:%ct;
else git log -1 --skip 1 --pretty=format:%ct; fi)
- secure: AwJJp7im+vXzxa/UsYu/EJNppu+ss9l0GW8ftaXLNnMRkLJyo6m24DCJuhfH/ynxrSq5b2Zc7gEqDfxVWUgpNR+rrHZoHe+R/QnL60sqQXE2KvblDvG4o4e9F7vFp1xsohJ3/TTm6footWEiVlP25oVkQgi/oWhFsygJD6VGerDa2CodtU2r4p6UV5KuI8mUZuQg+rndkosMZa1BkZcz6v8e1AmJkGiIl/Agw0ye6C9iav4KoU+EXRyoxEq+dTAuhnVKA3CntOngoPDrUTQy5303x6Gzaz7uByWIyIR+uEud65RvCBduxCgREazkXdCqd/vCS65gYYktxl3fNG2so5VpKAbF/pTOylWexB10xhB+k+alIxhl3QytUx1pqDR4WI6c8d6ot+sZd7AjwvlyWdwDPuLoDB2eA18K3HbFMgjONmJFFI3gyKfyg1z5FfZm6dogfuQGZeSyxuedDAo+FygKGbgvBa+JHerR1WjU+TnFcVpwgaX/sma8Q4ff9WYLw2YOIiSi0H5V5tDi8lrtOqZmIYH4Vv2Cbl1gaAsVOOo+IBfTWor4oJzAb/jRKuNbqvhUJIqW6RYS7f8c/LMrcdn+LWHGj3zcFQ+LNFxID4OCghf7A3FvmE8A5XD07w04ofnWzrgqzgWy5+38Qj23l5IpFNgD2XGNohk7JpeGm3E=
language: node_js
node_js:
- lts/*
before_install: yarn global add greenkeeper-lockfile@1
before_script:
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64
> ./cc-test-reporter
- chmod +x ./cc-test-reporter
- ./cc-test-reporter before-build
- greenkeeper-lockfile-update
script:
- npm run test
after_script:
- if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then ./cc-test-reporter after-build --exit-code
$TRAVIS_TEST_RESULT; fi
- greenkeeper-lockfile-upload
- cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing Guidelines
So you want to contribute to Push, huh? Well lucky for you, it's really easy to do so, because you're just dealing with like, a few hundred lines of JavaScript. It's not hard.
Alright. Now calm down and take a few deep breaths. Here we go. All you have to remember is two commands... think you can do that?
To BUILD Push, just run:
```bash
$ npm run build
```
To TEST Push on BrowserStack, run:
```bash
$ npm run test
```
See? Not hard at all. Unfortunately the Notifications API doesn't always play nicely with local sites, so don't get discouraged if you try running Push in a local HTML file and it doesn't work.
To TEST Push on a specific, locally-installed browser, you can run one of the following:
```bash
$ npm run test:opera
$ npm run test:firefox
$ npm run test:chrome
$ npm run test:safari
```
### Testing & Travis
Push uses the [Karma](https://karma-runner.github.io/1.0/index.html) JavaScript test runner, so read up on that if you want to make changes to any of the tests that are run. These tests are run post-push by [Travis CI](https://travis-ci.org), so look into that if you want to make any Travis configuration changes. Although, at this point I'd say Travis is all set. The tests might want to be expanded though.
### REAL IMPORTANT STUFF
**THERE IS ONLY ONE RULE TO PUSH CLUB** (and no, it's not that you can't talk about it). **WHENEVER** you make changes to `Push.js`, **RECOMPILE** and commit `push.min.js` as well. Until this build process can be wrapped into a sexy git hook of some sort, this is how changes to the library need to occur. **YOUR PR WILL NOT BE APPROVED UNLESS THIS HAPPENS**. That said, I did let it slide once because I wasn't thinking, but that's why I wrote this file to make sure it will never happen again.
Outside of that, contributing should not be at all scary and should be a fun and positive process. Now go out and write some killer JS! Wait... is there even such a thing?
================================================
FILE: LICENSE.md
================================================
# The MIT License (MIT)
Copyright (c) 2016 Tyler Nickerson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<div align="center">
[<img src="https://raw.githubusercontent.com/Nickersoft/push.js/master/logo.png" width="250">](http://pushjs.org)
<br/><br/>
[](https://travis-ci.org/Nickersoft/push.js)
[](https://coveralls.io/github/Nickersoft/push.js?branch=master)
[](https://snyk.io/test/github/nickersoft/push.js)
[](https://codeclimate.com/github/Nickersoft/push.js/maintainability)
[](https://npmjs.com/package/push.js)
[](https://npmjs.com/package/push.js)
[](https://greenkeeper.io/)
*Now a proud user of*
[<img src="https://raw.githubusercontent.com/Nickersoft/push.js/master/browserstack.png" width="200px" />](https://browserstack.com)
</div>
> ## Important Notice
> Push is currently looking for co-maintainers of the repo. The guy who originally made this library, [Tyler Nickerson](https://tylernickerson.com), while still visiting this repo from time to time, is busy trying to work on his company [Linguistic](https://github.com/linguistic) right now. As a result, he may not have time to answer everyone or fix bugs as quickly as they would like him too. If you find it pretty easy to find your way around this code and think you could help some people out, shoot me a message at [nickersoft@gmail.com](mailto:nickersoft@gmail.com) and let's talk.
### What is Push? ###
Push is the fastest way to get up and running with Javascript desktop notifications. A fairly new addition to the
official specification, the Notification API allows modern browsers such as Chrome, Safari, Firefox, and IE 9+ to push
notifications to a user's desktop. Push acts as a cross-browser solution to this API, falling back to use older
implementations if the user's browser does not support the new API.
You can quickly install Push via [npm](http://npmjs.com):
```
npm install push.js --save
```
Or, if you want something a little more lightweight, you can give [Bower](http://bower.io) a try:
```
bower install push.js --save
```
### Full Documentation ###
Full documentation for Push can be found at the project's new homepage [https://pushjs.org](https://pushjs.org).
See you there!
### Development ###
If you feel like this library is your jam and you want to contribute (or you think I'm an idiot who missed something),
check out Push's neat [contributing guidelines](CONTRIBUTING.md) on how you can make your mark.
### Credits ###
Push is based off the following work:
1. [HTML5-Desktop-Notifications](https://github.com/ttsvetko/HTML5-Desktop-Notifications) by [Tsvetan Tsvetkov](https://github.com/ttsvetko)
2. [notify.js](https://github.com/alexgibson/notify.js) by [Alex Gibson](https://github.com/alexgibson)
================================================
FILE: bin/push.js
================================================
/**
* @license
*
* Push v1.0.9
* =========
* A compact, cross-browser solution for the JavaScript Notifications API
*
* Credits
* -------
* Tsvetan Tsvetkov (ttsvetko)
* Alex Gibson (alexgibson)
*
* License
* -------
*
* The MIT License (MIT)
*
* Copyright (c) 2015-2017 Tyler Nickerson
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
!function(i,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(i=i||self).Push=t()}(this,function(){"use strict";var i={errors:{incompatible:"".concat("PushError:"," Push.js is incompatible with browser."),invalid_plugin:"".concat("PushError:"," plugin class missing from plugin manifest (invalid plugin). Please check the documentation."),invalid_title:"".concat("PushError:"," title of notification must be a string"),permission_denied:"".concat("PushError:"," permission request declined"),sw_notification_error:"".concat("PushError:"," could not show a ServiceWorker notification due to the following reason: "),sw_registration_error:"".concat("PushError:"," could not register the ServiceWorker due to the following reason: "),unknown_interface:"".concat("PushError:"," unable to create notification: unknown interface")}};function t(i){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(i){return typeof i}:function(i){return i&&"function"==typeof Symbol&&i.constructor===Symbol&&i!==Symbol.prototype?"symbol":typeof i})(i)}function n(i,t){if(!(i instanceof t))throw new TypeError("Cannot call a class as a function")}function e(i,t){for(var n=0;n<t.length;n++){var e=t[n];e.enumerable=e.enumerable||!1,e.configurable=!0,"value"in e&&(e.writable=!0),Object.defineProperty(i,e.key,e)}}function o(i,t,n){return t&&e(i.prototype,t),n&&e(i,n),i}function r(i,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");i.prototype=Object.create(t&&t.prototype,{constructor:{value:i,writable:!0,configurable:!0}}),t&&c(i,t)}function s(i){return(s=Object.setPrototypeOf?Object.getPrototypeOf:function(i){return i.__proto__||Object.getPrototypeOf(i)})(i)}function c(i,t){return(c=Object.setPrototypeOf||function(i,t){return i.__proto__=t,i})(i,t)}function a(i,t){return!t||"object"!=typeof t&&"function"!=typeof t?function(i){if(void 0===i)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return i}(i):t}var u=function(){function i(t){n(this,i),this._win=t,this.GRANTED="granted",this.DEFAULT="default",this.DENIED="denied",this._permissions=[this.GRANTED,this.DEFAULT,this.DENIED]}return o(i,[{key:"request",value:function(i,t){return arguments.length>0?this._requestWithCallback.apply(this,arguments):this._requestAsPromise()}},{key:"_requestWithCallback",value:function(i,t){var n,e=this,o=this.get(),r=!1,s=function(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:e._win.Notification.permission;r||(r=!0,void 0===n&&e._win.webkitNotifications&&(n=e._win.webkitNotifications.checkPermission()),n===e.GRANTED||0===n?i&&i():t&&t())};o!==this.DEFAULT?s(o):this._win.webkitNotifications&&this._win.webkitNotifications.checkPermission?this._win.webkitNotifications.requestPermission(s):this._win.Notification&&this._win.Notification.requestPermission?(n=this._win.Notification.requestPermission(s))&&n.then&&n.then(s).catch(function(){t&&t()}):i&&i()}},{key:"_requestAsPromise",value:function(){var i=this,t=this.get(),n=t!==this.DEFAULT,e=this._win.Notification&&this._win.Notification.requestPermission,o=this._win.webkitNotifications&&this._win.webkitNotifications.checkPermission;return new Promise(function(r,s){var c,a=!1,u=function(t){a||(a=!0,!function(t){return t===i.GRANTED||0===t}(t)?s():r())};n?u(t):o?i._win.webkitNotifications.requestPermission(function(i){u(i)}):e?(c=i._win.Notification.requestPermission(u))&&c.then&&c.then(u).catch(s):r()})}},{key:"has",value:function(){return this.get()===this.GRANTED}},{key:"get",value:function(){return this._win.Notification&&this._win.Notification.permission?this._win.Notification.permission:this._win.webkitNotifications&&this._win.webkitNotifications.checkPermission?this._permissions[this._win.webkitNotifications.checkPermission()]:navigator.mozNotification?this.GRANTED:this._win.external&&this._win.external.msIsSiteMode?this._win.external.msIsSiteMode()?this.GRANTED:this.DEFAULT:this.GRANTED}}]),i}(),f=function(){function i(){n(this,i)}return o(i,null,[{key:"isUndefined",value:function(i){return void 0===i}},{key:"isNull",value:function(i){return null===obj}},{key:"isString",value:function(i){return"string"==typeof i}},{key:"isFunction",value:function(i){return i&&"[object Function]"==={}.toString.call(i)}},{key:"isObject",value:function(i){return"object"===t(i)}},{key:"objectMerge",value:function(i,t){for(var n in t)i.hasOwnProperty(n)&&this.isObject(i[n])&&this.isObject(t[n])?this.objectMerge(i[n],t[n]):i[n]=t[n]}}]),i}(),l=function i(t){n(this,i),this._win=t},h=function(i){function t(){return n(this,t),a(this,s(t).apply(this,arguments))}return r(t,l),o(t,[{key:"isSupported",value:function(){return void 0!==this._win.Notification}},{key:"create",value:function(i,t){return new this._win.Notification(i,{icon:f.isString(t.icon)||f.isUndefined(t.icon)||f.isNull(t.icon)?t.icon:t.icon.x32,body:t.body,tag:t.tag,requireInteraction:t.requireInteraction})}},{key:"close",value:function(i){i.close()}}]),t}(),_=function(t){function e(){return n(this,e),a(this,s(e).apply(this,arguments))}return r(e,l),o(e,[{key:"isSupported",value:function(){return void 0!==this._win.navigator&&void 0!==this._win.navigator.serviceWorker}},{key:"getFunctionBody",value:function(i){var t=i.toString().match(/function[^{]+{([\s\S]*)}$/);return null!=t&&t.length>1?t[1]:null}},{key:"create",value:function(t,n,e,o,r){var s=this;this._win.navigator.serviceWorker.register(o),this._win.navigator.serviceWorker.ready.then(function(o){var c={id:t,link:e.link,origin:document.location.href,onClick:f.isFunction(e.onClick)?s.getFunctionBody(e.onClick):"",onClose:f.isFunction(e.onClose)?s.getFunctionBody(e.onClose):""};void 0!==e.data&&null!==e.data&&(c=Object.assign(c,e.data)),o.showNotification(n,{icon:e.icon,body:e.body,vibrate:e.vibrate,tag:e.tag,data:c,requireInteraction:e.requireInteraction,silent:e.silent}).then(function(){o.getNotifications().then(function(i){o.active.postMessage(""),r(i)})}).catch(function(t){throw new Error(i.errors.sw_notification_error+t.message)})}).catch(function(t){throw new Error(i.errors.sw_registration_error+t.message)})}},{key:"close",value:function(){}}]),e}(),v=function(i){function t(){return n(this,t),a(this,s(t).apply(this,arguments))}return r(t,l),o(t,[{key:"isSupported",value:function(){return void 0!==this._win.navigator.mozNotification}},{key:"create",value:function(i,t){var n=this._win.navigator.mozNotification.createNotification(i,t.body,t.icon);return n.show(),n}}]),t}(),d=function(i){function t(){return n(this,t),a(this,s(t).apply(this,arguments))}return r(t,l),o(t,[{key:"isSupported",value:function(){return void 0!==this._win.external&&void 0!==this._win.external.msIsSiteMode}},{key:"create",value:function(i,t){return this._win.external.msSiteModeClearIconOverlay(),this._win.external.msSiteModeSetIconOverlay(f.isString(t.icon)||f.isUndefined(t.icon)?t.icon:t.icon.x16,i),this._win.external.msSiteModeActivate(),null}},{key:"close",value:function(){this._win.external.msSiteModeClearIconOverlay()}}]),t}(),w=function(i){function t(){return n(this,t),a(this,s(t).apply(this,arguments))}return r(t,l),o(t,[{key:"isSupported",value:function(){return void 0!==this._win.webkitNotifications}},{key:"create",value:function(i,t){var n=this._win.webkitNotifications.createNotification(t.icon,i,t.body);return n.show(),n}},{key:"close",value:function(i){i.cancel()}}]),t}();return new(function(){function t(i){n(this,t),this._currentId=0,this._notifications={},this._win=i,this.Permission=new u(i),this._agents={desktop:new h(i),chrome:new _(i),firefox:new v(i),ms:new d(i),webkit:new w(i)},this._configuration={serviceWorker:"/serviceWorker.min.js",fallback:function(i){}}}return o(t,[{key:"_closeNotification",value:function(t){var n=!0,e=this._notifications[t];if(void 0!==e){if(n=this._removeNotification(t),this._agents.desktop.isSupported())this._agents.desktop.close(e);else if(this._agents.webkit.isSupported())this._agents.webkit.close(e);else{if(!this._agents.ms.isSupported())throw n=!1,new Error(i.errors.unknown_interface);this._agents.ms.close()}return n}return!1}},{key:"_addNotification",value:function(i){var t=this._currentId;return this._notifications[t]=i,this._currentId++,t}},{key:"_removeNotification",value:function(i){var t=!1;return this._notifications.hasOwnProperty(i)&&(delete this._notifications[i],t=!0),t}},{key:"_prepareNotification",value:function(i,t){var n,e=this;return n={get:function(){return e._notifications[i]},close:function(){e._closeNotification(i)}},t.timeout&&setTimeout(function(){n.close()},t.timeout),n}},{key:"_serviceWorkerCallback",value:function(i,t,n){var e=this,o=this._addNotification(i[i.length-1]);navigator&&navigator.serviceWorker&&(navigator.serviceWorker.addEventListener("message",function(i){var t=JSON.parse(i.data);"close"===t.action&&Number.isInteger(t.id)&&e._removeNotification(t.id)}),n(this._prepareNotification(o,t))),n(null)}},{key:"_createCallback",value:function(i,t,n){var e,o=this,r=null;if(t=t||{},e=function(i){o._removeNotification(i),f.isFunction(t.onClose)&&t.onClose.call(o,r)},this._agents.desktop.isSupported())try{r=this._agents.desktop.create(i,t)}catch(e){var s=this._currentId,c=this.config().serviceWorker;this._agents.chrome.isSupported()&&this._agents.chrome.create(s,i,t,c,function(i){return o._serviceWorkerCallback(i,t,n)})}else this._agents.webkit.isSupported()?r=this._agents.webkit.create(i,t):this._agents.firefox.isSupported()?this._agents.firefox.create(i,t):this._agents.ms.isSupported()?r=this._agents.ms.create(i,t):(t.title=i,this.config().fallback(t));if(null!==r){var a=this._addNotification(r),u=this._prepareNotification(a,t);f.isFunction(r.addEventListener)&&(f.isFunction(t.onShow)&&r.addEventListener("show",t.onShow),f.isFunction(t.onError)&&r.addEventListener("error",t.onError),f.isFunction(t.onClick)&&r.addEventListener("click",t.onClick),r.addEventListener("close",function(){e(a)}),r.addEventListener("cancel",function(){e(a)})),n(u)}n(null)}},{key:"create",value:function(t,n){var e,o=this;if(!f.isString(t))throw new Error(i.errors.invalid_title);return e=this.Permission.has()?function(i,e){try{o._createCallback(t,n,i)}catch(i){e(i)}}:function(e,r){o.Permission.request().then(function(){o._createCallback(t,n,e)}).catch(function(){r(i.errors.permission_denied)})},new Promise(e)}},{key:"count",value:function(){var i,t=0;for(i in this._notifications)this._notifications.hasOwnProperty(i)&&t++;return t}},{key:"close",value:function(i){var t;for(t in this._notifications)if(this._notifications.hasOwnProperty(t)&&this._notifications[t].tag===i)return this._closeNotification(t)}},{key:"clear",value:function(){var i,t=!0;for(i in this._notifications)this._notifications.hasOwnProperty(i)&&(t=t&&this._closeNotification(i));return t}},{key:"supported",value:function(){var i=!1;for(var t in this._agents)this._agents.hasOwnProperty(t)&&(i=i||this._agents[t].isSupported());return i}},{key:"config",value:function(i){return(void 0!==i||null!==i&&f.isObject(i))&&f.objectMerge(this._configuration,i),this._configuration}},{key:"extend",value:function(t){var n,e={}.hasOwnProperty;if(!e.call(t,"plugin"))throw new Error(i.errors.invalid_plugin);for(var o in e.call(t,"config")&&f.isObject(t.config)&&null!==t.config&&this.config(t.config),n=new(0,t.plugin)(this.config()))e.call(n,o)&&f.isFunction(n[o])&&(this[o]=n[o])}}]),t}())("undefined"!=typeof window?window:global)});
//# sourceMappingURL=push.js.map
================================================
FILE: bower.json
================================================
{
"name": "push.js",
"description": "A compact, cross-browser solution for the Javascript Notifications API",
"main": "bin/push.js",
"authors": [
"Tyler Nickerson"
],
"license": "MIT",
"homepage": "https://pushjs.org",
"ignore": [
"**/.*",
"coverage",
"node_modules",
"bower_components",
"tests",
"src",
"build",
"*.lock",
"*.json"
]
}
================================================
FILE: index.d.ts
================================================
declare module 'push.js' {
const defaultPush: Push;
export default defaultPush;
class Push {
Permission: PushPermission;
create(title: string, params?: PushNotificationParams): Promise<PushNotification>
close(tag: string): void;
clear(): void;
config(params: PushParams): void;
}
export interface PushNotificationParams {
body?: string;
icon?: string;
link?: string;
timeout?: number;
tag?: string;
requireInteraction?: boolean;
vibrate?: boolean;
silent?: boolean;
onClick?: Function;
onError?: Function;
}
export interface PushParams {
serviceWorker?: string;
fallback?: Function;
}
export interface PushPermission {
DEFAULT: string;
GRANTED: string;
DENIED: string;
request(onGranted?: Function, onDenied?: Function): void;
has(): boolean;
get(): string;
}
export interface PushNotification {
close(): void;
}
}
================================================
FILE: package.json
================================================
{
"name": "push.js",
"version": "1.0.12",
"description": "A compact, cross-browser solution for the Javascript Notifications API",
"main": "bin/push.min.js",
"scripts": {
"clean": "rimraf bin/",
"build": "rollup -c && uglifyjs --source-map -o bin/serviceWorker.min.js src/serviceWorker.js",
"test": "npm run build && karma start tests/karma.conf.js",
"test:chrome": "PUSHJS_TEST_BROWSER=Chrome npm run test",
"test:opera": "PUSHJS_TEST_BROWSER=Opera npm run test",
"test:firefox": "PUSHJS_TEST_BROWSER=Firefox npm run test",
"test:safari": "PUSHJS_TEST_BROWSER=Safari npm run test",
"prepublish": "npm run build",
"precommit": "lint-staged && npm run build && git add ./bin"
},
"files": [
"bin",
"*.md",
"*.png",
"*.d.ts"
],
"repository": {
"type": "git",
"url": "https://github.com/Nickersoft/push.js"
},
"author": "Tyler Nickerson",
"license": "MIT",
"bugs": {
"url": "https://github.com/Nickersoft/push.js/issues"
},
"homepage": "https://github.com/Nickersoft/push.js",
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-decorators": "^7.4.4",
"@babel/plugin-proposal-export-namespace-from": "^7.5.2",
"@babel/plugin-proposal-function-sent": "^7.5.0",
"@babel/plugin-proposal-json-strings": "^7.2.0",
"@babel/plugin-proposal-numeric-separator": "^7.2.0",
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/plugin-transform-flow-strip-types": "^7.4.4",
"@babel/plugin-transform-strict-mode": "^7.2.0",
"@babel/polyfill": "^7.4.4",
"@babel/preset-env": "^7.5.5",
"@babel/preset-flow": "^7.0.0",
"browserify": "^16.3.0",
"coveralls": "^3.0.5",
"flow-bin": "^0.103.0",
"husky": "^3.0.1",
"jasmine-core": "^3.4.0",
"js-yaml": "^3.13.1",
"karma": "^4.2.0",
"karma-browserstack-launcher": "~1.4.0",
"karma-chrome-launcher": "^3.0.0",
"karma-coverage": "^1.1.2",
"karma-firefox-launcher": "^1.1.0",
"karma-jasmine": "^2.0.1",
"karma-mocha-reporter": "^2.2.5",
"karma-opera-launcher": "^1.0.0",
"karma-safari-launcher": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"lint-staged": "^9.2.0",
"natives": "^1.1.6",
"platform": "^1.3.5",
"prettier": "^1.18.2",
"rimraf": "^2.6.3",
"rollup": "^1.17.0",
"rollup-plugin-alias": "^1.5.2",
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-commonjs": "^10.0.1",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^5.1.1"
},
"lint-staged": {
"*.{js,json,css}": [
"prettier --write",
"git add"
]
},
"dependencies": {},
"resolutions": {
"natives": "1.1.6"
}
}
================================================
FILE: rollup.config.js
================================================
import path from "path";
import resolve from "rollup-plugin-node-resolve";
import babel from "rollup-plugin-babel";
import commonjs from "rollup-plugin-commonjs";
import alias from "rollup-plugin-alias";
import { terser } from "rollup-plugin-terser";
const license = `/**
* @license
*
* Push v1.0.9
* =========
* A compact, cross-browser solution for the JavaScript Notifications API
*
* Credits
* -------
* Tsvetan Tsvetkov (ttsvetko)
* Alex Gibson (alexgibson)
*
* License
* -------
*
* The MIT License (MIT)
*
* Copyright (c) 2015-2017 Tyler Nickerson
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/`;
const common = {
input: "src/index.js",
output: {
banner: license,
file: "bin/push.min.js",
format: "umd",
name: "Push",
sourcemap: true
},
plugins: [
babel({
exclude: "node_modules/**"
}),
alias({
types: path.resolve(__dirname, "src/types"),
push: path.resolve(__dirname, "src/push/index"),
agents: path.resolve(__dirname, "src/agents/index")
}),
commonjs(),
resolve(),
terser({
output: {
beautify: false,
preamble: license
}
})
]
};
export default [
{
...common,
output: {
...common.output,
file: "bin/push.js"
}
},
{
...common,
output: {
...common.output,
file: "bin/push.min.js"
}
}
];
================================================
FILE: src/agents/AbstractAgent.js
================================================
// @flow
import type { Global } from 'types';
export default class AbstractAgent {
_win: Global;
constructor(win: Global) {
this._win = win;
}
}
================================================
FILE: src/agents/DesktopAgent.js
================================================
// @flow
import { AbstractAgent } from 'agents';
import { Util } from 'push';
import type { PushOptions, GenericNotification, Global } from 'types';
/**
* Notification agent for modern desktop browsers:
* Safari 6+, Firefox 22+, Chrome 22+, Opera 25+
*/
export default class DesktopAgent extends AbstractAgent {
_win: Global;
/**
* Returns a boolean denoting support
* @returns {Boolean} boolean denoting whether webkit notifications are supported
*/
isSupported() {
return this._win.Notification !== undefined;
}
/**
* Creates a new notification
* @param title - notification title
* @param options - notification options array
* @returns {Notification}
*/
create(title: string, options: PushOptions) {
return new this._win.Notification(title, {
icon:
Util.isString(options.icon) ||
Util.isUndefined(options.icon) ||
Util.isNull(options.icon)
? options.icon
: options.icon.x32,
body: options.body,
tag: options.tag,
requireInteraction: options.requireInteraction
});
}
/**
* Close a given notification
* @param notification - notification to close
*/
close(notification: GenericNotification) {
notification.close();
}
}
================================================
FILE: src/agents/MSAgent.js
================================================
// @flow
import { AbstractAgent } from 'agents';
import { Util } from 'push';
import type { PushOptions, Global } from 'types';
/**
* Notification agent for IE9
*/
export default class MSAgent extends AbstractAgent {
_win: Global;
/**
* Returns a boolean denoting support
* @returns {Boolean} boolean denoting whether webkit notifications are supported
*/
isSupported() {
return (
this._win.external !== undefined &&
this._win.external.msIsSiteMode !== undefined
);
}
/**
* Creates a new notification
* @param title - notification title
* @param options - notification options array
* @returns {Notification}
*/
create(title: string, options: PushOptions) {
/* Clear any previous notifications */
this._win.external.msSiteModeClearIconOverlay();
this._win.external.msSiteModeSetIconOverlay(
Util.isString(options.icon) || Util.isUndefined(options.icon)
? options.icon
: options.icon.x16,
title
);
this._win.external.msSiteModeActivate();
return null;
}
/**
* Close a given notification
* @param notification - notification to close
*/
close() {
this._win.external.msSiteModeClearIconOverlay();
}
}
================================================
FILE: src/agents/MobileChromeAgent.js
================================================
// @flow
import { Util, Messages } from 'push';
import { AbstractAgent } from 'agents';
import type { Global, GenericNotification, PushOptions } from 'types';
/**
* Notification agent for modern desktop browsers:
* Safari 6+, Firefox 22+, Chrome 22+, Opera 25+
*/
export default class MobileChromeAgent extends AbstractAgent {
_win: Global;
/**
* Returns a boolean denoting support
* @returns {Boolean} boolean denoting whether webkit notifications are supported
*/
isSupported() {
return (
this._win.navigator !== undefined &&
this._win.navigator.serviceWorker !== undefined
);
}
/**
* Returns the function body as a string
* @param func
*/
getFunctionBody(func: () => void) {
const str = func.toString().match(/function[^{]+{([\s\S]*)}$/);
return typeof str !== 'undefined' && str !== null && str.length > 1
? str[1]
: null;
}
/**
* Creates a new notification
* @param id ID of notification
* @param title Title of notification
* @param options Options object
* @param serviceWorker ServiceWorker path
* @param callback Callback function
*/
create(
id: number,
title: string,
options: PushOptions,
serviceWorker: string,
callback: (GenericNotification[]) => void
) {
/* Register ServiceWorker */
this._win.navigator.serviceWorker.register(serviceWorker);
this._win.navigator.serviceWorker.ready
.then(registration => {
/* Local data the service worker will use */
let localData = {
id: id,
link: options.link,
origin: document.location.href,
onClick: Util.isFunction(options.onClick)
? this.getFunctionBody(options.onClick)
: '',
onClose: Util.isFunction(options.onClose)
? this.getFunctionBody(options.onClose)
: ''
};
/* Merge the local data with user-provided data */
if (options.data !== undefined && options.data !== null)
localData = Object.assign(localData, options.data);
/* Show the notification */
registration
.showNotification(title, {
icon: options.icon,
body: options.body,
vibrate: options.vibrate,
tag: options.tag,
data: localData,
requireInteraction: options.requireInteraction,
silent: options.silent
})
.then(() => {
registration.getNotifications().then(notifications => {
/* Send an empty message so the ServiceWorker knows who the client is */
registration.active.postMessage('');
/* Trigger callback */
callback(notifications);
});
})
.catch(function(error) {
throw new Error(
Messages.errors.sw_notification_error +
error.message
);
});
})
.catch(function(error) {
throw new Error(
Messages.errors.sw_registration_error + error.message
);
});
}
/**
* Close all notification
*/
close() {
// Can't do this with service workers
}
}
================================================
FILE: src/agents/MobileFirefoxAgent.js
================================================
// @flow
import { AbstractAgent } from 'agents';
import type { Global, PushOptions } from 'types';
/**
* Notification agent for modern desktop browsers:
* Safari 6+, Firefox 22+, Chrome 22+, Opera 25+
*/
export default class MobileFirefoxAgent extends AbstractAgent {
_win: Global;
/**
* Returns a boolean denoting support
* @returns {Boolean} boolean denoting whether webkit notifications are supported
*/
isSupported() {
return this._win.navigator.mozNotification !== undefined;
}
/**
* Creates a new notification
* @param title - notification title
* @param options - notification options array
* @returns {Notification}
*/
create(title: string, options: PushOptions) {
let notification = this._win.navigator.mozNotification.createNotification(
title,
options.body,
options.icon
);
notification.show();
return notification;
}
}
================================================
FILE: src/agents/WebKitAgent.js
================================================
// @flow
import { AbstractAgent } from 'agents';
import type { Global, GenericNotification, PushOptions } from 'types';
/**
* Notification agent for old Chrome versions (and some) Firefox
*/
export default class WebKitAgent extends AbstractAgent {
_win: Global;
/**
* Returns a boolean denoting support
* @returns {Boolean} boolean denoting whether webkit notifications are supported
*/
isSupported() {
return this._win.webkitNotifications !== undefined;
}
/**
* Creates a new notification
* @param title - notification title
* @param options - notification options array
* @returns {Notification}
*/
create(title: string, options: PushOptions) {
let notification = this._win.webkitNotifications.createNotification(
options.icon,
title,
options.body
);
notification.show();
return notification;
}
/**
* Close a given notification
* @param notification - notification to close
*/
close(notification: GenericNotification) {
notification.cancel();
}
}
================================================
FILE: src/agents/index.js
================================================
import AbstractAgent from './AbstractAgent';
import DesktopAgent from './DesktopAgent';
import MobileChromeAgent from './MobileChromeAgent';
import MobileFirefoxAgent from './MobileFirefoxAgent';
import MSAgent from './MSAgent';
import WebKitAgent from './WebKitAgent';
export {
AbstractAgent,
DesktopAgent,
MobileChromeAgent,
MobileFirefoxAgent,
MSAgent,
WebKitAgent
};
================================================
FILE: src/index.js
================================================
// @flow
import { Push } from 'push';
export default new Push(typeof window !== 'undefined' ? window : global);
================================================
FILE: src/push/Messages.js
================================================
// @flow
const errorPrefix = 'PushError:';
export default {
errors: {
incompatible: `${errorPrefix} Push.js is incompatible with browser.`,
invalid_plugin: `${errorPrefix} plugin class missing from plugin manifest (invalid plugin). Please check the documentation.`,
invalid_title: `${errorPrefix} title of notification must be a string`,
permission_denied: `${errorPrefix} permission request declined`,
sw_notification_error: `${errorPrefix} could not show a ServiceWorker notification due to the following reason: `,
sw_registration_error: `${errorPrefix} could not register the ServiceWorker due to the following reason: `,
unknown_interface: `${errorPrefix} unable to create notification: unknown interface`
}
};
================================================
FILE: src/push/Permission.js
================================================
// @flow
import type { Global } from 'types';
export default class Permission {
// Private members
_permissions: string[];
_win: Global;
// Public members
GRANTED: string;
DEFAULT: string;
DENIED: string;
constructor(win: Global) {
this._win = win;
this.GRANTED = 'granted';
this.DEFAULT = 'default';
this.DENIED = 'denied';
this._permissions = [this.GRANTED, this.DEFAULT, this.DENIED];
}
/**
* Requests permission for desktop notifications
* @param {Function} onGranted - Function to execute once permission is granted
* @param {Function} onDenied - Function to execute once permission is denied
* @return {void, Promise}
*/
request(onGranted: () => void, onDenied: () => void) {
return arguments.length > 0
? this._requestWithCallback(...arguments)
: this._requestAsPromise();
}
/**
* Old permissions implementation deprecated in favor of a promise based one
* @deprecated Since V1.0.4
* @param {Function} onGranted - Function to execute once permission is granted
* @param {Function} onDenied - Function to execute once permission is denied
* @return {void}
*/
_requestWithCallback(onGranted: () => void, onDenied: () => void) {
const existing = this.get();
var resolved = false;
var resolve = (result = this._win.Notification.permission) => {
if (resolved) return;
resolved = true;
if (typeof result === 'undefined' && this._win.webkitNotifications)
result = this._win.webkitNotifications.checkPermission();
if (result === this.GRANTED || result === 0) {
if (onGranted) onGranted();
} else if (onDenied) onDenied();
};
var request;
/* Permissions already set */
if (existing !== this.DEFAULT) {
resolve(existing);
} else if (
this._win.webkitNotifications &&
this._win.webkitNotifications.checkPermission
) {
/* Safari 6+, Legacy webkit browsers */
this._win.webkitNotifications.requestPermission(resolve);
} else if (
this._win.Notification &&
this._win.Notification.requestPermission
) {
/* Safari 12+ */
/* This resolve argument will only be used in Safari */
/* CHrome, instead, returns a Promise */
request = this._win.Notification.requestPermission(resolve);
if (request && request.then) {
/* Chrome 23+ */
request.then(resolve).catch(function() {
if (onDenied) onDenied();
});
}
} else if (onGranted) {
/* Let the user continue by default */
onGranted();
}
}
/**
* Requests permission for desktop notifications in a promise based way
* @return {Promise}
*/
_requestAsPromise(): Promise<void> {
const existing = this.get();
let isGranted = result => result === this.GRANTED || result === 0;
/* Permissions already set */
var hasPermissions = existing !== this.DEFAULT;
/* Safari 6+, Chrome 23+ */
var isModernAPI =
this._win.Notification && this._win.Notification.requestPermission;
/* Legacy webkit browsers */
var isWebkitAPI =
this._win.webkitNotifications &&
this._win.webkitNotifications.checkPermission;
return new Promise((resolvePromise, rejectPromise) => {
var resolved = false;
var resolver = result => {
if (resolved) return;
resolved = true;
isGranted(result) ? resolvePromise() : rejectPromise();
};
var request;
if (hasPermissions) {
resolver(existing);
} else if (isWebkitAPI) {
this._win.webkitNotifications.requestPermission(result => {
resolver(result);
});
} else if (isModernAPI) {
/* Safari 12+ */
/* This resolver argument will only be used in Safari */
/* CHrome, instead, returns a Promise */
request = this._win.Notification.requestPermission(resolver);
if (request && request.then) {
/* Chrome 23+ */
request.then(resolver).catch(rejectPromise);
}
} else resolvePromise();
});
}
/**
* Returns whether Push has been granted permission to run
* @return {Boolean}
*/
has() {
return this.get() === this.GRANTED;
}
/**
* Gets the permission level
* @return {Permission} The permission level
*/
get() {
let permission;
/* Safari 6+, Chrome 23+ */
if (this._win.Notification && this._win.Notification.permission)
permission = this._win.Notification.permission;
else if (
this._win.webkitNotifications &&
this._win.webkitNotifications.checkPermission
)
/* Legacy webkit browsers */
permission = this._permissions[
this._win.webkitNotifications.checkPermission()
];
else if (navigator.mozNotification)
/* Firefox Mobile */
permission = this.GRANTED;
else if (this._win.external && this._win.external.msIsSiteMode)
/* IE9+ */
permission = this._win.external.msIsSiteMode()
? this.GRANTED
: this.DEFAULT;
else permission = this.GRANTED;
return permission;
}
}
================================================
FILE: src/push/Push.js
================================================
// @flow
import { Messages, Permission, Util } from 'push';
import type { PluginManifest, GenericNotification, PushOptions } from 'types';
/* Import notification agents */
import {
DesktopAgent,
MobileChromeAgent,
MobileFirefoxAgent,
MSAgent,
WebKitAgent
} from 'agents';
export default class Push {
// Private members
_agents: {
desktop: DesktopAgent,
chrome: MobileChromeAgent,
firefox: MobileFirefoxAgent,
ms: MSAgent,
webkit: WebKitAgent
};
_configuration: {
serviceWorker: string,
fallback: ({}) => void
};
_currentId: number;
_notifications: {};
_win: {};
// Public members
Permission: Permission;
constructor(win: {}) {
/* Private variables */
/* ID to use for new notifications */
this._currentId = 0;
/* Map of open notifications */
this._notifications = {};
/* Window object */
this._win = win;
/* Public variables */
this.Permission = new Permission(win);
/* Agents */
this._agents = {
desktop: new DesktopAgent(win),
chrome: new MobileChromeAgent(win),
firefox: new MobileFirefoxAgent(win),
ms: new MSAgent(win),
webkit: new WebKitAgent(win)
};
this._configuration = {
serviceWorker: '/serviceWorker.min.js',
fallback: function(payload) {}
};
}
/**
* Closes a notification
* @param id ID of notification
* @returns {boolean} denotes whether the operation was successful
* @private
*/
_closeNotification(id: number | string) {
let success = true;
const notification = this._notifications[id];
if (notification !== undefined) {
success = this._removeNotification(id);
/* Safari 6+, Firefox 22+, Chrome 22+, Opera 25+ */
if (this._agents.desktop.isSupported())
this._agents.desktop.close(notification);
else if (this._agents.webkit.isSupported())
/* Legacy WebKit browsers */
this._agents.webkit.close(notification);
else if (this._agents.ms.isSupported())
/* IE9 */
this._agents.ms.close();
else {
success = false;
throw new Error(Messages.errors.unknown_interface);
}
return success;
}
return false;
}
/**
* Adds a notification to the global dictionary of notifications
* @param {Notification} notification
* @return {Integer} Dictionary key of the notification
* @private
*/
_addNotification(notification: GenericNotification) {
const id = this._currentId;
this._notifications[id] = notification;
this._currentId++;
return id;
}
/**
* Removes a notification with the given ID
* @param {Integer} id - Dictionary key/ID of the notification to remove
* @return {Boolean} boolean denoting success
* @private
*/
_removeNotification(id: number | string) {
let success = false;
if (this._notifications.hasOwnProperty(id)) {
/* We're successful if we omit the given ID from the new array */
delete this._notifications[id];
success = true;
}
return success;
}
/**
* Creates the wrapper for a given notification
*
* @param {Integer} id - Dictionary key/ID of the notification
* @param {Map} options - Options used to create the notification
* @returns {Map} wrapper hashmap object
* @private
*/
_prepareNotification(id: number, options: PushOptions) {
let wrapper;
/* Wrapper used to get/close notification later on */
wrapper = {
get: () => {
return this._notifications[id];
},
close: () => {
this._closeNotification(id);
}
};
/* Autoclose timeout */
if (options.timeout) {
setTimeout(() => {
wrapper.close();
}, options.timeout);
}
return wrapper;
}
/**
* Find the most recent notification from a ServiceWorker and add it to the global array
* @param notifications
* @private
*/
_serviceWorkerCallback(
notifications: GenericNotification[],
options: PushOptions,
resolve: ({} | null) => void
) {
let id = this._addNotification(notifications[notifications.length - 1]);
/* Listen for close requests from the ServiceWorker */
if (navigator && navigator.serviceWorker) {
navigator.serviceWorker.addEventListener('message', event => {
const data = JSON.parse(event.data);
if (data.action === 'close' && Number.isInteger(data.id))
this._removeNotification(data.id);
});
resolve(this._prepareNotification(id, options));
}
resolve(null);
}
/**
* Callback function for the 'create' method
* @return {void}
* @private
*/
_createCallback(
title: string,
options: PushOptions,
resolve: ({} | null) => void
) {
let notification = null;
let onClose;
/* Set empty settings if none are specified */
options = options || {};
/* onClose event handler */
onClose = id => {
/* A bit redundant, but covers the cases when close() isn't explicitly called */
this._removeNotification(id);
if (Util.isFunction(options.onClose)) {
options.onClose.call(this, notification);
}
};
/* Safari 6+, Firefox 22+, Chrome 22+, Opera 25+ */
if (this._agents.desktop.isSupported()) {
try {
/* Create a notification using the API if possible */
notification = this._agents.desktop.create(title, options);
} catch (e) {
const id = this._currentId;
const sw = this.config().serviceWorker;
const cb = notifications =>
this._serviceWorkerCallback(
notifications,
options,
resolve
);
/* Create a Chrome ServiceWorker notification if it isn't supported */
if (this._agents.chrome.isSupported()) {
this._agents.chrome.create(id, title, options, sw, cb);
}
}
/* Legacy WebKit browsers */
} else if (this._agents.webkit.isSupported())
notification = this._agents.webkit.create(title, options);
else if (this._agents.firefox.isSupported())
/* Firefox Mobile */
this._agents.firefox.create(title, options);
else if (this._agents.ms.isSupported())
/* IE9 */
notification = this._agents.ms.create(title, options);
else {
/* Default fallback */
options.title = title;
this.config().fallback(options);
}
if (notification !== null) {
const id = this._addNotification(notification);
const wrapper = this._prepareNotification(id, options);
/* Notification callbacks */
if (Util.isFunction(notification.addEventListener)) {
if (Util.isFunction(options.onShow))
notification.addEventListener('show', options.onShow);
if (Util.isFunction(options.onError))
notification.addEventListener('error', options.onError);
if (Util.isFunction(options.onClick))
notification.addEventListener('click', options.onClick);
notification.addEventListener('close', () => {
onClose(id);
});
notification.addEventListener('cancel', () => {
onClose(id);
});
}
/* Return the wrapper so the user can call close() */
resolve(wrapper);
}
/* By default, pass an empty wrapper */
resolve(null);
}
/**
* Creates and displays a new notification
* @param {Array} options
* @return {Promise}
*/
create(title: string, options: {}): Promise<void> {
let promiseCallback;
/* Fail if no or an invalid title is provided */
if (!Util.isString(title)) {
throw new Error(Messages.errors.invalid_title);
}
/* Request permission if it isn't granted */
if (!this.Permission.has()) {
promiseCallback = (resolve: () => void, reject: string => void) => {
this.Permission
.request()
.then(() => {
this._createCallback(title, options, resolve);
})
.catch(() => {
reject(Messages.errors.permission_denied);
});
};
} else {
promiseCallback = (resolve: () => void, reject: string => void) => {
try {
this._createCallback(title, options, resolve);
} catch (e) {
reject(e);
}
};
}
return new Promise(promiseCallback);
}
/**
* Returns the notification count
* @return {Integer} The notification count
*/
count() {
let count = 0;
let key;
for (key in this._notifications)
if (this._notifications.hasOwnProperty(key)) count++;
return count;
}
/**
* Closes a notification with the given tag
* @param {String} tag - Tag of the notification to close
* @return {Boolean} boolean denoting success
*/
close(tag: string) {
let key, notification;
for (key in this._notifications) {
if (this._notifications.hasOwnProperty(key)) {
notification = this._notifications[key];
/* Run only if the tags match */
if (notification.tag === tag) {
/* Call the notification's close() method */
return this._closeNotification(key);
}
}
}
}
/**
* Clears all notifications
* @return {Boolean} boolean denoting whether the clear was successful in closing all notifications
*/
clear() {
let key,
success = true;
for (key in this._notifications)
if (this._notifications.hasOwnProperty(key))
success = success && this._closeNotification(key);
return success;
}
/**
* Denotes whether Push is supported in the current browser
* @returns {boolean}
*/
supported() {
let supported = false;
for (var agent in this._agents)
if (this._agents.hasOwnProperty(agent))
supported = supported || this._agents[agent].isSupported();
return supported;
}
/**
* Modifies settings or returns all settings if no parameter passed
* @param settings
*/
config(settings?: {}) {
if (
typeof settings !== 'undefined' ||
(settings !== null && Util.isObject(settings))
)
Util.objectMerge(this._configuration, settings);
return this._configuration;
}
/**
* Copies the functions from a plugin to the main library
* @param plugin
*/
extend(manifest: PluginManifest) {
var plugin,
Plugin,
hasProp = {}.hasOwnProperty;
if (!hasProp.call(manifest, 'plugin')) {
throw new Error(Messages.errors.invalid_plugin);
} else {
if (
hasProp.call(manifest, 'config') &&
Util.isObject(manifest.config) &&
manifest.config !== null
) {
this.config(manifest.config);
}
Plugin = manifest.plugin;
plugin = new Plugin(this.config());
for (var member in plugin) {
if (
hasProp.call(plugin, member) &&
Util.isFunction(plugin[member])
)
// $FlowFixMe
this[member] = plugin[member];
}
}
}
}
================================================
FILE: src/push/Util.js
================================================
// @flow
export default class Util {
static isUndefined(obj) {
return obj === undefined;
}
static isNull(obs) {
return obj === null;
}
static isString(obj) {
return typeof obj === 'string';
}
static isFunction(obj) {
return obj && {}.toString.call(obj) === '[object Function]';
}
static isObject(obj) {
return typeof obj === 'object';
}
static objectMerge(target, source) {
for (var key in source) {
if (
target.hasOwnProperty(key) &&
this.isObject(target[key]) &&
this.isObject(source[key])
) {
this.objectMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
}
================================================
FILE: src/push/index.js
================================================
import Messages from './Messages';
import Permission from './Permission';
import Util from './Util';
import Push from './Push';
export { Messages, Permission, Util, Push };
================================================
FILE: src/serviceWorker.js
================================================
'use strict';
function isFunction(obj) {
return obj && {}.toString.call(obj) === '[object Function]';
}
function runFunctionString(funcStr) {
if (funcStr.trim().length > 0) {
var func = new Function(funcStr);
if (isFunction(func)) {
func();
}
}
}
self.addEventListener('message', function(event) {
self.client = event.source;
});
self.onnotificationclose = function(event) {
runFunctionString(event.notification.data.onClose);
/* Tell Push to execute close callback */
self.client.postMessage(
JSON.stringify({
id: event.notification.data.id,
action: 'close'
})
);
};
self.onnotificationclick = function(event) {
var link, origin, href;
if (
typeof event.notification.data.link !== 'undefined' &&
event.notification.data.link !== null
) {
origin = event.notification.data.origin;
link = event.notification.data.link;
href = origin.substring(0, origin.indexOf('/', 8)) + '/';
/* Removes prepending slash, as we don't need it */
if (link[0] === '/') {
link = link.length > 1 ? link.substring(1, link.length) : '';
}
event.notification.close();
/* This looks to see if the current is already open and focuses if it is */
event.waitUntil(
clients
.matchAll({
type: 'window'
})
.then(function(clientList) {
var client, full_url;
for (var i = 0; i < clientList.length; i++) {
client = clientList[i];
full_url = href + link;
/* Covers case where full_url might be http://example.com/john and the client URL is http://example.com/john/ */
if (
full_url[full_url.length - 1] !== '/' &&
client.url[client.url.length - 1] === '/'
) {
full_url += '/';
}
if (client.url === full_url && 'focus' in client) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow('/' + link);
}
})
.catch(function(error) {
throw new Error(
'A ServiceWorker error occurred: ' + error.message
);
})
);
}
runFunctionString(event.notification.data.onClick);
};
================================================
FILE: src/types.js
================================================
export type GenericNotification = Notification | webkitNotifications;
export type Global = {
Notification?: Notification,
webkitNotifications?: webkitNotifications
};
export type PushOptions = {
body?: string,
icon?: string,
link?: string,
timeout?: number,
tag?: string,
requireInteraction?: boolean,
vibrate?: boolean,
silent?: boolean,
onClick?: Function,
onError?: Function
};
export type PluginManifest = {
plugin: {},
config?: {}
};
================================================
FILE: tests/browsers.conf.js
================================================
var BROWSER_FIREFOX = 'Firefox',
BROWSER_CHROME = 'Chrome',
BROWSER_EDGE = 'Edge',
BROWSER_IE = 'ie',
BROWSER_OPERA = 'Opera',
BROWSER_SAFARI = 'Safari';
function getWindowsDesktop(browser, version) {
return {
base: 'BrowserStack',
browser: browser,
browser_version: version.toString() + '.0',
os: 'Windows',
os_version: '10'
};
}
function getOSXDesktop(browser, version, os) {
var rounded;
if (!os) os = version < 25 ? 'Snow Leopard' : 'Sierra';
rounded = Math.floor(version);
if (version == rounded) version = version.toString() + '.0';
return {
base: 'BrowserStack',
browser: browser,
browser_version: version,
os: 'OS X',
os_version: os
};
}
function getMobile(browser) {
return {
base: 'BrowserStack',
real_mobile: true,
device: 'Google Pixel',
browser: 'Mobile ' + browser,
os: 'android',
os_version: '7.1'
};
}
module.exports = {
bs_firefox_mac: getOSXDesktop(BROWSER_FIREFOX, 54),
bs_firefox_mac_old: getOSXDesktop(BROWSER_FIREFOX, 21),
bs_chrome_mac: getOSXDesktop(BROWSER_CHROME, 59),
bs_edge_win: getWindowsDesktop(BROWSER_EDGE, 15),
bs_safari_mac: getOSXDesktop(BROWSER_SAFARI, 10.1, 'Sierra'),
bs_opera_mac: getOSXDesktop(BROWSER_OPERA, 46)
// bs_chrome_mac_old: getOSXDesktop(BROWSER_CHROME, 16), <-- issues testing Push on old Chrome versions in BrowserStack
// bs_firefox_mobile: getMobile(BROWSER_CHROME) <-- can't work because of HTTPS requirement (wth dude)
};
================================================
FILE: tests/karma.conf.js
================================================
// Karma configuration
// Generated on Tue Jul 21 2015 22:34:30 GMT-0400 (EDT)
let browsers, selected_browsers;
browsers = require('./browsers.conf');
selected_browsers = Object.keys(browsers);
const browser = process.env.PUSHJS_TEST_BROWSER;
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '../',
...(!browser && {
browserStack: {
username: 'Nickersoft',
accessKey: 'peTScQRRBpSkOGjybGpd'
}
}),
coverageReporter: {
// specify a common output directory
dir: 'coverage',
reporters: [
{
type: 'lcov',
subdir: '.'
}
]
},
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],
plugins: [
'karma-jasmine',
'karma-mocha-reporter',
'karma-coverage',
'karma-sourcemap-loader',
...(browser
? [`karma-${browser.toLowerCase()}-launcher`]
: ['karma-browserstack-launcher'])
],
// list of files / patterns to load in the browser
files: [
'./node_modules/platform/platform.js',
'./node_modules/@babel/polyfill/dist/polyfill.min.js',
'./bin/push.min.js',
'./tests/push.tests.js',
'./src/serviceWorker.min.js'
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'./bin/push.js': ['sourcemap', 'coverage']
},
// src results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['mocha', 'coverage', ...(!browser ? ['BrowserStack'] : [])],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing src whenever any file changes
autoWatch: false,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: browser ? [browser] : selected_browsers,
// Continuous Integration mode
// if true, Karma captures browsers, runs the src and exits
singleRun: true,
// custom browser launchers for BrowserStack
...(!browser && { customLaunchers: browsers })
});
};
================================================
FILE: tests/push.tests.js
================================================
var BROWSER_CHROME = 'Chrome',
BROWSER_FIREFOX = 'Firefox',
BROWSER_EDGE = 'Edge',
BROWSER_OPERA = 'Opera',
TEST_TITLE = 'title',
TEST_BODY = 'body',
TEST_TIMEOUT = 1000,
TEST_TAG = 'foo',
TEST_TAG_2 = 'bar',
TEST_ICON = 'icon',
TEST_SW_DEFAULT = '/serviceWorker.min.js',
NOOP = function() {
return null;
};
describe('proper support detection', function() {
function isBrowser(browser) {
return platform.name.toLowerCase() === browser.toLowerCase();
}
function getVersion() {
return parseFloat(platform.version);
}
function isSupported() {
return Push.supported();
}
it('should detect Firefox support correctly', function() {
if (isBrowser(BROWSER_FIREFOX)) {
if (getVersion() > 21) expect(isSupported()).toBeTruthy();
else expect(isSupported()).toBeFalsy();
} else {
pending();
}
});
it('should detect Chrome support correctly', function() {
if (isBrowser(BROWSER_CHROME)) {
if (getVersion() > 4) expect(isSupported()).toBeTruthy();
else expect(isSupported()).toBeFalsy();
} else {
pending();
}
});
it('should detect Opera support correctly', function() {
if (isBrowser(BROWSER_OPERA)) {
if (getVersion() > 23) expect(isSupported()).toBeTruthy();
else expect(isSupported()).toBeFalsy();
} else {
pending();
}
});
it('should detect Edge support correctly', function() {
if (isBrowser(BROWSER_EDGE)) {
if (getVersion() > 14) expect(isSupported()).toBeTruthy();
else expect(isSupported()).toBeFalsy();
} else {
pending();
}
});
});
describe('adding plugins', function() {
it('reject invalid plugin manifests', function() {
var testPlugin = function() {
this.testFunc = function() {};
};
expect(Push.extend.bind(Push, testPlugin)).toThrow();
});
it('accept valid plugin manifests', function() {
var testPlugin = {
plugin: function() {
this.testFunc = function() {};
}
};
Push.extend(testPlugin);
expect(Push.testFunc).toBeDefined();
});
it('only allow object-based configs', function() {
spyOn(window.Push, 'config');
var testPlugin = {
config: null,
plugin: function() {
this.testFunc = function() {};
}
};
Push.extend(testPlugin);
expect(Push.config.calls.count()).toEqual(1); // config() is called one by default in extend()
var testPlugin2 = {
config: {},
plugin: function() {
this.testFunc = function() {};
}
};
Push.extend(testPlugin2);
expect(Push.config.calls.count()).toBeGreaterThan(1);
});
});
describe('changing configuration', function() {
it('returns the current configuration if no parameters passed', function() {
var output = {
serviceWorker: TEST_SW_DEFAULT,
fallback: function(payload) {}
};
var configKeys = Object.keys(Push.config());
Object.keys(output).forEach(function(k) {
expect(configKeys.includes(k)).toBeTruthy();
});
});
it('adds a configuration if one is specified', function() {
Push.config({
a: 1
});
expect(Push.config().a).toBeDefined();
expect(Push.config().a).toBe(1);
});
it('should be capable of performing a deep merge', function() {
var input1 = {
b: {
c: 1,
d: {
e: 2,
f: 3
}
}
};
var input2 = {
b: {
d: {
e: 2,
f: 4
},
g: 5
}
};
var output = {
c: 1,
d: {
e: 2,
f: 4
},
g: 5
};
Push.config(input1);
Push.config(input2);
expect(Push.config().b).toBeDefined();
expect(JSON.stringify(Push.config().b)).toBe(JSON.stringify(output));
});
});
if (Push.supported()) {
Push.config({
serviceWorker: TEST_SW_DEFAULT
});
function initRequestSpy(granted) {
var param_str, param_int;
param_str = granted ? Push.Permission.GRANTED : Push.Permission.DEFAULT;
param_int = granted ? 0 : 1;
/* Safari 6+, Legacy webkit browsers */
if (
window.webkitNotifications &&
window.webkitNotifications.checkPermission
) {
spyOn(window.webkitNotifications, 'requestPermission').and.callFake(
function(cb) {
cb(param_int);
}
);
} else if (
/* Chrome 23+ */
window.Notification &&
window.Notification.requestPermission
) {
spyOn(window.Notification, 'requestPermission').and.callFake(
function() {
return new Promise(function(resolve) {
resolve(param_str);
});
}
);
}
}
function getRequestObject() {
var obj = {};
/* Safari 6+, Legacy webkit browsers */
if (
window.webkitNotifications &&
window.webkitNotifications.checkPermission
)
return window.webkitNotifications.requestPermission;
/* Chrome 23+ */ else if (
window.Notification &&
window.Notification.requestPermission
)
return window.Notification.requestPermission;
return null;
}
describe('initialization', function() {
it('should create a new instance', function() {
expect(window.Push !== undefined).toBeTruthy();
});
it('isSupported should return a boolean', function() {
expect(typeof Push.supported()).toBe('boolean');
});
});
describe('permission', function() {
var callback; // Callback spy
beforeAll(function() {
jasmine.clock().uninstall();
jasmine.clock().install();
});
beforeEach(function() {
callback = jasmine.createSpy('callback');
});
it('should have permission stored as a string varant', function() {
expect(typeof Push.Permission.get()).toBe('string');
});
it('should update permission value if permission is denied and execute callback (deprecated)', function(done) {
spyOn(Push.Permission, 'get').and.returnValue(
Push.Permission.DEFAULT
);
initRequestSpy(false);
Push.Permission.request(NOOP, function() {
callback();
expect(Push.Permission.has()).toBe(false);
expect(callback).toHaveBeenCalled();
done();
});
});
it('should update permission value if permission is denied and execute callback (with promise)', function(done) {
spyOn(Push.Permission, 'get').and.returnValue(
Push.Permission.DEFAULT
);
initRequestSpy(false);
Push.Permission.request()
.then(NOOP)
.catch(function() {
callback();
expect(callback).toHaveBeenCalled();
expect(Push.Permission.has()).toBe(false);
done();
});
});
it('should request permission if permission is not granted', function() {
spyOn(Push.Permission, 'get').and.returnValue(
Push.Permission.DEFAULT
);
initRequestSpy(false);
Push.create(TEST_TITLE).then(function() {
expect(getRequestObject()).toHaveBeenCalled();
});
});
it('should update permission value if permission is granted and execute callback (deprecated)', function() {
spyOn(Push.Permission, 'get').and.returnValue(
Push.Permission.GRANTED
);
initRequestSpy(true);
Push.Permission.request(callback, NOOP);
jasmine.clock().tick(TEST_TIMEOUT);
expect(Push.Permission.has()).toBe(true);
expect(callback).toHaveBeenCalled();
});
it('should update permission value if permission is granted and execute callback (with promise)', function() {
spyOn(Push.Permission, 'get').and.returnValue(
Push.Permission.GRANTED
);
initRequestSpy(true);
Push.Permission.request();
jasmine.clock().tick(TEST_TIMEOUT);
expect(Push.Permission.has()).toBe(true);
});
it('should not request permission if permission is already granted', function() {
spyOn(Push.Permission, 'get').and.returnValue(
Push.Permission.GRANTED
);
initRequestSpy(true);
Push.Permission.request();
Push.create(TEST_TITLE)
.then(function() {
expect(getRequestObject()).not.toHaveBeenCalled();
})
.catch(function() {});
});
});
describe('creating notifications', function() {
beforeAll(function() {
jasmine.clock().uninstall();
jasmine.clock().install();
spyOn(Push.Permission, 'get').and.returnValue(
Push.Permission.DEFAULT
);
initRequestSpy(true);
});
beforeEach(function() {
Push.clear();
});
it('should throw exception if no title is provided', function() {
expect(function() {
Push.create();
}).toThrow();
});
it('should return a valid notification wrapper', function(done) {
Push.create(TEST_TITLE).then(function(wrapper) {
expect(wrapper).not.toBe(undefined);
expect(wrapper.get).not.toBe(undefined);
expect(wrapper.close).not.toBe(undefined);
done();
});
});
it('should return promise successfully', function() {
var promise = Push.create(TEST_TITLE)
.then(function() {
expect(promise.then).not.toBe(undefined);
})
.catch(function() {});
});
it('should pass in all API options correctly', function(done) {
// Vibrate omitted because Firefox will default to using the Notification API, not service workers
// Timeout, requestPermission, and event listeners also omitted from this src :(
Push.create(TEST_TITLE, {
body: TEST_BODY,
icon: TEST_ICON,
tag: TEST_TAG,
silent: true
}).then(function(wrapper) {
var notification = wrapper.get();
// Some browsers, like Safari, choose to omit this info
if (notification.title)
expect(notification.title).toBe(TEST_TITLE);
if (notification.body)
expect(notification.body).toBe(TEST_BODY);
if (notification.icon)
expect(notification.icon).toContain(TEST_ICON); // Some browsers append the document location, so we gotta use toContain()
expect(notification.tag).toBe(TEST_TAG);
if (notification.hasOwnProperty('silent')) {
expect(notification.silent).toBe(true);
}
done();
});
});
it('should return the increase the notification count', function(done) {
expect(Push.count()).toBe(0);
Push.create(TEST_TITLE).then(function() {
expect(Push.count()).toBe(1);
done();
});
});
});
describe('event listeners', function() {
var callback, // callback spy
testListener = function(name, cb) {
var event = new Event(name),
options = {},
key,
promise;
key =
'on' +
name[0].toUpperCase() +
name.substr(1, name.length - 1);
options[key] = callback;
Push.create(TEST_TITLE, options)
.then(function(wrapper) {
var notification = wrapper.get();
notification.dispatchEvent(event);
expect(callback).toHaveBeenCalled();
cb();
})
.catch(function() {});
};
beforeAll(function() {
initRequestSpy(true);
});
beforeEach(function() {
callback = jasmine.createSpy('callback');
spyOn(Push.Permission, 'get').and.returnValue(
Push.Permission.DEFAULT
);
});
it('should execute onClick listener correctly', function(done) {
testListener('click', done);
});
it('should execute onShow listener correctly', function(done) {
testListener('show', done);
});
it('should execute onError listener correctly', function(done) {
testListener('error', done);
});
it('should execute onClose listener correctly', function(done) {
testListener('close', done);
});
});
describe('closing notifications', function() {
var callback; // Callback spy
var closeSpy;
beforeAll(function() {
initRequestSpy(true);
jasmine.clock().uninstall();
jasmine.clock().install();
});
beforeEach(function() {
closeSpy = spyOn(window.Notification.prototype, 'close');
spyOn(Push.Permission, 'get').and.returnValue(
Push.Permission.DEFAULT
);
closeSpy.calls.reset();
Push.clear();
callback = jasmine.createSpy('callback');
});
it('should close notifications on close callback', function(done) {
Push.create(TEST_TITLE, {
onClose: callback
}).then(function(wrapper) {
var notification = wrapper.get();
expect(Push.count()).toBe(1);
notification.dispatchEvent(new Event('close'));
expect(Push.count()).toBe(0);
done();
});
});
it('should close notifications using wrapper', function(done) {
Push.create(TEST_TITLE, {
onClose: callback
}).then(function(wrapper) {
expect(Push.count()).toBe(1);
wrapper.close();
expect(closeSpy).toHaveBeenCalled();
expect(Push.count()).toBe(0);
done();
});
});
it('should close notifications using given timeout', function(done) {
Push.create(TEST_TITLE, {
timeout: TEST_TIMEOUT
}).then(function() {
jasmine.clock().tick(500);
expect(Push.count()).toBe(1);
expect(closeSpy).not.toHaveBeenCalled();
jasmine.clock().tick(TEST_TIMEOUT);
expect(closeSpy).toHaveBeenCalled();
expect(Push.count()).toBe(0);
done();
});
});
it('should close a notification given a tag', function(done) {
Push.create(TEST_TITLE, {
tag: TEST_TAG
}).then(function() {
expect(Push.count()).toBe(1);
expect(Push.close(TEST_TAG)).toBeTruthy();
expect(closeSpy).toHaveBeenCalled();
expect(Push.count()).toBe(0);
done();
});
});
it('should close all notifications when cleared', function(done) {
Promise.all([
Push.create(TEST_TITLE, {
tag: TEST_TAG
}),
Push.create('hello world!', {
tag: TEST_TAG_2
})
]).then(function() {
expect(Push.count()).toBeGreaterThan(0);
expect(Push.clear()).toBeTruthy();
expect(closeSpy).toHaveBeenCalled();
expect(Push.count()).toBe(0);
done();
});
});
});
} else {
describe('fallback functionality', function() {
it('should ensure fallback method fires correctly', function(done) {
var fallback = jasmine.createSpy('fallback');
Push.config({
fallback: fallback
});
Push.create(TEST_TITLE).then(function() {
expect(fallback).toHaveBeenCalled();
done();
});
});
it('should ensure all notification options are passed to the fallback', function(done) {
Push.config({
fallback: function(payload) {
expect(payload.title).toBe(TEST_TITLE);
expect(payload.body).toBe(TEST_BODY);
expect(payload.icon).toBe(TEST_ICON);
expect(payload.tag).toBe(TEST_TAG);
done();
}
});
Push.create(TEST_TITLE, {
body: TEST_BODY,
icon: TEST_ICON,
tag: TEST_TAG
}).catch(function() {});
});
});
}
gitextract_6ktotom7/
├── .babelrc
├── .browserslistrc
├── .codeclimate.yml
├── .csslintrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.yml
├── .flowconfig
├── .gitignore
├── .npmignore
├── .nvmrc
├── .prettierrc
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── bin/
│ └── push.js
├── bower.json
├── index.d.ts
├── package.json
├── rollup.config.js
├── src/
│ ├── agents/
│ │ ├── AbstractAgent.js
│ │ ├── DesktopAgent.js
│ │ ├── MSAgent.js
│ │ ├── MobileChromeAgent.js
│ │ ├── MobileFirefoxAgent.js
│ │ ├── WebKitAgent.js
│ │ └── index.js
│ ├── index.js
│ ├── push/
│ │ ├── Messages.js
│ │ ├── Permission.js
│ │ ├── Push.js
│ │ ├── Util.js
│ │ └── index.js
│ ├── serviceWorker.js
│ └── types.js
└── tests/
├── browsers.conf.js
├── karma.conf.js
└── push.tests.js
SYMBOL INDEX (69 symbols across 14 files)
FILE: bin/push.js
function t (line 38) | function t(i){return(t="function"==typeof Symbol&&"symbol"==typeof Symbo...
function n (line 38) | function n(i,t){if(!(i instanceof t))throw new TypeError("Cannot call a ...
function e (line 38) | function e(i,t){for(var n=0;n<t.length;n++){var e=t[n];e.enumerable=e.en...
function o (line 38) | function o(i,t,n){return t&&e(i.prototype,t),n&&e(i,n),i}
function r (line 38) | function r(i,t){if("function"!=typeof t&&null!==t)throw new TypeError("S...
function s (line 38) | function s(i){return(s=Object.setPrototypeOf?Object.getPrototypeOf:funct...
function c (line 38) | function c(i,t){return(c=Object.setPrototypeOf||function(i,t){return i._...
function a (line 38) | function a(i,t){return!t||"object"!=typeof t&&"function"!=typeof t?funct...
function i (line 38) | function i(t){n(this,i),this._win=t,this.GRANTED="granted",this.DEFAULT=...
function i (line 38) | function i(){n(this,i)}
function t (line 38) | function t(){return n(this,t),a(this,s(t).apply(this,arguments))}
function e (line 38) | function e(){return n(this,e),a(this,s(e).apply(this,arguments))}
function t (line 38) | function t(){return n(this,t),a(this,s(t).apply(this,arguments))}
function t (line 38) | function t(){return n(this,t),a(this,s(t).apply(this,arguments))}
function t (line 38) | function t(){return n(this,t),a(this,s(t).apply(this,arguments))}
function t (line 38) | function t(i){n(this,t),this._currentId=0,this._notifications={},this._w...
FILE: index.d.ts
class Push (line 6) | class Push {
type PushNotificationParams (line 18) | interface PushNotificationParams {
type PushParams (line 31) | interface PushParams {
type PushPermission (line 36) | interface PushPermission {
type PushNotification (line 48) | interface PushNotification {
FILE: src/agents/AbstractAgent.js
class AbstractAgent (line 4) | class AbstractAgent {
method constructor (line 7) | constructor(win: Global) {
FILE: src/agents/DesktopAgent.js
class DesktopAgent (line 10) | class DesktopAgent extends AbstractAgent {
method isSupported (line 17) | isSupported() {
method create (line 27) | create(title: string, options: PushOptions) {
method close (line 45) | close(notification: GenericNotification) {
FILE: src/agents/MSAgent.js
class MSAgent (line 9) | class MSAgent extends AbstractAgent {
method isSupported (line 16) | isSupported() {
method create (line 29) | create(title: string, options: PushOptions) {
method close (line 49) | close() {
FILE: src/agents/MobileChromeAgent.js
class MobileChromeAgent (line 10) | class MobileChromeAgent extends AbstractAgent {
method isSupported (line 17) | isSupported() {
method getFunctionBody (line 28) | getFunctionBody(func: () => void) {
method create (line 43) | create(
method close (line 109) | close() {
FILE: src/agents/MobileFirefoxAgent.js
class MobileFirefoxAgent (line 9) | class MobileFirefoxAgent extends AbstractAgent {
method isSupported (line 16) | isSupported() {
method create (line 26) | create(title: string, options: PushOptions) {
FILE: src/agents/WebKitAgent.js
class WebKitAgent (line 8) | class WebKitAgent extends AbstractAgent {
method isSupported (line 15) | isSupported() {
method create (line 25) | create(title: string, options: PushOptions) {
method close (line 41) | close(notification: GenericNotification) {
FILE: src/push/Permission.js
class Permission (line 4) | class Permission {
method constructor (line 14) | constructor(win: Global) {
method request (line 28) | request(onGranted: () => void, onDenied: () => void) {
method _requestWithCallback (line 41) | _requestWithCallback(onGranted: () => void, onDenied: () => void) {
method _requestAsPromise (line 89) | _requestAsPromise(): Promise<void> {
method has (line 138) | has() {
method get (line 146) | get() {
FILE: src/push/Push.js
method let (line 284) | let promiseCallback;
method notification (line 336) | notification;
FILE: src/push/Util.js
class Util (line 2) | class Util {
method isUndefined (line 3) | static isUndefined(obj) {
method isNull (line 7) | static isNull(obs) {
method isString (line 11) | static isString(obj) {
method isFunction (line 15) | static isFunction(obj) {
method isObject (line 19) | static isObject(obj) {
method objectMerge (line 23) | static objectMerge(target, source) {
FILE: src/serviceWorker.js
function isFunction (line 3) | function isFunction(obj) {
function runFunctionString (line 7) | function runFunctionString(funcStr) {
FILE: tests/browsers.conf.js
function getWindowsDesktop (line 8) | function getWindowsDesktop(browser, version) {
function getOSXDesktop (line 18) | function getOSXDesktop(browser, version, os) {
function getMobile (line 36) | function getMobile(browser) {
FILE: tests/push.tests.js
function isBrowser (line 17) | function isBrowser(browser) {
function getVersion (line 21) | function getVersion() {
function isSupported (line 25) | function isSupported() {
function initRequestSpy (line 178) | function initRequestSpy(granted) {
function getRequestObject (line 209) | function getRequestObject() {
Condensed preview — 39 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (98K chars).
[
{
"path": ".babelrc",
"chars": 656,
"preview": "{\n \"presets\": [\n [\n \"@babel/preset-env\",\n {\n \"modules\": false\n }\n ],\n \"@babel/preset-flo"
},
{
"path": ".browserslistrc",
"chars": 148,
"preview": "Firefox >= 22\nChrome >= 5\nSafari >= 6\nOpera >= 25\nAndroid >= 4.4\nBlackberry >= 10\nOperaMobile >= 37\nFirefoxAndroid >= 47"
},
{
"path": ".codeclimate.yml",
"chars": 387,
"preview": "---\nengines:\n csslint:\n enabled: true\n duplication:\n enabled: true\n config:\n languages:\n - ruby\n "
},
{
"path": ".csslintrc",
"chars": 107,
"preview": "--exclude-exts=.min.css\n--ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes\n"
},
{
"path": ".editorconfig",
"chars": 180,
"preview": "# editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 4\ntab_width = 2\nend_of_line = lf\ncharset = utf-8\n"
},
{
"path": ".eslintignore",
"chars": 16,
"preview": "**/*{.,-}min.js\n"
},
{
"path": ".eslintrc.yml",
"chars": 6056,
"preview": "---\nparserOptions:\n sourceType: module\n ecmaFeatures:\n jsx: true\n\nenv:\n amd: true\n browser: true\n es6: true\n jq"
},
{
"path": ".flowconfig",
"chars": 468,
"preview": "[ignore]\n.*/node_modules/.*\n\n[include]\n.*/src/.*\n\n[options]\n# Add emojis to status messages because emojis are cool 😎\nem"
},
{
"path": ".gitignore",
"chars": 1629,
"preview": "# Created by https://www.gitignore.io\n\n### OSX ###\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n"
},
{
"path": ".npmignore",
"chars": 3,
"preview": ".*\n"
},
{
"path": ".nvmrc",
"chars": 5,
"preview": "v8.12"
},
{
"path": ".prettierrc",
"chars": 70,
"preview": "singleQuote: true\nsemi: true\nuseTabs: false\ntabWidth: 4\nprintWidth: 80"
},
{
"path": ".travis.yml",
"chars": 1526,
"preview": "env:\n global:\n - CC_TEST_REPORTER_ID=71e04b5c32896d7201c79e2db2af1a89fc375f668392802961019875e71b179f\n - GIT_COMMITTE"
},
{
"path": "CONTRIBUTING.md",
"chars": 1969,
"preview": "# Contributing Guidelines\n\nSo you want to contribute to Push, huh? Well lucky for you, it's really easy to do so, becaus"
},
{
"path": "LICENSE.md",
"chars": 1084,
"preview": "# The MIT License (MIT)\n\nCopyright (c) 2016 Tyler Nickerson\n\nPermission is hereby granted, free of charge, to any person"
},
{
"path": "README.md",
"chars": 3133,
"preview": "<div align=\"center\">\n \n[<img src=\"https://raw.githubusercontent.com/Nickersoft/push.js/master/logo.png\" width=\"250\">]"
},
{
"path": "bin/push.js",
"chars": 12926,
"preview": "/**\n * @license\n *\n * Push v1.0.9\n * =========\n * A compact, cross-browser solution for the JavaScript Notifications API"
},
{
"path": "bower.json",
"chars": 394,
"preview": "{\n \"name\": \"push.js\",\n \"description\": \"A compact, cross-browser solution for the Javascript Notifications API\",\n \"mai"
},
{
"path": "index.d.ts",
"chars": 1068,
"preview": "declare module 'push.js' {\n\n const defaultPush: Push;\n export default defaultPush;\n\n class Push {\n Permi"
},
{
"path": "package.json",
"chars": 3182,
"preview": "{\n \"name\": \"push.js\",\n \"version\": \"1.0.12\",\n \"description\": \"A compact, cross-browser solution for the Javascri"
},
{
"path": "rollup.config.js",
"chars": 2416,
"preview": "import path from \"path\";\nimport resolve from \"rollup-plugin-node-resolve\";\nimport babel from \"rollup-plugin-babel\";\nimpo"
},
{
"path": "src/agents/AbstractAgent.js",
"chars": 167,
"preview": "// @flow\nimport type { Global } from 'types';\n\nexport default class AbstractAgent {\n _win: Global;\n\n constructor(w"
},
{
"path": "src/agents/DesktopAgent.js",
"chars": 1391,
"preview": "// @flow\nimport { AbstractAgent } from 'agents';\nimport { Util } from 'push';\nimport type { PushOptions, GenericNotifica"
},
{
"path": "src/agents/MSAgent.js",
"chars": 1354,
"preview": "// @flow\nimport { AbstractAgent } from 'agents';\nimport { Util } from 'push';\nimport type { PushOptions, Global } from '"
},
{
"path": "src/agents/MobileChromeAgent.js",
"chars": 3884,
"preview": "// @flow\nimport { Util, Messages } from 'push';\nimport { AbstractAgent } from 'agents';\nimport type { Global, GenericNot"
},
{
"path": "src/agents/MobileFirefoxAgent.js",
"chars": 983,
"preview": "// @flow\nimport { AbstractAgent } from 'agents';\nimport type { Global, PushOptions } from 'types';\n\n/**\n * Notification "
},
{
"path": "src/agents/WebKitAgent.js",
"chars": 1136,
"preview": "// @flow\nimport { AbstractAgent } from 'agents';\nimport type { Global, GenericNotification, PushOptions } from 'types';\n"
},
{
"path": "src/agents/index.js",
"chars": 396,
"preview": "import AbstractAgent from './AbstractAgent';\nimport DesktopAgent from './DesktopAgent';\nimport MobileChromeAgent from '."
},
{
"path": "src/index.js",
"chars": 113,
"preview": "// @flow\nimport { Push } from 'push';\n\nexport default new Push(typeof window !== 'undefined' ? window : global);\n"
},
{
"path": "src/push/Messages.js",
"chars": 781,
"preview": "// @flow\nconst errorPrefix = 'PushError:';\n\nexport default {\n errors: {\n incompatible: `${errorPrefix} Push.js"
},
{
"path": "src/push/Permission.js",
"chars": 5788,
"preview": "// @flow\nimport type { Global } from 'types';\n\nexport default class Permission {\n // Private members\n _permissions"
},
{
"path": "src/push/Push.js",
"chars": 12601,
"preview": "// @flow\nimport { Messages, Permission, Util } from 'push';\nimport type { PluginManifest, GenericNotification, PushOptio"
},
{
"path": "src/push/Util.js",
"chars": 821,
"preview": "// @flow\nexport default class Util {\n static isUndefined(obj) {\n return obj === undefined;\n }\n\n static i"
},
{
"path": "src/push/index.js",
"chars": 174,
"preview": "import Messages from './Messages';\nimport Permission from './Permission';\nimport Util from './Util';\nimport Push from '."
},
{
"path": "src/serviceWorker.js",
"chars": 2721,
"preview": "'use strict';\n\nfunction isFunction(obj) {\n return obj && {}.toString.call(obj) === '[object Function]';\n}\n\nfunction r"
},
{
"path": "src/types.js",
"chars": 498,
"preview": "export type GenericNotification = Notification | webkitNotifications;\n\nexport type Global = {\n Notification?: Notific"
},
{
"path": "tests/browsers.conf.js",
"chars": 1608,
"preview": "var BROWSER_FIREFOX = 'Firefox',\n BROWSER_CHROME = 'Chrome',\n BROWSER_EDGE = 'Edge',\n BROWSER_IE = 'ie',\n BR"
},
{
"path": "tests/karma.conf.js",
"chars": 2975,
"preview": "// Karma configuration\n// Generated on Tue Jul 21 2015 22:34:30 GMT-0400 (EDT)\nlet browsers, selected_browsers;\n\nbrowser"
},
{
"path": "tests/push.tests.js",
"chars": 18175,
"preview": "var BROWSER_CHROME = 'Chrome',\n BROWSER_FIREFOX = 'Firefox',\n BROWSER_EDGE = 'Edge',\n BROWSER_OPERA = 'Opera',\n"
}
]
About this extraction
This page contains the full source code of the Nickersoft/push.js GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 39 files (90.8 KB), approximately 22.3k tokens, and a symbol index with 69 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.