├── .gitignore
├── .jshintignore
├── .jshintrc
├── .travis.yml
├── Gruntfile.coffee
├── LICENSE
├── README.md
├── build
└── tasks
│ ├── clean.coffee
│ ├── compress.coffee
│ ├── copy.coffee
│ ├── env.coffee
│ ├── es6.coffee
│ ├── istanbul.coffee
│ ├── jshint.coffee
│ ├── mocha.coffee
│ ├── shell.coffee
│ ├── stylus.coffee
│ └── watch.coffee
├── chrome-extension
├── _locales
│ ├── en
│ │ └── messages.json
│ └── es
│ │ └── messages.json
├── key.pem
├── manifest.json
└── test
│ └── configure.js
├── docs
├── Modules.md
├── NewComponent.md
├── NewPage.md
├── README.md
├── WebdriverReference.md
├── _assets
│ ├── activity.png
│ ├── background.png
│ ├── component.png
│ ├── contentscript.png
│ ├── dependencies.json
│ ├── dom.png
│ ├── environment.png
│ ├── extension.png
│ ├── index.png
│ ├── notifications.png
│ ├── overview.png
│ ├── storage.png
│ ├── tabs.png
│ └── watcher.png
└── modules
│ ├── background.md
│ ├── contentscript.md
│ └── lib
│ ├── activity.md
│ ├── component.md
│ ├── components
│ ├── donation-goal.md
│ ├── log-table.md
│ └── reminders.md
│ ├── dom.md
│ ├── environment.md
│ ├── extension.md
│ ├── index.md
│ ├── notifications.md
│ ├── pages
│ ├── donations.md
│ ├── getting-started.md
│ ├── log.md
│ └── settings.md
│ ├── storage.md
│ ├── tabs.md
│ └── watcher.md
├── package.json
├── shared
├── fonts
│ ├── FontAwesome.otf
│ ├── fontawesome-webfont.eot
│ ├── fontawesome-webfont.svg
│ ├── fontawesome-webfont.ttf
│ └── fontawesome-webfont.woff
├── html
│ └── index.html
├── img
│ ├── bitcoin.png
│ ├── bitcoin_pay.png
│ ├── dwolla.png
│ ├── dwolla_pay.png
│ ├── logo19.png
│ ├── logo48.png
│ ├── logo64.png
│ ├── paypal.png
│ ├── settings.png
│ └── stripe.png
├── scripts
│ ├── background.js
│ ├── contentscript.js
│ └── lib
│ │ ├── activity.js
│ │ ├── component.js
│ │ ├── components
│ │ ├── donation-goal
│ │ │ ├── donation-goal.html
│ │ │ ├── donation-goal.js
│ │ │ └── donation-goal.styl
│ │ ├── log-table
│ │ │ ├── entry-history.html
│ │ │ ├── log-table.html
│ │ │ ├── log-table.js
│ │ │ └── log-table.styl
│ │ ├── reminders
│ │ │ ├── reminder-interval.html
│ │ │ ├── reminder-interval.js
│ │ │ ├── reminder-interval.styl
│ │ │ ├── reminder-thresh-global.html
│ │ │ ├── reminder-thresh-global.js
│ │ │ ├── reminder-thresh-global.styl
│ │ │ ├── reminder-thresh-local.html
│ │ │ ├── reminder-thresh-local.js
│ │ │ └── reminder-thresh-local.styl
│ │ └── user-agreement
│ │ │ ├── user-agreement.html
│ │ │ ├── user-agreement.js
│ │ │ └── user-agreement.styl
│ │ ├── defaults.js
│ │ ├── dom.js
│ │ ├── environment.js
│ │ ├── extension.js
│ │ ├── hardcoded-doms.js
│ │ ├── identifier.js
│ │ ├── index.js
│ │ ├── notifications.js
│ │ ├── pages
│ │ ├── donations
│ │ │ ├── donations.html
│ │ │ ├── donations.js
│ │ │ ├── donations.styl
│ │ │ └── entry-donation.html
│ │ ├── getting-started
│ │ │ ├── getting-started.html
│ │ │ ├── getting-started.js
│ │ │ └── getting-started.styl
│ │ ├── log
│ │ │ ├── log.html
│ │ │ ├── log.js
│ │ │ └── log.styl
│ │ └── settings
│ │ │ ├── settings.html
│ │ │ ├── settings.js
│ │ │ └── settings.styl
│ │ ├── processors
│ │ ├── dwolla.js
│ │ └── paypal.js
│ │ ├── server-requests.js
│ │ ├── storage.js
│ │ ├── tabs.js
│ │ ├── utils
│ │ ├── calculate.js
│ │ ├── js-yaml.js
│ │ └── tipsy-txt-parser.js
│ │ └── watcher.js
├── styles
│ ├── index.styl
│ └── table-sorting.styl
└── vendor
│ ├── deparam.js
│ ├── dwolla.js
│ └── font-awesome.min.css
├── test
├── integration
│ ├── extension-driver.js
│ ├── setup.js
│ └── tests
│ │ ├── basic.js
│ │ └── watcher.js
├── normalizePaths.js
└── unit
│ ├── runner.js
│ └── tests
│ └── storage.js
└── wp-tipsy-payment-info
├── ReadMe.txt
└── TipsyInjection.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /build/tools
2 | /bower_components
3 | /node_modules
4 | /chrome-extension/dist
5 | /cjs
6 | /test/coverage
7 |
--------------------------------------------------------------------------------
/.jshintignore:
--------------------------------------------------------------------------------
1 | shared/vendor/dwolla.js
2 | shared/vendor/deparam.js
3 | shared/scripts/lib/utils/js-yaml.js
4 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "eqnull": true,
3 | "node": true,
4 | "browser": true,
5 | "esnext": true,
6 | "proto": true,
7 | "globals": {
8 | "$": true,
9 | "chrome": true,
10 | "self": true,
11 | "combyne": true,
12 | "moment": true,
13 | "deparam": true,
14 | "Tablesort": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # Install Chrome in Travis-CI.
2 | dist: trusty
3 | sudo: required
4 |
5 | addons:
6 | apt:
7 | sources:
8 | - google-chrome
9 | packages:
10 | - google-chrome-stable
11 |
12 | before_script:
13 | - "export DISPLAY=:99.0"
14 | - "sh -e /etc/init.d/xvfb start"
15 | - "sleep 3"
16 | - "cat /etc/init.d/xvfb"
17 | script: npm run test
18 |
19 | language: node_js
20 | node_js:
21 | - "6.1"
22 | env:
23 | global:
24 | - secure: "ZUeyY/wcPM40iP7eGZxPCrOEOBjjhvW9tZJ5ZLxD8n4qB3HVdcU/tstUSuBF/GgTG7BgnLC3TMUP2l6pP6li/JOxfHXrEqVNwrG8VHhpVNMdogVaL/3d0UER/s2cT2ioU7B3bkc4YFc1vR67h235KLaVip8YhMpcF1hhR1P7SC0="
25 | - secure: "q5oQpQRFCAfe+y6FgG7Jie48H2glHdAmatbanlYv35WVOwIk2G2SEzKrqSmMgx9mEwWhgK80+8i1bhj85x9uLCXPgJuCTqX4tjdqpL5GcQ354LDveHaXI4NjynrrUg8LF4VtEn8SOQOok7DWcI/9kcAGVfnZmlPQ3fiUdJlcXEE="
26 |
--------------------------------------------------------------------------------
/Gruntfile.coffee:
--------------------------------------------------------------------------------
1 | module.exports = ->
2 | @loadTasks 'build/tasks'
3 |
4 | @registerTask 'default', [
5 | 'jshint'
6 | 'chrome-extension'
7 | ]
8 |
9 | @registerTask 'coverage', [
10 | 'env:coverage'
11 | 'instrument'
12 | 'test'
13 | 'storeCoverage'
14 | 'makeReport'
15 | ]
16 |
17 | @registerTask 'test', [
18 | 'mochaTest:chrome-extension'
19 | 'mochaTest:shared'
20 | ]
21 |
22 | @registerTask 'chrome-extension', [
23 | 'clean:chrome-extension'
24 | 'compress:chrome-extension'
25 | 'copy:chrome-extension'
26 | 'stylus:chrome-extension'
27 | 'es6:chrome-extension'
28 | 'shell:chrome-extension'
29 | ]
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017 MIT Haystack
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Tipsy
2 | -----
3 |
4 | [](https://travis-ci.org/haystack/tipsy)
5 |
6 |
7 | ### Developing ###
8 |
9 | If you wish to work on Tipsy, you can find instructions on getting started
10 | below. It uses [Node](http://nodejs.org) to install and run the extension.
11 |
12 | At the moment the build process and tests only work in Linux.
13 |
14 | #### Installing Node and dependencies ####
15 |
16 | Go to the [Node](http://nodejs.org) homepage and install for your platform.
17 |
18 | Next, open a command line prompt and enter the project directory. You will
19 | install all development dependencies with one command:
20 |
21 | ``` bash
22 | npm install
23 | ```
24 |
25 | ### Working on Chrome extension ###
26 |
27 | To build the Chrome extension, you will need [Google
28 | Chrome](http://chrome.com/) installed.
29 |
30 | Once it is installed and configured you can build the extension with:
31 |
32 | ``` bash
33 | npm run build-chrome
34 | ```
35 |
36 | #### Loading the unpacked extension ####
37 |
38 | The source code necessary to run the extension as unpacked lives in the
39 | *dist/tipsy* directory and can be dragged into the Extensions tab within
40 | Chrome.
41 |
42 | #### Watching the filesystem for changes ####
43 |
44 | You can have the extension automatically recompiled with:
45 |
46 | ``` shell
47 | npm run watch-chrome
48 | ```
49 |
50 | #### Extension url ####
51 |
52 | chrome-extension://ajcjbhihdfmefgbenbkpgalkjglcbmmp/html/index.html
53 |
54 | #### Watching the filesystem for changes ####
55 |
56 | You can have both extensions automatically recompiled with:
57 |
58 | ``` shell
59 | npm run watch
60 | ```
61 |
--------------------------------------------------------------------------------
/build/tasks/clean.coffee:
--------------------------------------------------------------------------------
1 | module.exports = ->
2 | @loadNpmTasks 'grunt-contrib-clean'
3 |
4 | @config 'clean',
5 | 'chrome-extension': ['chrome-extension/dist']
6 |
--------------------------------------------------------------------------------
/build/tasks/compress.coffee:
--------------------------------------------------------------------------------
1 | module.exports = ->
2 | @loadNpmTasks 'grunt-contrib-compress'
3 |
4 | @config 'compress',
5 | 'chrome-extension':
6 | options:
7 | archive: 'chrome-extension/dist/tipsy.zip'
8 | mode: 'zip'
9 |
10 | files: [
11 | { src: ['node_modules/purecss/*'], dest: '.' }
12 | { src: ['**/*'], expand: true, cwd: 'shared' }
13 | {
14 | src: [
15 | 'key.pem'
16 | '_locales/**'
17 | ]
18 | expand: true
19 | cwd: 'chrome-extension'
20 | }
21 | ]
22 |
--------------------------------------------------------------------------------
/build/tasks/copy.coffee:
--------------------------------------------------------------------------------
1 | module.exports = ->
2 | @loadNpmTasks 'grunt-contrib-copy'
3 |
4 | chromeDest = 'chrome-extension/dist/tipsy'
5 |
6 | npmDeps = [
7 | 'node_modules/jquery/dist/*',
8 | 'node_modules/purecss/build/*',
9 | 'node_modules/combyne/dist/*'
10 | 'node_modules/moment/min/*'
11 | 'node_modules/tablesort/*'
12 | ]
13 |
14 | @config 'copy',
15 | 'chrome-extension':
16 | files: [
17 | {
18 | src: npmDeps
19 | expand: true
20 | dest: chromeDest
21 | }
22 | {
23 | src: [
24 | '**/*'
25 | '!_assets/**'
26 | ]
27 | expand: true
28 | cwd: 'shared'
29 | dest: chromeDest
30 | }
31 | {
32 | src: [
33 | 'manifest.json'
34 | '_locales/**'
35 | ]
36 | expand: true
37 | cwd: 'chrome-extension'
38 | dest: chromeDest
39 | }
40 | ]
41 |
--------------------------------------------------------------------------------
/build/tasks/env.coffee:
--------------------------------------------------------------------------------
1 | module.exports = ->
2 | @loadNpmTasks 'grunt-env'
3 |
4 | @config 'env',
5 | coverage:
6 | CODE_COV: true
7 |
--------------------------------------------------------------------------------
/build/tasks/es6.coffee:
--------------------------------------------------------------------------------
1 | transpiler = require 'es6-module-transpiler'
2 |
3 | buildES6 = (options) ->
4 | container = new transpiler.Container(
5 | resolvers: [new transpiler.FileResolver([options.path])]
6 | formatter: new transpiler.formatters.bundle
7 | )
8 |
9 | container.getModule options.module
10 |
11 | if options.target is "chrome-extension"
12 | container.write "chrome-extension/dist/tipsy/" + options.chrome
13 |
14 | module.exports = ->
15 | @registerTask 'es6', 'Compiles ES6 modules.', ->
16 |
17 | target = @args[0]
18 |
19 | # Extension.
20 | buildES6
21 | target: target
22 | path: 'shared/scripts/lib'
23 | module: 'index'
24 | chrome: 'js/tipsy.js'
25 |
26 | # Background.
27 | buildES6
28 | target: target
29 | path: 'shared/scripts'
30 | module: 'background'
31 | chrome: 'js/background.js'
32 |
33 | # ContentScript.
34 | buildES6
35 | target: target
36 | path: 'shared/scripts'
37 | module: 'contentscript'
38 | chrome: 'js/contentscript.js'
39 |
--------------------------------------------------------------------------------
/build/tasks/istanbul.coffee:
--------------------------------------------------------------------------------
1 | istanbulTraceur = require 'istanbul-traceur'
2 |
3 | module.exports = ->
4 | @loadNpmTasks 'grunt-istanbul'
5 |
6 | # Inject the Istanbul Traceur version to provide proper ES6 coverage.
7 | task = require.cache[require.resolve('istanbul')]
8 | task.exports.Instrumenter = istanbulTraceur.Instrumenter
9 |
10 | @config 'instrument',
11 | options:
12 | basePath: 'test/coverage/instrument'
13 |
14 | files: 'shared/**/*.js'
15 |
16 | @config 'storeCoverage',
17 | options:
18 | dir: 'test/coverage/reports'
19 |
20 | @config 'makeReport',
21 | options:
22 | type: 'lcov'
23 | dir: 'test/coverage/reports'
24 |
25 | src: 'test/coverage/reports/**/*.json'
26 |
--------------------------------------------------------------------------------
/build/tasks/jshint.coffee:
--------------------------------------------------------------------------------
1 | module.exports = ->
2 | @loadNpmTasks 'grunt-contrib-jshint'
3 |
4 | # Run your source code through JSHint's defaults.
5 | @config 'jshint',
6 | options:
7 | jshintrc: '.jshintrc'
8 |
9 | files: [
10 | 'shared/**/*.js'
11 | ]
12 |
--------------------------------------------------------------------------------
/build/tasks/mocha.coffee:
--------------------------------------------------------------------------------
1 | module.exports = ->
2 | @loadNpmTasks 'grunt-mocha-test'
3 |
4 | @config 'mochaTest',
5 | options:
6 | clearRequireCache: true
7 |
8 | 'chrome-extension':
9 | src: [
10 | 'test/integration/setup.js'
11 | 'test/integration/extension-driver.js'
12 | 'chrome-extension/test/configure.js'
13 | 'test/integration/tests/**/*.js'
14 | ]
15 |
16 | 'shared':
17 | src: [
18 | 'test/unit/runner.js'
19 | 'test/unit/tests/**/*.js'
20 | ]
21 |
--------------------------------------------------------------------------------
/build/tasks/shell.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | fs = require 'fs'
3 |
4 | module.exports = ->
5 | @loadNpmTasks 'grunt-shell'
6 |
7 | env = process.env
8 |
9 | chrome = 'echo Skipping Chrome'
10 | python2 = 'echo Skipping Python2'
11 |
12 | # https://code.google.com/p/selenium/wiki/ChromeDriver#Requirements
13 | if process.platform is 'linux'
14 | chrome = '/usr/bin/google-chrome'
15 | if not fs.existsSync chrome
16 | chrome = '/usr/bin/chromium'
17 | else if process.platform is 'darwin'
18 | chrome = '"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"'
19 | else if process.platform is 'win32'
20 | chrome = '"' + 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe' + '"'
21 |
22 | if not fs.existsSync chrome
23 | chrome = '"' + 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' + '"'
24 |
25 | if process.platform is 'linux'
26 | python2 = 'cd build/tools ; grep -Rl python . | xargs sed -ri "s/([^!]|^)python(\\s|$)/\\1python2\\2/g"'
27 |
28 | @config 'shell',
29 | 'chrome-extension':
30 | command: [
31 | chrome
32 | '--pack-extension=' + path.resolve('chrome-extension/dist/tipsy')
33 | '--pack-extension-key=' + path.resolve('chrome-extension/key.pem')
34 | '--no-message-box'
35 | ].join(' ')
36 |
37 | 'python2':
38 | command: python2
39 |
--------------------------------------------------------------------------------
/build/tasks/stylus.coffee:
--------------------------------------------------------------------------------
1 | module.exports = ->
2 | @loadNpmTasks 'grunt-contrib-stylus'
3 |
4 | @config 'stylus',
5 | 'chrome-extension':
6 | files:
7 | 'chrome-extension/dist/tipsy/css/tipsy.css': 'shared/styles/index.styl'
8 |
--------------------------------------------------------------------------------
/build/tasks/watch.coffee:
--------------------------------------------------------------------------------
1 | module.exports = ->
2 | @loadNpmTasks 'grunt-contrib-watch'
3 |
4 | @config 'watch',
5 | 'chrome-extension':
6 | files: [
7 | 'chrome-extension/**/*'
8 | '!chrome-extension/dist/**/*'
9 | 'shared/**/*'
10 | ]
11 |
12 | tasks: [
13 | 'chrome-extension'
14 | 'es6'
15 | ]
16 |
--------------------------------------------------------------------------------
/chrome-extension/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extName": {
3 | "message": "Tipsy",
4 | "description": "Text string (no longer than 45 characters) extension name"
5 | },
6 | "extDescription": {
7 | "message": "Support the creators of the content you love!",
8 | "description": "Text string (no longer than 132 characters) describing the extension. Description appears in chrome://extensions."
9 | },
10 | "browserActionTitle": {
11 | "message": "Tipsy",
12 | "description": "Browser action default_title text string"
13 | }
14 | }
--------------------------------------------------------------------------------
/chrome-extension/_locales/es/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extName": {
3 | "message": "Chrome Extension Base",
4 | "description": "Text string (no longer than 45 characters) extension name"
5 | },
6 | "extDescription": {
7 | "message": "Use as the basis for new extensions!",
8 | "description": "Text string (no longer than 132 characters) describing the extension. Description appears in chrome://extensions."
9 | },
10 | "browserActionTitle": {
11 | "message": "Chrome Extension Base",
12 | "description": "Browser action default_title text string"
13 | }
14 | }
--------------------------------------------------------------------------------
/chrome-extension/key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDdLQrbsHbaohtU
3 | GgB5F3QR6X0ofI6y8WPB4PlhCFX4zK0pp8OxEX4+M/I3lTJFkD3iLmlVO9AYb52E
4 | W0ZLpAzOP0BUipruEIsejB/P0qgzP/gEm2PktQ3QogOaJCZxDFCp20BQX0X9ezNE
5 | wVocvm7GGIEcf2Cjw39bB4Vp7mgHsAsmTxewDsmiLHIBXPBf+Icipiy6tYYmFz/I
6 | yG2MvXuMSVFegfHIBixjRPx/8xmQT1iE3IGI4hLu8PRRNfpb8C+yNRuWQW2gp+LW
7 | WUZPGvTUEilOAwTqqJW7YnSR9TgOK1k8Pp+VGebVV7vwyTh/iAPBnO6ZBga8yOb3
8 | xVqmF45JAgMBAAECggEAYw30KfW7FSm6wYyvn4vQcOE4K3S1WBDh04fVSA66qiXI
9 | e7pl2xxxhJwxI5GPJTZ3cJ/GjuStyvPaANf8AI5lKc2MGxDEWFBSbgjlimbW67T/
10 | d9i8AUbQ/BpDMLp1+PVB/wBxqk0xBFgz2twZZnwnElMRJ9koR8+bbwJMTuf18VKi
11 | AyxSw/Gfx2Pi2UPzc7FBtwEBX2rd1JnSWg3Im6DzhkfgFZcur5KNBm894ynU7GRl
12 | yQve3VznQ3Jzilj2xngb44rPpWl5O3b0Wqnrn1iPTl2vGa0920LD+fYzvkyzYseh
13 | kcAuHpMv/AzWNIJF9TFCoBTsne3rGaylfKhksAt9HQKBgQD7nZKG30f7bI1JcaT4
14 | eJ0J+0ucUL99s/BxFWbE0Sslb+CasW7PHSmgq9oW5hp8zDsLeaLEcobRKsQcwPuF
15 | VIPBddDOMwMKgReUAsdGUGf6Y0FxnFFY7cuUXVWgb5B5meQ1bfY90ghCTT0hRPaS
16 | P5uT3HxIGi7EAgY5EPwQ+zAOwwKBgQDhB67CXykAG6aEiuEw7DWYzN2BJ8j20sbz
17 | 7Whr78bZrjzbJ/3pjsq/ZQJ05DikG6DNa1zWON8cZes1fhl+3Ri7IdyulXtC4yLL
18 | uqYr6HBfCoYG3pVSE6hAo6iz0thHUOI/zNZKT4RVFuTHQ4wDsX/0Rl1SyGXQJuO5
19 | wsPwKsj2AwKBgCy8Q0T/hcjJ8ATS08Xpi+Iub68HHES5LVKtv2vW1Jj/XyuhyFXC
20 | lZgfddMEbkkp9oV/xtSumBGwTNXf6dg2woYu8ET5BN1lPk/ufoed3B7EbupIJJ5v
21 | CPcD8SlpLIKyPcTSHCm5ogZHvUqg/EXcUUjktqQLI61tvrV+s5JBVrYJAoGBANsh
22 | ixm2Zwu24VnSj+X/L1YjsVPTNUy+BoWE25m4PfC+Tn6vm//zUBY/O7wufcW5Lca7
23 | 1QS7DvDtgrVtnVA/55RbLjZIVGbXHow7rxO03rB+Y/OOjuQFRmPjuyWZnYkdB6VP
24 | SCHG+zuM9q3gZhk2oT5zwu8ZPKQNKtc7BWj7kQSXAoGAEe6okFX/7m6sp4KSFuoE
25 | IiQF4tr3T8jTWacn4U/ie14mx15w+D+hxx3510ojXXkPIrkPz5flfdfveQC5wdml
26 | nZCTDgzu9uC6OJgfLI8VjI2kY7RD8rgO1AXx7qrA+ZNONKx128kVsKrEc/NCNsTu
27 | +G3jSyQWwG+JLGxLKIWlk+s=
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/chrome-extension/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "tipsy",
4 | "version": "0.1.12",
5 | "description": "A prototype",
6 | "icons": {
7 | "19": "img/logo19.png",
8 | "48": "img/logo48.png",
9 | "64": "img/logo64.png"
10 | },
11 | "default_locale": "en",
12 | "background": {
13 | "scripts": ["js/background.js"]
14 | },
15 | "browser_action": {
16 | "default_icon": {
17 | "19": "img/logo19.png",
18 | "48": "img/logo48.png",
19 | "64": "img/logo48.png"
20 | },
21 | "default_title": "Tipsy"
22 | },
23 | "content_scripts": [
24 | {
25 | "matches": [
26 | "http://*/*",
27 | "https://*/*"
28 | ],
29 | "js": [
30 | "js/contentscript.js"
31 | ],
32 | "run_at":"document_end"
33 | }
34 | ],
35 | "permissions": [
36 | "alarms",
37 | "tabs",
38 | "history",
39 | "idle",
40 | "storage",
41 | "unlimitedStorage",
42 | "notifications",
43 | "https://www.dwolla.com/",
44 | "https://uat.dwolla.com/"
45 | ],
46 | "web_accessible_resources": [
47 | "js/**/*",
48 | "js/contentscript.js.map",
49 | "bower_components/**/*",
50 | "html/index.html"
51 | ],
52 | "content_security_policy": "script-src 'unsafe-eval' 'self' https://www.dwolla.com https://uat.dwolla.com; object-src 'self'; style-src 'unsafe-inline' 'self' https://uat.dwolla.com https://www.dwolla.com https://fonts.googleapis.com"
53 | }
54 |
--------------------------------------------------------------------------------
/chrome-extension/test/configure.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | process.exit();
3 |
4 | var chrome = require('selenium-webdriver/chrome');
5 | var chromeDriver = require('chromedriver');
6 |
7 | ExtensionDriver.prototype.navigate = function(url) {
8 | return this._driver.get('chrome-extension://' + this._id + '/' + url);
9 | };
10 |
11 | var id = 'bpngoepojmffegnjicpfjcakgajpmenk';
12 |
13 | before(function(done) {
14 | this.timeout(20000);
15 | this.environment = 'chrome';
16 |
17 | var test = this;
18 |
19 | var service = new chrome.ServiceBuilder(chromeDriver.path).build();
20 | chrome.setDefaultService(service);
21 |
22 | var options = new chrome.Options();
23 | options.addExtensions(path.resolve('chrome-extension/dist/tipsy.crx'), function() {
24 | test.driver = new chrome.Driver(options, service);
25 |
26 | //driver.manage().timeouts().implicitlyWait(1000);
27 | test.extensionDriver = new ExtensionDriver(test.driver, id);
28 | test.extensionDriver.navigate('html/index.html').then(done);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/docs/Modules.md:
--------------------------------------------------------------------------------
1 | ## Modules
2 |
3 | Tipsy is broken out into many isolated modules that should do one thing or many
4 | related things well. It is authored in the ES6 module specification flavor.
5 | A good reference and learning resource is: http://jsmodules.io/
6 |
7 | ### Creating a new module
8 |
9 | A simple module could be a file that looks like this:
10 |
11 | ``` javascript
12 | export var someVariable = 'someValue';
13 | ```
14 |
15 | A module can also import other files. Use relative paths to import:
16 |
17 | ``` javascript
18 | import someValue from './some-file';
19 | ```
20 |
21 | You may see the syntax:
22 |
23 | ``` javascript
24 | import { someValue } from './some-file';
25 | ```
26 |
27 | This allows you to pull a specific exported property from the module.
28 |
--------------------------------------------------------------------------------
/docs/NewComponent.md:
--------------------------------------------------------------------------------
1 | ### Adding a new Component
2 |
3 | Components inside Tipsy are defined in: shared/scripts/lib/components/. You
4 | will see existing components already in here that you can base new
5 | functionality off of.
6 |
7 | Creating a new component:
8 |
9 | - Add a folder to `shared/scripts/lib/components/` named after the component.
10 | You should only use lowercase and hyphen separated.
11 | Example: reminder-threshold
12 | - You could copy the reminders component wholesale and then make the necessary
13 | tweaks to get started.
14 | - Add HTML, JS, and Stylus files inside here named after the component.
15 | - `components/reminder-threshold/reminder-treshold.html`
16 | - `components/reminder-threshold/reminder-treshold.js`
17 | - `components/reminder-threshold/reminder-treshold.styl`
18 | - Once you've done this, you'll need to register it in the page (or globally
19 | with Component.register).
20 | - To register in a given page, look at components/settings/settings.js for
21 | inspiration. You will import calls like:
22 |
23 | ``` javascript
24 | import RemindersComponent from '../../components/reminders/reminders';
25 | ```
26 |
27 | Add one to match your component:
28 |
29 | ``` javascript
30 | import ReminderThresholdComponent from '../../components/reminder-threshold/reminder-threshold';
31 | ```
32 |
33 | - You'll notice that the Component is rendered immediately after the page
34 | has finished rendering inside `afterRender`.
35 |
36 | ``` javascript
37 | new RemindersComponent(select('set-reminders', this.el)).render();
38 | ```
39 |
40 | Your code would look something like:
41 |
42 | ``` javascript
43 | new ReminderThresholdComponent(select('reminder-threshold', this.el)).render();
44 | ```
45 |
46 | - Add the markup for the component inside the settings page markup
47 | (settings/settings.html):
48 |
49 | ``` html
50 |
51 |
52 |
53 | ```
54 |
55 | - (Optionally) Add the styles file to: `shared/styles/indx.styl`.
56 | - Under `// Components` around line 17 you'll see a convention for adding
57 | new component styles.
58 | - Adding reminder-threshold would look something like:
59 |
60 | ``` stylus
61 | reminder-threshold
62 | display block
63 | @import '../scripts/lib/components/reminder-threshold/reminder-threshold.styl'
64 | ```
65 |
66 | - (Optionally) Add content to the markup file:
67 | `components/reminder-threshold/reminder-threshold.html`. This is the
68 | template that will be rendered in place of the settings page markup.
69 |
70 | - Now you need to add the code for the Component:
71 |
72 | ``` javascript
73 | 'use strict';
74 |
75 | import Component from '../../component';
76 |
77 | function ReminderThresholdComponent() {
78 | Component.prototype.constructor.apply(this, arguments);
79 | }
80 |
81 | RemindersComponent.prototype = {
82 | template: 'components/reminder-threshold/reminder-threshold.html',
83 |
84 | events: {
85 | 'click': 'handleClick'
86 | },
87 |
88 | handleClick: function(ev) {
89 | console.log('Component was clicked', ev);
90 | },
91 |
92 | afterRender: function() {
93 | // Do something with this.$el (jQuery element) after the view is
94 | // rendered.
95 | }
96 | };
97 |
98 | ReminderThresholdComponent.prototype.__proto__ = Component.prototype;
99 |
100 | export default ReminderThresholdComponent;
101 | ```
102 |
103 | - Look to other components for inspiration.
104 |
--------------------------------------------------------------------------------
/docs/NewPage.md:
--------------------------------------------------------------------------------
1 | ### Creating a new Page
2 |
3 | Pages are simply Components inside the extension. They are registered once
4 | each, using the `Component.registerPage` method.
5 |
6 | ``` javascript
7 | Component.registerPage("#my-page", MyPageConstructor);
8 | ```
9 |
10 | You will need to import your constructor before using:
11 |
12 | ``` javascript
13 | import MyPageConstructor from './pages/my-page/my-page';
14 | ```
15 |
16 | There is, however, a better mechanism in place which is used for registering
17 | pages. Inside: `shared/scripts/lib/index.js` you will see a pages object
18 | near the top of the file. Add the id of the page and the component
19 | constructor; the code following will automatically register and render when
20 | applicable.
21 |
22 | Once you have added this code you will need to modify the
23 | `shared/html/index.html` file to add in your page placeholder. Open this file
24 | and you'll see `` tags that contain `id`'s that match the id's
25 | assigned above. Add your page placeholder here:
26 |
27 | ``` html
28 |
29 | ```
30 |
31 | You may also want to add your page to the navigation list in the ``
32 | element.
33 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | Tipsy is architected in a way that allows for a shared codebase between any
2 | browser extension platform. Currently it supports Chrome and Firefox.
3 |
4 | At a high level, the folder structure breaks down into:
5 |
6 | - `/build`
7 | - `/docs`
8 | - `/chrome-extension`
9 | - `/shared`
10 | - `/test`
11 |
12 | #### Build
13 |
14 | The build directory contains one committed folder and one ignored folder. The
15 | committed folder is where all Grunt task configuration lives. These tasks are
16 | authored in CoffeeScript. You can read more about this design decision here:
17 |
18 | http://tbranyen.com/post/coffeescript-has-the-ideal-syntax-for-configurations
19 |
20 | The second directory that is ignored, is `/build/tools`, which contains the
21 | tooling necessary to build the Firefox Addon. These tools are fetched during
22 | the `npm install` stage and therefore not committed into the repository.
23 |
24 | #### Documentation
25 |
26 | The `/docs` folder Contains the documentation you are currently reading.
27 |
28 | #### Chrome extension
29 |
30 | The chrome-extension directory contains the unique files and folders that
31 | cannot be shared. These items include the `_locales` folder and the key to
32 | sign the extension. This also contains the extension manifest file.
33 |
34 | The distribution directory, `dist`, contains the unpacked built extension and
35 | the signed `.crx` file.
36 |
37 | The `test` directory contains configuration for the Webdriver test runner which
38 | allows it to load and navigate to the extension.
39 |
40 | #### Shared assets
41 |
42 | This folder contains all the markup, images, source code, and styles for the
43 | extension. The styles and images are consistent between the extensions. The
44 | markup and source code need to resolve differences so there is branching logic
45 | involved.
46 |
47 | The markup is processed using the
48 | [grunt-targethtml](https://github.com/changer/grunt-targethtml) task. This
49 | task is able to remove the vendor script tags from the page within Firefox as
50 | they are dynamically injected. This is to overcome limitations with addons.
51 |
52 | The source is organized into three distinct pieces that can be often reused
53 | and shared. The associated graphs visualize the dependency structure.
54 |
55 | 
56 |
57 | ##### Background script
58 |
59 | This code runs "eternally" so long as the extension is installed. It starts up
60 | whenever the browser is open and is responsible for tracking tab activity and
61 | communicating with the content script and extension library.
62 |
63 | ##### Content script
64 |
65 | This script is injected into individual tabs to communicate with the the
66 | background script. It can track mouse movement and media activity. It also
67 | locates the ` ` tag which is used to identify if a page has opt'd into
68 | Tipsy.
69 |
70 | ##### Extension library
71 |
72 | Located within the `/shared/scripts/lib` folder, and starting with `index.js`,
73 | this library source is what powers the actual extension UI/UX.
74 |
75 | #### Tests
76 |
77 | The tests directory is broken down into unit and integration parts. The unit
78 | tests are written with Mocha and ES6 Module Loader and loaded via Grunt. The
79 | integration tests are necessary to test extension behavior that must happen
80 | within a real browser.
81 |
--------------------------------------------------------------------------------
/docs/WebdriverReference.md:
--------------------------------------------------------------------------------
1 | ### Webdriver Reference
2 |
3 | This is a reference of all available methods that are usable by the Webdriver
4 | client.
5 |
6 | More information can be found here:
7 | https://code.google.com/p/selenium/wiki/WebDriverJs
8 |
9 | Name | Type
10 | ------------------- | --------
11 | controlFlow | function
12 | schedule | function
13 | getSession | function
14 | getCapabilities | function
15 | quit | function
16 | actions | function
17 | executeScript | function
18 | executeAsyncScript | function
19 | call | function
20 | wait | function
21 | sleep | function
22 | getWindowHandle | function
23 | getAllWindowHandles | function
24 | getPageSource | function
25 | close | function
26 | get | function
27 | getCurrentUrl | function
28 | getTitle | function
29 | findElement | function
30 | findDomElement_ | function
31 | isElementPresent | function
32 | findElements | function
33 | takeScreenshot | function
34 | manage | function
35 | navigate | function
36 | switchTo | function
37 |
--------------------------------------------------------------------------------
/docs/_assets/activity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/docs/_assets/activity.png
--------------------------------------------------------------------------------
/docs/_assets/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/docs/_assets/background.png
--------------------------------------------------------------------------------
/docs/_assets/component.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/docs/_assets/component.png
--------------------------------------------------------------------------------
/docs/_assets/contentscript.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/docs/_assets/contentscript.png
--------------------------------------------------------------------------------
/docs/_assets/dependencies.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/docs/_assets/dependencies.json
--------------------------------------------------------------------------------
/docs/_assets/dom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/docs/_assets/dom.png
--------------------------------------------------------------------------------
/docs/_assets/environment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/docs/_assets/environment.png
--------------------------------------------------------------------------------
/docs/_assets/extension.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/docs/_assets/extension.png
--------------------------------------------------------------------------------
/docs/_assets/index.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/docs/_assets/index.png
--------------------------------------------------------------------------------
/docs/_assets/notifications.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/docs/_assets/notifications.png
--------------------------------------------------------------------------------
/docs/_assets/overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/docs/_assets/overview.png
--------------------------------------------------------------------------------
/docs/_assets/storage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/docs/_assets/storage.png
--------------------------------------------------------------------------------
/docs/_assets/tabs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/docs/_assets/tabs.png
--------------------------------------------------------------------------------
/docs/_assets/watcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/docs/_assets/watcher.png
--------------------------------------------------------------------------------
/docs/modules/background.md:
--------------------------------------------------------------------------------
1 | ### Background script
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This code runs "eternally" so long as the extension is installed. It starts up
8 | whenever the browser is open and is responsible for tracking tab activity and
9 | communicating with the content script and extension library.
10 |
11 | #### Purpose
12 |
13 | - Hooks up the extension icon and handles opening the extension in a new tab.
14 | - Initializes the activity logging.
15 | - Injects the content script.
16 | - Ensures the event watcher is loaded.
17 |
18 | #### Notes
19 |
20 | You should update this file whenever high level dependencies change. The
21 | scripts array contains all global dependencies from `node_modules` and the
22 | extension library script. This is required to inject into Firefox.
23 |
--------------------------------------------------------------------------------
/docs/modules/contentscript.md:
--------------------------------------------------------------------------------
1 | ### Content script
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This script is injected into individual tabs to communicate with the the
8 | background script. It can track mouse movement and media activity. It also
9 | locates the ` ` tag which is used to identify if a page has opt'd into
10 | Tipsy.
11 |
12 | #### Purpose
13 |
14 | - Locate ` ` tags and parse them for relevant Tipsy data.
15 | - Communicate with the background script when the tag is parsed and when
16 | events occur that affect the idle.
17 | - Parses out the domain from the page, which excludes the subdomain.
18 | - Hooks up browser based events that should trigger an idle.
19 |
20 | #### Notes
21 |
22 | The domain parsing is a bit of a hack that utilizes cookies to find the most
23 | generic allowed domain name, excluding subdomains.
24 |
--------------------------------------------------------------------------------
/docs/modules/lib/activity.md:
--------------------------------------------------------------------------------
1 | ### Activity
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This module is responsible for modifying the internal tab tracking cache and
8 | the log object inside the storage engine.
9 |
10 | This module does not do any monitoring or tracking itself. That happens within
11 | the watcher module.
12 |
13 | #### Purpose
14 |
15 | - To start and stop tab activity recording.
16 |
17 | #### Notes
18 |
19 | It may be required to convert the explicit storage dependency to be injected
20 | allowing for alternative implementations to be used. Although it's possible
21 | that this storage mechanism will be useful to other extensions as well.
22 |
23 | If querying is necessary, a new function `query` could be introduced. This is
24 | not necessary for this extension, but might be useful.
25 |
26 | #### Exported properties
27 |
28 | Name | Type | Description
29 | ---------- | -------- | -----------
30 | initialize | function | Ensures a log object in the storage engine is present.
31 | start | function | Activates a tab to be tracked internally.
32 | stop | function | Deactivates and saves the recorded tab information.
33 |
--------------------------------------------------------------------------------
/docs/modules/lib/component.md:
--------------------------------------------------------------------------------
1 | ### Component
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This module is an implementation of a component-like structure. Very similar
8 | to the Backbone.View class.
9 |
10 | #### Purpose
11 |
12 | - To provide base structure for creating components and pages.
13 |
14 | #### Notes
15 |
16 |
17 | #### Exported constructor
18 |
19 | `Component`
20 |
21 |
22 |
23 | #### Prototype properties
24 |
25 | Name | Type | Description
26 | ---------- | -------- | -----------
27 | constructor | function | Ensures a log object in the storage engine is present.
28 | bindEvents | function | Activates a tab to be tracked internally.
29 | fetch | function | Deactivates and saves the recorded tab information.
30 | render | function | Deactivates and saves the recorded tab information.
31 | $ | function | Deactivates and saves the recorded tab information.
32 | register | function | Deactivates and saves the recorded tab information.
33 | registerPage | function | Deactivates and saves the recorded tab information.
34 |
35 |
--------------------------------------------------------------------------------
/docs/modules/lib/components/donation-goal.md:
--------------------------------------------------------------------------------
1 | ### Extension
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This module exports a single variable that indicates the current environment.
8 |
9 | #### Purpose
10 |
11 | - To let other modules know what environment the extension is in.
12 |
13 | #### Notes
14 |
15 | It's a very cheap lookup that simply checks if `chrome` is a global variable.
16 | If it is not, it is assumed the environment is Firefox.
17 |
18 | #### Exported properties
19 |
20 | Name | Type | Description
21 | ----------- | ------ | -----------
22 | environment | string | Either 'chrome' or 'firefox' depending on environment.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/modules/lib/components/log-table.md:
--------------------------------------------------------------------------------
1 | ### Extension
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This module exports a single variable that indicates the current environment.
8 |
9 | #### Purpose
10 |
11 | - To let other modules know what environment the extension is in.
12 |
13 | #### Notes
14 |
15 | It's a very cheap lookup that simply checks if `chrome` is a global variable.
16 | If it is not, it is assumed the environment is Firefox.
17 |
18 | #### Exported properties
19 |
20 | Name | Type | Description
21 | ----------- | ------ | -----------
22 | environment | string | Either 'chrome' or 'firefox' depending on environment.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/modules/lib/components/reminders.md:
--------------------------------------------------------------------------------
1 | ### Extension
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This module exports a single variable that indicates the current environment.
8 |
9 | #### Purpose
10 |
11 | - To let other modules know what environment the extension is in.
12 |
13 | #### Notes
14 |
15 | It's a very cheap lookup that simply checks if `chrome` is a global variable.
16 | If it is not, it is assumed the environment is Firefox.
17 |
18 | #### Exported properties
19 |
20 | Name | Type | Description
21 | ----------- | ------ | -----------
22 | environment | string | Either 'chrome' or 'firefox' depending on environment.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/modules/lib/dom.md:
--------------------------------------------------------------------------------
1 | ### DOM
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | A lightweight abstraction that provides Document querying with selectors.
8 |
9 | #### Purpose
10 |
11 | - The content script should do its best to not affect other scripts on a given
12 | page. This script provides the necessary lookups to avoid a library like
13 | jQuery.
14 |
15 | #### Notes
16 |
17 | This module is completely standalone and should realistically only be depended
18 | on by the content script. Early on in development, it was attempted to avoid
19 | jQuery (which turned out to be a bad idea, backfilling jQuery is a pain), so
20 | there may be other modules that are still using some functions.
21 |
22 | In the case of `selectAll` the return value will always be an Array, instead of
23 | a NodeList, which is beneficial for Array operations.
24 |
25 | Both methods allow the passing of a context object, which is a DOM Element that
26 | will scope the lookups.
27 |
28 | #### Exported properties
29 |
30 | Name | Type | Description
31 | ---------- | -------- | -----------
32 | select | function | Selects a single element (first) found for a selector.
33 | selectAll | function | Selects all elements found for a selector.
34 |
--------------------------------------------------------------------------------
/docs/modules/lib/environment.md:
--------------------------------------------------------------------------------
1 | ### Environment
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This module exports a single variable that indicates the current environment.
8 |
9 | #### Purpose
10 |
11 | - To let other modules know what environment the extension is in.
12 |
13 | #### Notes
14 |
15 | It's a very cheap lookup that simply checks if `chrome` is a global variable.
16 | If it is not, it is assumed the environment is Firefox.
17 |
18 | #### Exported properties
19 |
20 | Name | Type | Description
21 | ----------- | ------ | -----------
22 | environment | string | Either 'chrome' or 'firefox' depending on environment.
23 |
--------------------------------------------------------------------------------
/docs/modules/lib/extension.md:
--------------------------------------------------------------------------------
1 | ### Extension
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This module exports a single variable that indicates the current environment.
8 |
9 | #### Purpose
10 |
11 | - To let other modules know what environment the extension is in.
12 |
13 | #### Notes
14 |
15 | It's a very cheap lookup that simply checks if `chrome` is a global variable.
16 | If it is not, it is assumed the environment is Firefox.
17 |
18 | #### Exported properties
19 |
20 | Name | Type | Description
21 | ----------- | ------ | -----------
22 | environment | string | Either 'chrome' or 'firefox' depending on environment.
23 |
24 |
--------------------------------------------------------------------------------
/docs/modules/lib/index.md:
--------------------------------------------------------------------------------
1 | ### Extension
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This module exports a single variable that indicates the current environment.
8 |
9 | #### Purpose
10 |
11 | - To let other modules know what environment the extension is in.
12 |
13 | #### Notes
14 |
15 | It's a very cheap lookup that simply checks if `chrome` is a global variable.
16 | If it is not, it is assumed the environment is Firefox.
17 |
18 | #### Exported properties
19 |
20 | Name | Type | Description
21 | ----------- | ------ | -----------
22 | environment | string | Either 'chrome' or 'firefox' depending on environment.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/modules/lib/notifications.md:
--------------------------------------------------------------------------------
1 | ### Extension
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This module exports a single variable that indicates the current environment.
8 |
9 | #### Purpose
10 |
11 | - To let other modules know what environment the extension is in.
12 |
13 | #### Notes
14 |
15 | It's a very cheap lookup that simply checks if `chrome` is a global variable.
16 | If it is not, it is assumed the environment is Firefox.
17 |
18 | #### Exported properties
19 |
20 | Name | Type | Description
21 | ----------- | ------ | -----------
22 | environment | string | Either 'chrome' or 'firefox' depending on environment.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/modules/lib/pages/donations.md:
--------------------------------------------------------------------------------
1 | ### Extension
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This module exports a single variable that indicates the current environment.
8 |
9 | #### Purpose
10 |
11 | - To let other modules know what environment the extension is in.
12 |
13 | #### Notes
14 |
15 | It's a very cheap lookup that simply checks if `chrome` is a global variable.
16 | If it is not, it is assumed the environment is Firefox.
17 |
18 | #### Exported properties
19 |
20 | Name | Type | Description
21 | ----------- | ------ | -----------
22 | environment | string | Either 'chrome' or 'firefox' depending on environment.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/modules/lib/pages/getting-started.md:
--------------------------------------------------------------------------------
1 | ### Extension
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This module exports a single variable that indicates the current environment.
8 |
9 | #### Purpose
10 |
11 | - To let other modules know what environment the extension is in.
12 |
13 | #### Notes
14 |
15 | It's a very cheap lookup that simply checks if `chrome` is a global variable.
16 | If it is not, it is assumed the environment is Firefox.
17 |
18 | #### Exported properties
19 |
20 | Name | Type | Description
21 | ----------- | ------ | -----------
22 | environment | string | Either 'chrome' or 'firefox' depending on environment.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/modules/lib/pages/log.md:
--------------------------------------------------------------------------------
1 | ### Extension
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This module exports a single variable that indicates the current environment.
8 |
9 | #### Purpose
10 |
11 | - To let other modules know what environment the extension is in.
12 |
13 | #### Notes
14 |
15 | It's a very cheap lookup that simply checks if `chrome` is a global variable.
16 | If it is not, it is assumed the environment is Firefox.
17 |
18 | #### Exported properties
19 |
20 | Name | Type | Description
21 | ----------- | ------ | -----------
22 | environment | string | Either 'chrome' or 'firefox' depending on environment.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/modules/lib/pages/settings.md:
--------------------------------------------------------------------------------
1 | ### Extension
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This module exports a single variable that indicates the current environment.
8 |
9 | #### Purpose
10 |
11 | - To let other modules know what environment the extension is in.
12 |
13 | #### Notes
14 |
15 | It's a very cheap lookup that simply checks if `chrome` is a global variable.
16 | If it is not, it is assumed the environment is Firefox.
17 |
18 | #### Exported properties
19 |
20 | Name | Type | Description
21 | ----------- | ------ | -----------
22 | environment | string | Either 'chrome' or 'firefox' depending on environment.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/modules/lib/storage.md:
--------------------------------------------------------------------------------
1 | ### Extension
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This module exports a single variable that indicates the current environment.
8 |
9 | #### Purpose
10 |
11 | - To let other modules know what environment the extension is in.
12 |
13 | #### Notes
14 |
15 | It's a very cheap lookup that simply checks if `chrome` is a global variable.
16 | If it is not, it is assumed the environment is Firefox.
17 |
18 | #### Exported properties
19 |
20 | Name | Type | Description
21 | ----------- | ------ | -----------
22 | environment | string | Either 'chrome' or 'firefox' depending on environment.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/modules/lib/tabs.md:
--------------------------------------------------------------------------------
1 | ### Extension
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This module exports a single variable that indicates the current environment.
8 |
9 | #### Purpose
10 |
11 | - To let other modules know what environment the extension is in.
12 |
13 | #### Notes
14 |
15 | It's a very cheap lookup that simply checks if `chrome` is a global variable.
16 | If it is not, it is assumed the environment is Firefox.
17 |
18 | #### Exported properties
19 |
20 | Name | Type | Description
21 | ----------- | ------ | -----------
22 | environment | string | Either 'chrome' or 'firefox' depending on environment.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/modules/lib/watcher.md:
--------------------------------------------------------------------------------
1 | ### Extension
2 |
3 | 
4 |
5 | #### Overview
6 |
7 | This module exports a single variable that indicates the current environment.
8 |
9 | #### Purpose
10 |
11 | - To let other modules know what environment the extension is in.
12 |
13 | #### Notes
14 |
15 | It's a very cheap lookup that simply checks if `chrome` is a global variable.
16 | If it is not, it is assumed the environment is Firefox.
17 |
18 | #### Exported properties
19 |
20 | Name | Type | Description
21 | ----------- | ------ | -----------
22 | environment | string | Either 'chrome' or 'firefox' depending on environment.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tipsy",
3 | "private": true,
4 | "scripts": {
5 | "grunt": "grunt",
6 | "watch": "grunt default watch",
7 | "watch-chrome": "grunt chrome-extension watch:chrome-extension",
8 | "build": "grunt",
9 | "build-chrome": "grunt chrome-extension",
10 | "test": "grunt && grunt coverage",
11 | "test-chrome": "grunt chrome-extension mochaTest:chrome-extension",
12 | "postinstall": "npm dedupe && npm run python2",
13 | "python2": "grunt shell:python2",
14 | "cjs": "compile-modules convert -f commonjs shared/scripts/**/*.js -o cjs/"
15 | },
16 | "devDependencies": {
17 | "chromedriver": "^2.26.1",
18 | "es6-module-loader": "^0.15.0",
19 | "es6-module-transpiler": "^0.10.0",
20 | "grunt": "^0.4.5",
21 | "grunt-cli": "^0.1.13",
22 | "grunt-contrib-clean": "^0.6.0",
23 | "grunt-contrib-compress": "^0.13.0",
24 | "grunt-contrib-copy": "^0.8.0",
25 | "grunt-contrib-jshint": "^0.11.0",
26 | "grunt-contrib-stylus": "^0.20.0",
27 | "grunt-contrib-watch": "^0.6.1",
28 | "grunt-env": "^0.4.4",
29 | "grunt-istanbul": "^0.4.0",
30 | "grunt-mocha-test": "^0.12.7",
31 | "grunt-shell": "^1.1.2",
32 | "istanbul": "^0.3.7",
33 | "istanbul-traceur": "^1.0.7",
34 | "madge": "^0.4.1",
35 | "mocha": "^2.2.0",
36 | "promise": "^7.1.1",
37 | "selenium-standalone": "^5.9.0",
38 | "selenium-webdriver": "^2.51.0"
39 | },
40 | "dependencies": {
41 | "combyne": "^2.0.0",
42 | "jquery": "^2.1.3",
43 | "moment": "^2.9.0",
44 | "purecss": "^0.6.0",
45 | "tablesort": "^3.0.2"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/shared/fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/shared/fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/shared/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/shared/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/shared/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/shared/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/shared/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/shared/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/shared/html/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Tipsy
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | tipsy beta
17 | Visit Log
18 | Settings
19 | Contributions
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/shared/img/bitcoin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/shared/img/bitcoin.png
--------------------------------------------------------------------------------
/shared/img/bitcoin_pay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/shared/img/bitcoin_pay.png
--------------------------------------------------------------------------------
/shared/img/dwolla.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/shared/img/dwolla.png
--------------------------------------------------------------------------------
/shared/img/dwolla_pay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/shared/img/dwolla_pay.png
--------------------------------------------------------------------------------
/shared/img/logo19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/shared/img/logo19.png
--------------------------------------------------------------------------------
/shared/img/logo48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/shared/img/logo48.png
--------------------------------------------------------------------------------
/shared/img/logo64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/shared/img/logo64.png
--------------------------------------------------------------------------------
/shared/img/paypal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/shared/img/paypal.png
--------------------------------------------------------------------------------
/shared/img/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/shared/img/settings.png
--------------------------------------------------------------------------------
/shared/img/stripe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/shared/img/stripe.png
--------------------------------------------------------------------------------
/shared/scripts/background.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { createExtension } from './lib/extension';
4 | import { initialize } from './lib/activity';
5 | import './lib/watcher';
6 |
7 | // This call hooks up the click functionality in Chrome.
8 | createExtension({
9 | id: 'tipsy-icon',
10 | label: 'Launch Tipsy',
11 | indexUrl: 'html/index.html',
12 |
13 | icon: {
14 | '19': './img/logo19.png',
15 | '48': './img/logo48.png',
16 | '64': './img/logo64.png'
17 | }
18 | });
19 |
20 | // Ensure that the activity logger has been initialized before trying to load
21 | // the content script.
22 | initialize();
23 |
--------------------------------------------------------------------------------
/shared/scripts/contentscript.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { environment } from './lib/environment';
4 | import { selectAll } from './lib/dom';
5 | import { domains } from './lib/hardcoded-doms';
6 | import { parseTxt } from './lib/utils/tipsy-txt-parser';
7 |
8 | /**
9 | * Allows communication between content script and background script.
10 | *
11 | * @param {Object} body - the message payload.
12 | */
13 | function postMessage(body) {
14 | body = JSON.stringify(body);
15 | chrome.runtime.sendMessage(body);
16 | }
17 |
18 | // Place little dom node to let page know tipsy is installed. This is
19 | // how it should be done according to google:
20 | // https://developer.chrome.com/webstore/inline_installation
21 | var isInstalledNode = document.createElement('div');
22 | isInstalledNode.id = 'tipsy-is-installed';
23 | document.body.appendChild(isInstalledNode);
24 | /**
25 | * Debounce a function to not thrash the message passing.
26 | *
27 | * @param {Function} fn - A function to debounce.
28 | * @param {number} delay - How long to wait.
29 | * @return {Function} A new function that will be used in place and debounced.
30 | */
31 | function debounce(fn, delay) {
32 | // Close around this variable so that subsequent calls will always reference
33 | // the same timer.
34 | var timeout;
35 |
36 | return function() {
37 | var args = arguments;
38 |
39 | // Always clear the timeout.
40 | clearTimeout(timeout);
41 |
42 | // Set a new timeout.
43 | timeout = setTimeout(function() {
44 | fn.apply(this, args);
45 | }.bind(this), delay);
46 | };
47 | }
48 |
49 | /**
50 | * Find the current domain name.
51 | *
52 | * Technique from:
53 | * http://rossscrivener.co.uk/blog/javascript-get-domain-exclude-subdomain
54 | *
55 | * @return {string} the domain name
56 | */
57 | function findDomain() {
58 | var i = 0;
59 | var domain = document.domain;
60 | var p = domain.split('.');
61 | var s = '_gd' + (new Date()).getTime();
62 |
63 | // Detect if the cookie has already been set, otherwise attempt setting.
64 | while (i < (p.length - 1) && document.cookie.indexOf(s + '=' + s) == -1) {
65 | domain = p.slice(-1 - (++i)).join('.');
66 |
67 | // Test setting a cookie.
68 | document.cookie = s + "=" + s + ";domain=" + domain + ";";
69 | }
70 |
71 | // Remove the cookie.
72 | document.cookie = s + "=;expires=Thu, 01 Jan 1970 00:00:01 GMT;domain=" +
73 | domain + ";";
74 |
75 | // Modifying here to remove the leading `www.`.
76 | if (domain.indexOf('www.') === 0) {
77 | domain = domain.slice(4);
78 | }
79 |
80 | return domain;
81 | }
82 |
83 | // Find all links on the page.
84 | var links = selectAll('link');
85 |
86 | // Build up an object with page and author details for the extension.
87 | var messageBody = {
88 | hostname: findDomain(),
89 | list: []
90 | };
91 |
92 | // Iterate over all links and filter down to the last link that contains the
93 | // correct metadata.
94 |
95 | if (!domains[messageBody.hostname]) {
96 |
97 | messageBody.list = links.filter(function(link) {
98 | return link.rel === 'author';
99 | }).map(function(link) {
100 | var author = {};
101 |
102 | // Personal identification.
103 | author.name = link.getAttribute('name');
104 | author.href = link.getAttribute('href');
105 | author.gravatar = link.getAttribute('gravatar');
106 |
107 | // Payment information.
108 | author.dwolla = link.getAttribute('dwolla') || link.getAttribute('data-dwolla');
109 | author.bitcoin = link.getAttribute('bitcoin') || link.getAttribute('data-bitcoin');
110 | author.paypal = link.getAttribute('paypal') || link.getAttribute('data-paypal');
111 | author.stripe = link.getAttribute('stripe') || link.getAttribute('data-stripe');
112 | return author;
113 | });
114 |
115 | } else {
116 | var newArray = [];
117 | newArray[0] = domains[messageBody.hostname];
118 | var author = newArray;
119 | messageBody.list = author;
120 | }
121 |
122 | // if nothing, check for tipsy.txt
123 | if (messageBody.list.length === 0) {
124 |
125 |
126 |
127 | messageBody.list = parseTxt();
128 | }
129 |
130 |
131 | //console.log(messageBody)
132 |
133 |
134 | // Send this message body back to the extension.
135 | postMessage({
136 | name: 'author',
137 | data: messageBody
138 | });
139 |
140 | // Makes it easier to denote what the current page idle is. Binds multiple
141 | // events to an element and then passes along what the state should be.
142 | var addEvent = function(element, events, state) {
143 | // Allow multiple events to be bound.
144 | events.split(' ').forEach(function(event) {
145 | element.addEventListener(event, debounce(function() {
146 | postMessage({
147 | name: 'isIdle',
148 | data: state
149 | });
150 | }, 500), true);
151 | });
152 | };
153 |
154 | // Whenever the mouse moves or the page scrolls, set the state to `false`.
155 | addEvent(document.body, 'scroll mousemove', false);
156 |
157 | // Loop through all media types and bind to their respective state events to
158 | // update the idle state.
159 | selectAll('audio, video').forEach(function(media) {
160 | addEvent(media, 'abort pause', true);
161 | addEvent(media, 'playing', false);
162 | });
163 |
--------------------------------------------------------------------------------
/shared/scripts/lib/activity.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { environment } from './environment';
4 | import storage from './storage';
5 | import { tabs } from './tabs';
6 | import { updateDBTimeSpentAuthored } from './server-requests';
7 |
8 | /**
9 | * This ensures that log is an object and not undefined, as the `storage#get`
10 | * call defaults to an empty object if it was not previously set.
11 | *
12 | * @return {Promise} that resolves once the log has been updated.
13 | */
14 | export function initialize() {
15 | return storage.get('log').then(function(log) {
16 | return storage.set('log', log);
17 | });
18 | }
19 |
20 | /**
21 | * Sets up the tab in the cache with the current access time and the tab id.
22 | * In Firefox this will also attempt to grab the favicon url.
23 | *
24 | * @param {Object} tab - to monitor.
25 | */
26 | export function start(tab) {
27 | if (tab.tab) {
28 | tab = tab.tab;
29 | }
30 |
31 | // Ensure we're working with a tab object, which may or may not already
32 | // exist.
33 | var currentTab = tabs[tab.id] || {};
34 |
35 | // Add the tab object and current access time.
36 | currentTab.accessTime = currentTab.accessTime || Date.now();
37 | currentTab.tab = currentTab.tab || tab;
38 |
39 | console.info('Started tracking: %s', tab.url);
40 |
41 | tabs[tab.id] = currentTab;
42 | }
43 |
44 | /**
45 | * Once the activity module determines the tab is inactive or closed, this
46 | * function is called to end the tab tracking.
47 | *
48 | * @param {Object} tab - to monitor.
49 | * @return {Promise} that resolves when log has been written to storage engine.
50 | */
51 | export function stop(tab) {
52 | // Open access to the current log so that we can append the latest tab entry
53 | // into it.
54 |
55 | return storage.get('log').then(function(log) {
56 | // If we stop on a non-tab or do not have a notion of this tab, simply
57 | // return, it's not being tracked.
58 | if (!tab || !tabs[tab.id]) {
59 | return;
60 | }
61 |
62 | // Make sure we've started this tab, otherwise this is an invalid state.
63 | // Only work with HTTP links for now, omits weird `chrome://` urls.
64 | if (!tabs[tab.id].author || tab.url.indexOf('http') !== 0) {
65 | console.info('Stopped tracking: %s', tab.url);
66 |
67 | delete tabs[tab.id].accessTime;
68 | delete tabs[tab.id].tab;
69 | return;
70 | }
71 |
72 | var host = tabs[tab.id].author.hostname;
73 |
74 | // Ensure that the log for this url is an array of entries.
75 | log[host] = Array.isArray(log[host]) ? log[host] : [];
76 |
77 | // Never push an entry in that does not contain the host.
78 | if (tab.url.indexOf(host) > -1) {
79 | // Add the information necessary to render the log and payments correctly
80 | var timeSpent = Date.now() - tabs[tab.id].accessTime;
81 |
82 | //FIXME: make sure we only add authored that have payment option
83 | var list = tabs[tab.id].author.list;
84 | if (list.length >= 1 && (list[0].bitcoin || list[0].dwolla ||
85 | list[0].paypal || list[0].stripe)) {
86 | storage.get('settings').then(function(settings) {
87 | var oldTime = settings.timeSpentAuthored || 0;
88 | settings.timeSpentAuthored = oldTime + timeSpent;
89 | updateDBTimeSpentAuthored(settings);
90 | // Makes sure that first time extension notices a site that
91 | // has author we know when that was for rate calculation and prediction.
92 | if (typeof settings.timeStarted === 'undefined') {
93 | settings.timeStarted = Date.now();
94 | }
95 |
96 | if (typeof settings.totalPaid === 'undefined') {
97 | settings.totalPaid = 0;
98 | }
99 |
100 | return storage.set('settings', settings);
101 | }).catch(function(ex) {
102 | console.log(ex);
103 | console.log(ex.stack);
104 | });
105 | }
106 |
107 | // make sure that the tab was finished loading before it stopped
108 | if (typeof tabs[tab.id].accessTime != 'undefined') {
109 |
110 | // update the daysVisited if necessary
111 | if (log[host][0] && log[host][0].daysVisited) {
112 | var lastTimeVisited = new Date(log[host][log[host].length-1].accessTime);
113 | var now = new Date();
114 | var isSameDay = (lastTimeVisited.getDate() == now.getDate() &&
115 | lastTimeVisited.getMonth() == now.getMonth() &&
116 | lastTimeVisited.getFullYear() == now.getFullYear());
117 | if (!isSameDay) {
118 | log[host][0].daysVisited = log[host][0].daysVisited + 1;
119 | }
120 | } else {
121 | log[host].unshift({'daysVisited':1});
122 | }
123 |
124 | // add to log
125 | log[host].push({
126 | author: tabs[tab.id].author,
127 | tab: tab,
128 | accessTime: tabs[tab.id].accessTime,
129 | timeSpent: timeSpent,
130 | paid: false
131 | });
132 | }
133 | }
134 |
135 | // Write log updates back to the storage engine.
136 | return storage.set('log', log).then(function() {
137 | // Remove the tab from the list to monitor.
138 | console.info('Stopped tracking: %s', tab.url);
139 |
140 | delete tabs[tab.id].accessTime;
141 | delete tabs[tab.id].tab;
142 | });
143 | }).catch(function(ex) {
144 | console.log(ex);
145 | console.log(ex.stack);
146 | });
147 | }
148 |
--------------------------------------------------------------------------------
/shared/scripts/lib/component.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { select, selectAll } from './dom';
4 |
5 | /**
6 | * Represents a Component
7 | */
8 | function Component() {
9 | this.constructor.apply(this, arguments);
10 | }
11 |
12 | /**
13 | * Sets up the component with an element and template. This will also fetch
14 | * the associated template and compile it to a function.
15 | *
16 | * @param {Object} element - to associate with the component.
17 | * @param {string} template - path to fetch.
18 | */
19 | Component.prototype.constructor = function(element, template) {
20 | this.el = element;
21 | this.template = template || this.template;
22 |
23 | var component = this;
24 |
25 | this.compiled = this.fetch(this.template).then(function(contents) {
26 | var template = combyne(contents);
27 |
28 | // Register all filters to the template.
29 | [].concat(component.filters).forEach(function(filter) {
30 | template.registerFilter(filter, component[filter]);
31 | });
32 |
33 | return template;
34 | });
35 |
36 | this.bindEvents();
37 | };
38 |
39 | /**
40 | * Extracts declarative events and binds to the internal element, very similar
41 | * to `Backbone.View#events`.
42 | */
43 | Component.prototype.bindEvents = function() {
44 | var events = this.events || {};
45 |
46 | Object.keys(events).forEach(function(eventAndSelector) {
47 | var fn = events[eventAndSelector];
48 | var parts = eventAndSelector.split(' ');
49 | var event = parts[0];
50 | var selector = parts.slice(1).join(' ');
51 | var component = this;
52 | var el = component.el;
53 |
54 | // Bind the event and adding in the necessary code for event delegation.
55 | $(el).on(event, selector, function(ev) {
56 | ev.stopImmediatePropagation();
57 | return component[fn].call(component, ev);
58 | });
59 | }, this);
60 | };
61 |
62 | /**
63 | * Makes an asynchronous request to the extension filesystem to retrieve a
64 | * template.
65 | *
66 | * @param {string} template - to fetch.
67 | * @return {Promise} that resolves once the template is fetched.
68 | */
69 | Component.prototype.fetch = function(template) {
70 | return new Promise(function(resolve, reject) {
71 | var xhr = new window.XMLHttpRequest();
72 |
73 | xhr.addEventListener('load', function() {
74 | resolve(this.responseText);
75 | }, true);
76 |
77 | xhr.addEventListener('error', reject, true);
78 |
79 | xhr.open('GET', '../scripts/lib/' + template, true);
80 | xhr.send();
81 | });
82 | };
83 |
84 | /**
85 | * Extracts the component's data (context) from the arguments or serialize
86 | * function. This data is then passed into the previously fetched and compiled
87 | * template. From there the result is injected into the component's internal
88 | * element. Finally the `afterRender` is triggered if it exists.
89 | *
90 | * @param {Object} context - data to render in the template.
91 | * @return {Promise} that resolves when the rendering completes.
92 | */
93 | Component.prototype.render = function(context) {
94 | var component = this;
95 | var element = this.el;
96 |
97 | context = context || (component.serialize ? component.serialize() : {});
98 |
99 | return this.compiled.then(function(template) {
100 | return template.render(context);
101 | }).then(function(contents) {
102 | element.innerHTML = contents;
103 | return contents;
104 | }, function(ex) {
105 | // If there was an error provide some useful reporting.
106 | console.log(ex.stack);
107 | }).then(function() {
108 | if (component.afterRender) {
109 | component.afterRender();
110 | }
111 | }).catch(function(ex) {
112 | console.log(component, context);
113 | console.log(ex.stack);
114 | });
115 | };
116 |
117 | /**
118 | * A scoped (to the component's internal element) jQuery lookup.
119 | *
120 | * @param {string} selector - to search for.
121 | * @return {Object} jQuery wrapped object containing the matched elements.
122 | */
123 | Component.prototype.$ = function(selector) {
124 | return $(selector, this.el);
125 | };
126 |
127 | /**
128 | * Loops through matching elements and initializes a component instance into
129 | * each one.
130 | *
131 | * @param {string} selector - to search for matching elements.
132 | * @param {Function} Component - constructor to initialize.
133 | * @param {Object} context - element to scope selector matching to.
134 | */
135 | Component.register = function(selector, Component, context) {
136 | $(selector, context).each(function() {
137 | new Component(this);
138 | });
139 | };
140 |
141 | /**
142 | * Register a page and return it. This differs from `register`.
143 | *
144 | * @param {string} selector
145 | * @param {Function} Component
146 | * @param {Object} context
147 | * @return {Object}
148 | */
149 | Component.registerPage = function(selector, Component, context) {
150 | var element = select(selector, context);
151 | return new Component(element);
152 | };
153 |
154 | export default Component;
155 |
--------------------------------------------------------------------------------
/shared/scripts/lib/components/donation-goal/donation-goal.html:
--------------------------------------------------------------------------------
1 | Set a contribution goal
2 |
3 |
4 |
5 | Choose a rate that you want Tipsy to use when calculating your contribution.
6 |
7 |
8 |
9 |
10 |
11 | per
12 |
13 | Minute
14 | Hour
15 | Day
16 |
17 | spent on a webpage.
18 |
19 |
20 |
21 |
22 |
23 |
24 | per calendar
25 |
26 | Hour
27 | Day
28 | Week
29 | Month
30 |
31 |
32 |
33 |
34 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/shared/scripts/lib/components/donation-goal/donation-goal.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Component from '../../component';
4 | import storage from '../../storage';
5 | import { intervals, defaults } from '../../defaults';
6 |
7 | function DonationGoalComponent() {
8 | Component.prototype.constructor.apply(this, arguments);
9 | var component = this;
10 |
11 | storage.get('settings').then(function(settings) {
12 | component.rateType = settings.rateType || 'browsingRate';
13 | component.render();
14 | });
15 |
16 | /*
17 | storage.get('settings').then(function(settings) {
18 | $.post('http://tipsy.csail.mit.edu/test/test4.php',{userId:settings.userId, time:settings.timeSpentAuthored.toString()});
19 | });
20 | */
21 |
22 | storage.onChange(function() {
23 | component.updateEstimate(null, component);
24 | });
25 | }
26 |
27 | DonationGoalComponent.prototype = {
28 | template: 'components/donation-goal/donation-goal.html',
29 |
30 | events: {
31 | 'keyup input[type=text]': 'filterInput',
32 | 'blur input[type=text]': 'formatAndSave',
33 | 'change input[type=text]': 'formatAndSave',
34 | 'change input[type=radio]': 'rateSelected',
35 | 'change select': 'updateInterval'
36 | },
37 |
38 | rateSelected: function(ev) {
39 |
40 | this.updateRateDescription(ev.target.id);
41 | },
42 |
43 | updateInterval: function(ev) {
44 | var id = ev.target.id;
45 |
46 | var minutes = Number(ev.target.value);
47 | var component = this;
48 |
49 | return storage.get('settings').then(function(settings) {
50 | if (id == 'calendarRateInterval') {
51 | settings.donationIntervalCalendarRate = minutes;
52 | } else if (id == 'browsingRateInterval') {
53 | settings.donationIntervalBrowsingRate = minutes;
54 | }
55 |
56 |
57 | //component.updateOwe(settings);
58 |
59 | return storage.set('settings', settings);
60 | });
61 | },
62 |
63 | filterInput: function(ev) {
64 | var val = ev.target.value.replace(/[^0-9.]/g, '');
65 | //this.$('input[type=text]').val('$' + val);
66 | if (ev.target.id == 'calendarRateGoal') {
67 | this.$('#calendarRateGoal').val('$' + val);
68 | }
69 | else if (ev.target.id == 'browsingRateGoal') {
70 | this.$('#browsingRateGoal').val('$' + val);
71 | }
72 | },
73 |
74 | formatAndSave: function(ev) {
75 | var component = this;
76 | var val = ev.target.value.replace(/[^0-9.]/g, '');
77 | var currency = '$' + parseFloat(val).toFixed(2);
78 |
79 | if (!(component.$('#calendarRateGoal').prop('disabled'))) {
80 | component.$('#calendarRateGoal').val(currency);
81 | }
82 | if (!(component.$('#browsingRateGoal').prop('disabled'))) {
83 | component.$('#browsingRateGoal').val(currency);
84 | }
85 |
86 | storage.get('settings').then(function(settings) {
87 |
88 | if (!(component.$('#calendarRateGoal').prop('disabled'))) {
89 | settings.donationGoalCalendarRate = currency;
90 | }
91 | if (!(component.$('#browsingRateGoal').prop('disabled'))) {
92 | settings.donationGoalBrowsingRate = currency;
93 | }
94 | //component.updateOwe(settings);
95 |
96 | return storage.set('settings', settings);
97 | }).catch(function(ex) {
98 | console.log(ex);
99 | console.log(ex.stack);
100 | });
101 | },
102 |
103 | updateEstimate: function(rateType, component) {
104 | storage.get('settings').then(function(settings) {
105 | if (!rateType) {
106 | rateType = settings.rateType;
107 | }
108 |
109 | if (rateType == 'browsingRate') {
110 | if (settings.timeStarted && settings.donationGoalBrowsingRate && settings.donationIntervalBrowsingRate) {
111 | var timeSpan = Date.now() - settings.timeStarted;
112 | var timeSpent = settings.timeSpentAuthored;
113 | var fracSpent = timeSpent / timeSpan;
114 | var estimatePerMin = fracSpent * parseFloat(settings.donationGoalBrowsingRate.slice(1)) / settings.donationIntervalBrowsingRate; // per minute
115 |
116 | var estimate = defaults.estimate.minutes * estimatePerMin;
117 | component.$('.avgTime').html("Based on your browsing activity since " + moment(settings.timeStarted).fromNow() + ", after "+ defaults.estimate.amount + " " + defaults.estimate.type + " you are estimated to pay a total of $" + estimate.toFixed(2).toString() +" .");
118 | } else {
119 | component.$('.avgTime').html("Not enough data yet to give you a meaningful estimate.");
120 | }
121 | } else if (rateType == 'calendarRate') {
122 | component.$('.avgTime').text("");
123 | } else {
124 | console.error("No or wrong rateType selected");
125 | }
126 | }).catch(function(ex) {
127 | console.log(ex);
128 | console.log(ex.stack);
129 | });
130 | },
131 |
132 | disableOtherRate: function(rateType, component) {
133 | var other;
134 | if (rateType == "browsingRate") {
135 | other = "calendarRate";
136 | } else if (rateType == "calendarRate") {
137 | other = "browsingRate";
138 | }
139 |
140 | component.$('#' + other + 'Goal').prop('disabled', true);
141 | component.$('#' + other + 'Interval').prop('disabled', true);
142 |
143 | component.$('#' + rateType + 'Goal').prop('disabled', false);
144 | component.$('#' + rateType + 'Interval').prop('disabled', false);
145 |
146 | component.$('.' + other + 'Text').removeClass('active');
147 | component.$('.' + rateType + 'Text').addClass('active');
148 | },
149 |
150 | updateRateDescription: function(id) {
151 | var rateType;
152 | var component = this;
153 | if (id == 'browsingRateRadio') {
154 | rateType = 'browsingRate';
155 | }
156 | else if (id == "calendarRateRadio") {
157 | rateType = 'calendarRate';
158 | }
159 | component.disableOtherRate(rateType, component);
160 | component.updateEstimate(rateType, component);
161 |
162 | storage.get('settings').then(function(settings) {
163 | settings.rateType = rateType;
164 | component.rateType = rateType;
165 | return storage.set('settings', settings);
166 | }).catch(function(ex) {
167 | console.log(ex);
168 | console.log(ex.stack);
169 | });
170 | },
171 |
172 | updateRateDisplay: function(rateType) {
173 |
174 | var id = rateType + 'Radio';
175 | this.$('#' + id).prop('checked', true);
176 | this.updateRateDescription(id);
177 |
178 | },
179 |
180 | afterRender: function() {
181 | var component = this;
182 | var inputBrowsingRate = this.$('#browsingRateGoal');
183 | var inputCalendarRate = this.$('#calendarRateGoal');
184 | var selectBrowsingRate = this.$('#browsingRateInterval');
185 | var selectCalendarRate = this.$('#calendarRateInterval');
186 |
187 | storage.get('settings').then(function(settings) {
188 | inputBrowsingRate.val(settings.donationGoalBrowsingRate);
189 | inputCalendarRate.val(settings.donationGoalCalendarRate);
190 |
191 | settings.rateType = component.rateType;
192 |
193 | selectBrowsingRate.find('[value=' + settings.donationIntervalBrowsingRate.toString() + ']').attr('selected', true);
194 |
195 | selectCalendarRate.find('[value=' + settings.donationIntervalCalendarRate.toString() + ']').attr('selected', true);
196 |
197 |
198 | component.updateRateDisplay(component.rateType);
199 | return storage.set('settings', settings);
200 |
201 | }).catch(function(ex) {
202 | console.log(ex);
203 | });
204 | }
205 | };
206 |
207 | DonationGoalComponent.prototype.__proto__ = Component.prototype;
208 |
209 | export default DonationGoalComponent;
210 |
--------------------------------------------------------------------------------
/shared/scripts/lib/components/donation-goal/donation-goal.styl:
--------------------------------------------------------------------------------
1 | select, input[type=text]
2 | padding 5px
3 | font-size 24px
4 | font-weight bold
5 | color #5F5F5F
6 | transition background linear .1s
7 | background rgba(255, 249, 184, 0.65)
8 | border: 2px solid #E0E0E0;
9 | border-radius: 5px;
10 | box-shadow: 4px 1px rgba(224, 224, 224, 0.25);
11 | box-sizing border-box
12 | width 8%
13 |
14 | &:focus
15 | outline none
16 |
17 | &:disabled
18 | opacity 0.30
19 |
20 |
21 | input[type=radio]
22 | background-color rgba(255, 249, 184, 0.65)
23 |
24 | select
25 | width auto
26 |
27 | &:disabled
28 | opacity 0.30
29 |
30 | span.per
31 | margin-left 10px
32 | margin-right 10px
33 | font-size 24px
34 | font-weight bold
35 | color #E0E0E0
36 |
37 | &.active
38 | color #848484
39 |
40 | span.rates
41 | margin-left 10px
42 | margin-right 10px
43 | font-size 24px
44 | font-weight bold
45 | color #848484
--------------------------------------------------------------------------------
/shared/scripts/lib/components/log-table/entry-history.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | URL
7 | Author information
8 | Access time
9 |
10 |
11 |
12 |
13 | {%each entry.entries as visit%}
14 | {%if visit.tab%}
15 |
16 | {{visit.tab.url}}
17 |
18 | {%if visit.author.list.length%}
19 | {%if visit.author.list.0.name%}
20 | {{visit.author.list.0.name}}
21 | {%else%}
22 |
23 | No name available
24 | {%endif%}
25 | {%else%}
26 | No author tag present
27 | {%endif%}
28 |
29 |
30 | {{visit|formatAccessTime}}
31 |
32 | {%endif%}
33 | {%endeach%}
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/shared/scripts/lib/components/log-table/log-table.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Domain
5 | Has Author Information
6 | Visit count
7 | Days with visit
8 | Time spent
9 | Last time visited
10 |
11 |
12 |
13 |
14 | {%if not entries.length%}
15 |
16 | No log items to display.
17 |
18 | {%else%}
19 | {%each entries as entry key%}
20 |
21 |
22 | {{entry.host}}
23 |
24 |
25 | {%if entry.authorCount%}
26 | {{entry.entries|getAuthor}}
27 | {%else%}
28 | No author information
29 | {%endif%}
30 |
31 | {{entry.entries.length|fixOffByOne}}
32 | {{entry.entries|getDays}}
33 | {{entry.entries|timeSpent}}
34 | {{entry.entries|findLast|formatAccessTime}}
35 |
36 | {%endeach%}
37 | {%endif%}
38 |
39 |
40 |
--------------------------------------------------------------------------------
/shared/scripts/lib/components/log-table/log-table.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Component from '../../component';
4 | import storage from '../../storage';
5 |
6 | function LogTableComponent() {
7 | Component.prototype.constructor.apply(this, arguments);
8 | }
9 |
10 | LogTableComponent.prototype = {
11 | template: 'components/log-table/log-table.html',
12 |
13 | // Register these functions as filters.
14 | filters: [
15 | 'timeSpent',
16 | 'keyLength',
17 | 'hasAuthor',
18 | 'findLast',
19 | 'formatAccessTime',
20 | 'getDays',
21 | 'fixOffByOne',
22 | 'getAuthor'
23 | ],
24 |
25 | /**
26 | * Fixes a bug where daysVisited object counts as an entry addition.
27 | *
28 | * @param val
29 | * @return
30 | */
31 | fixOffByOne: function(val) {
32 | return Number(val) - 1;
33 | },
34 |
35 | /**
36 | * timeSpent
37 | *
38 | * @param val
39 | * @param isValue
40 | * @return
41 | */
42 | timeSpent: function(val, isValue) {
43 | var time = moment.duration(val.slice(1,val.length).reduce(function(prev, current) {
44 | return prev + current.timeSpent;
45 | }, 0), 'milliseconds');
46 |
47 | if (isValue) {
48 | return time;
49 | }
50 |
51 | return time.humanize();
52 | },
53 |
54 | /**
55 | * timeSpentValue
56 | *
57 | * @param val
58 | * @return
59 | */
60 | timeSpentValue: function(val) {
61 | return moment.duration(val.reduce(function(prev, current) {
62 | return prev + current.timeSpent;
63 | }, 0), 'milliseconds');
64 | },
65 |
66 | /**
67 | * keyLength
68 | *
69 | * @param val
70 | * @return
71 | */
72 | keyLength: function(val) {
73 | return Object.keys(val).length;
74 | },
75 |
76 | /**
77 | * hasAuthor
78 | *
79 | * @param val
80 | * @return
81 | */
82 | hasAuthor: function(val) {
83 | return val.authorCount ? 'has' : 'no';
84 | },
85 |
86 | /**
87 | * findLast
88 | *
89 | * @param val
90 | * @return
91 | */
92 | findLast: function(val) {
93 | return val[val.length - 1];
94 | },
95 |
96 | /**
97 | * getDays
98 | *
99 | * @param val
100 | * @ return the amount of days visited
101 | */
102 | getDays: function(val) {
103 | return val[0].daysVisited;
104 | },
105 |
106 |
107 | /**
108 | * getAuthor
109 | *
110 | * @param val
111 | * @ return the author name
112 | */
113 | getAuthor: function(val) {
114 | console.log(val);
115 | return val[1].author.list[0].name;
116 | },
117 |
118 | /**
119 | * formatAccessTime
120 | *
121 | * @param val
122 | * @param isValue
123 | * @return
124 | */
125 | formatAccessTime: function(val, isValue) {
126 | var date = new Date(val.accessTime);
127 |
128 | if (isValue) {
129 | return moment(date).unix();
130 | }
131 |
132 | return moment(date).format("h:mmA - ddd, MMM Do, YYYY");
133 | },
134 |
135 | afterRender: function() {
136 | var component = this;
137 |
138 | // This event will fire before tablesort is hit and will remove any
139 | // expanded entries to avoid order confusion.
140 | this.el.addEventListener('click', function(ev) {
141 | var thead = component.$('thead')[0];
142 |
143 | // Wipe out all entry history logs.
144 | if ($.contains(thead, ev.target)) {
145 | component.$('.entry-history').remove();
146 | component.$('.active').removeClass('active');
147 | }
148 | }, true);
149 |
150 | // Enable table sorting.
151 | this.tablesort = new Tablesort(this.$('table')[0], {
152 | descending: false
153 | });
154 | }
155 | };
156 |
157 | LogTableComponent.prototype.__proto__ = Component.prototype;
158 |
159 | export default LogTableComponent;
160 |
--------------------------------------------------------------------------------
/shared/scripts/lib/components/log-table/log-table.styl:
--------------------------------------------------------------------------------
1 | table
2 | word-wrap break-word
3 | table-layout fixed
4 |
5 | td:not(:first-child)
6 | max-width 150px
7 | overflow hidden
8 | text-overflow ellipsis
9 | white-space nowrap
10 |
11 | > table
12 | margin-top 14px
13 |
14 | th
15 | font-size 80%
16 |
17 | td
18 | .relative
19 | position relative
20 |
21 | a
22 | color #333333
23 |
24 | &:hover
25 | color #6B6B6B
26 |
27 | tr
28 | cursor pointer
29 |
30 | &.entry-history
31 | > td
32 | background-color #FFF !important
33 |
34 | .url
35 | width 500px
36 |
37 | &.has-author
38 | td
39 | background-color #F0FFF1 !important
40 |
41 | &:hover, &.active
42 | td
43 | background-color #DEFCD2 !important
44 |
45 | .favicon
46 | opacity 1
47 |
48 | &.no-author
49 | &:hover, &.active
50 | td
51 | background-color #FCE6D2 !important
52 |
53 | .favicon
54 | opacity 1
55 |
56 | td
57 | background-color #FFF7F0 !important
58 |
59 |
60 | .favicon
61 | width 14px
62 | height 14px
63 | position relative
64 | left -7px
65 | top 1px
66 | opacity 0.5
67 |
68 | &.missing
69 | display inline-block
70 | background url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH3gkSDTIVm5UJUwAAAUxJREFUSMfV1L9LHEEUwPGPsihioyD5C2wsTJPSSjT4o7UUwcpKTJneqE3AyjpdythopylE7BSx0UKLWN0ZBZEcEVHU5imLLNzo3RY+GPa7AzvfeW/2DSVHC8bxuUzJIh7KGq050Q9MYRffg//ga3Ct0Qxm4n0Vo8F76Au+eGMW1gJqscgNroJvcRl8/xZBhu7YYWcMaMtl2NXIAWdRhgEsRXmaFZvoyvAvJk5D1qy4g9ayGy0rmPuAyeBjrKMfwwnr/cTfeoIMPcFn8WzPzb1qw0WCSvTGcx2xj8MEwXWK4CN+B69jOkq2nCAYwkE9wRE+Bf/PdfdWgqCSkkEvfgVvYA5jmE8QTBSVcuHFXdSsOH+6KooyWAnewbfI4EvCorM4qVeiau4vusj9RYsJgmrKGdSwXfBhtdFOHsndrM2IjpeHXMpoiZ0Peq/xCLvSi/4y9MJXAAAAAElFTkSuQmCC")
71 |
--------------------------------------------------------------------------------
/shared/scripts/lib/components/reminders/reminder-interval.html:
--------------------------------------------------------------------------------
1 | Choose your reminder
2 |
3 |
4 |
5 | These are simple notifications, reminding you to check your contributions page.
6 |
7 |
8 |
9 |
48 |
49 |
50 | Your next reminder will trigger: {{ nextNotified|calendar }}.
51 |
52 |
53 |
67 |
--------------------------------------------------------------------------------
/shared/scripts/lib/components/reminders/reminder-interval.styl:
--------------------------------------------------------------------------------
1 | position relative
2 |
3 | output
4 | display block
5 | width calc(100% - 30px)
6 | padding 12px
7 | position relative
8 | height 20px
9 |
10 | span
11 | position absolute
12 | color #E0E0E0
13 |
14 | &.active
15 | color #5F5F5F
16 |
17 | &:nth-child(1)
18 | left 15px
19 |
20 | &:nth-child(2)
21 | left calc(25% - 20px)
22 |
23 | &:nth-child(3)
24 | left calc(50% - 20px)
25 |
26 | &:nth-child(4)
27 | left calc(75% - 20px)
28 |
29 | &:nth-child(5)
30 | right -7px
31 |
32 | &.active:disabled
33 | color #FFFFFF
34 | span.intervalCheckbox
35 | margin-left 10px
36 | margin-right 10px
37 | font-size 24px
38 | font-weight bold
39 | color #848484
40 |
41 | span.fixedIntervalReminder
42 | margin-left 10px
43 | margin-right 10px
44 | font-size 24px
45 | font-weight bold
46 | color #E0E0E0
47 |
48 | &.active
49 | color #848484
50 |
51 | span.dateText, span.dayText
52 | margin-left 10px
53 | margin-right 10px
54 | font-size 24px
55 | font-weight bold
56 | color #E0E0E0
57 |
58 | &.active
59 | color #848484
60 |
61 | div.pos
62 | margin-left 270px
63 | margin-right 10px
64 |
65 | select
66 | padding 10px
67 | font-size 24px
68 | font-weight bold
69 | color #5F5F5F
70 | transition background linear .1s
71 | background rgba(255, 249, 184, 0.65)
72 | border: 2px solid #E0E0E0;
73 | border-radius: 5px;
74 | box-shadow: 4px 1px rgba(224, 224, 224, 0.25);
75 | box-sizing border-box
76 | width auto
77 |
78 | &:disabled
79 | opacity 0.30
80 |
81 | input[type=time]
82 | padding 10px
83 | font-size 24px
84 | font-weight bold
85 | color #5F5F5F
86 | transition background linear .1s
87 | background rgba(255, 249, 184, 0.65)
88 | border: 2px solid #E0E0E0;
89 | border-radius: 5px;
90 | box-shadow: 4px 1px rgba(224, 224, 224, 0.25);
91 | box-sizing border-box
92 | width auto
93 |
94 | &:disabled
95 | opacity 0.30
--------------------------------------------------------------------------------
/shared/scripts/lib/components/reminders/reminder-thresh-global.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Remind me when my total payment reaches
5 |
6 |
7 |
--------------------------------------------------------------------------------
/shared/scripts/lib/components/reminders/reminder-thresh-global.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Component from '../../component';
4 | import storage from '../../storage';
5 |
6 | function ReminderThreshGlobalComponent() {
7 | Component.prototype.constructor.apply(this, arguments);
8 | var span = this.$('.remindWhen');
9 | var input = this.$('input[type=text]');
10 | span.removeClass('active');
11 | input.prop('disabled', true);
12 | var component = this;
13 | storage.get('settings').then(function(settings) {
14 |
15 | if (typeof settings.globalThresholdReminderEnabled === 'undefined') {
16 | settings.globalThresholdReminderEnabled = true;
17 | }
18 |
19 | if (typeof settings.reminderThreshGlobal === 'undefined') {
20 | settings.reminderThreshGlobal = '$10.00';
21 | }
22 |
23 | component.globalThresholdReminderEnabled = settings.globalThresholdReminderEnabled;
24 | component.reminderThreshGlobal = settings.reminderThreshGlobal;
25 | return storage.set('settings', settings);
26 | }).catch(function(ex) {
27 | console.log(ex);
28 | console.log(ex.stack);
29 | });
30 | }
31 |
32 | ReminderThreshGlobalComponent.prototype = {
33 | template: 'components/reminders/reminder-thresh-global.html',
34 |
35 | events: {
36 | 'keyup input[type=text]': 'filterInput',
37 | 'blur input[type=text]': 'formatAndSave',
38 | 'change input[type=text]': 'formatAndSave',
39 | 'change input[type=checkbox]': 'selectedReminderThreshGlobal'
40 | },
41 |
42 | selectedReminderThreshGlobal: function(ev) {
43 | var input = this.$('input[type=text]');
44 | var span = this.$('.remindWhen');
45 | var isChecked = this.$('#threshGlobalCheckbox').prop('checked');
46 |
47 | if (isChecked) {
48 | input.prop('disabled', false);
49 | span.addClass('active');
50 | } else if (!isChecked) {
51 | span.removeClass('active');
52 | input.prop('disabled', true);
53 | }
54 |
55 | // update the settings
56 | storage.get('settings').then(function(settings) {
57 | settings.globalThresholdReminderEnabled = isChecked;
58 | return storage.set('settings', settings);
59 | }).catch(function(ex) {
60 | console.log(ex);
61 | console.log(ex.stack);
62 | });
63 | },
64 |
65 | filterInput: function(ev) {
66 | var val = ev.target.value.replace(/[^0-9.]/g, '');
67 | this.$('input[type=text]').val('$' + val);
68 | },
69 |
70 | formatAndSave: function(ev) {
71 | var component = this;
72 | var val = ev.target.value.replace(/[^0-9.]/g, '');
73 | var currency = '$' + parseFloat(val).toFixed(2);
74 |
75 | this.$('input[type=text]').val(currency);
76 |
77 | storage.get('settings').then(function(settings) {
78 | settings.reminderThreshGlobal = currency;
79 | settings.globalReminded = false;
80 | return storage.set('settings', settings);
81 | });
82 | },
83 |
84 |
85 | afterRender: function() {
86 | // clear the checkboxes first to prevent flashing
87 | var span = this.$('.remindWhen');
88 | var input = this.$('input[type=text]');
89 | span.removeClass('active');
90 | input.prop('disabled', true);
91 | this.$('#threshGlobalCheckbox').prop('checked', false);
92 | var component = this;
93 |
94 | var isSelected;
95 |
96 | storage.get('settings').then(function(settings) {
97 | input.val(settings.reminderThreshGlobal || component.reminderThreshGlobal);
98 | isSelected = settings.globalThresholdReminderEnabled;
99 | component.globalThresholdReminderEnabled = isSelected;
100 |
101 | if (isSelected === true || (typeof isSelected === 'undefined')) {
102 | component.$('#threshGlobalCheckbox').prop('checked', true);
103 | component.selectedReminderThreshGlobal(null);
104 | } else if(isSelected === false) {
105 | component.$('#threshGlobalCheckbox').prop('checked', false);
106 | component.selectedReminderThreshGlobal(null);
107 | } else {
108 | console.info('error reading if check selected from settings');
109 | }
110 | }).catch(function(ex) {
111 | console.log(ex);
112 | });
113 | }
114 | };
115 |
116 | ReminderThreshGlobalComponent.prototype.__proto__ = Component.prototype;
117 |
118 | export default ReminderThreshGlobalComponent;
119 |
--------------------------------------------------------------------------------
/shared/scripts/lib/components/reminders/reminder-thresh-global.styl:
--------------------------------------------------------------------------------
1 | select, input[type=text]
2 | padding 10px
3 | font-size 24px
4 | font-weight bold
5 | color #5F5F5F
6 | transition background linear .1s
7 | background rgba(255, 249, 184, 0.65)
8 | border: 2px solid #E0E0E0;
9 | border-radius: 5px;
10 | box-shadow: 4px 1px rgba(224, 224, 224, 0.25);
11 | box-sizing border-box
12 | width 15%
13 |
14 | &:focus
15 | outline none
16 |
17 | &:disabled
18 | opacity 0.30
19 |
20 |
21 | span.threshGlobalCheckbox
22 | margin-left 10px
23 | margin-right 10px
24 | font-size 24px
25 | font-weight bold
26 | color #848484
27 |
28 | span.remindWhen
29 | margin-left 10px
30 | margin-right 10px
31 | font-size 24px
32 | font-weight bold
33 | color #E0E0E0
34 |
35 |
36 | &.active
37 | color #848484
--------------------------------------------------------------------------------
/shared/scripts/lib/components/reminders/reminder-thresh-local.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Remind me when my payment for a particular author reaches
5 |
6 |
7 |
--------------------------------------------------------------------------------
/shared/scripts/lib/components/reminders/reminder-thresh-local.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Component from '../../component';
4 | import storage from '../../storage';
5 |
6 | function ReminderThreshLocalComponent() {
7 | Component.prototype.constructor.apply(this, arguments);
8 | var span = this.$('.remindWhen');
9 | var input = this.$('input[type=text]');
10 | span.removeClass('active');
11 | input.prop('disabled', true);
12 | var component = this;
13 | storage.get('settings').then(function(settings) {
14 |
15 | if (typeof settings.localThresholdReminderEnabled === 'undefined') {
16 | settings.localThresholdReminderEnabled = true;
17 | }
18 |
19 | if (typeof settings.reminderThreshLocal === 'undefined') {
20 | settings.reminderThreshLocal = '$10.00';
21 | }
22 |
23 | component.localThresholdReminderEnabled = settings.localThresholdReminderEnabled;
24 | component.reminderThreshLocal = settings.reminderThreshLocal;
25 | return storage.set('settings', settings);
26 | }).catch(function(ex) {
27 | console.log(ex);
28 | console.log(ex.stack);
29 | });
30 | }
31 |
32 | ReminderThreshLocalComponent.prototype = {
33 | template: 'components/reminders/reminder-thresh-local.html',
34 |
35 | events: {
36 | 'keyup input[type=text]': 'filterInput',
37 | 'blur input[type=text]': 'formatAndSave',
38 | 'change input[type=text]': 'formatAndSave',
39 | 'change input[type=checkbox]': 'selectedReminderThreshLocal'
40 | },
41 |
42 | selectedReminderThreshLocal: function(ev) {
43 | var input = this.$('input[type=text]');
44 | var span = this.$('.remindWhen');
45 | var isChecked = this.$('#threshLocalCheckbox').prop('checked');
46 |
47 | if (isChecked) {
48 | input.prop('disabled', false);
49 | span.addClass('active');
50 | } else if (!isChecked) {
51 | span.removeClass('active');
52 | input.prop('disabled', true);
53 | }
54 |
55 | // update the settings
56 | storage.get('settings').then(function(settings) {
57 | settings.localThresholdReminderEnabled = isChecked;
58 | return storage.set('settings', settings);
59 | }).catch(function(ex) {
60 | console.log(ex);
61 | console.log(ex.stack);
62 | });
63 | },
64 |
65 | filterInput: function(ev) {
66 | var val = ev.target.value.replace(/[^0-9.]/g, '');
67 | this.$('input[type=text]').val('$' + val);
68 | },
69 |
70 | formatAndSave: function(ev) {
71 | var component = this;
72 | var val = ev.target.value.replace(/[^0-9.]/g, '');
73 | var currency = '$' + parseFloat(val).toFixed(2);
74 |
75 | this.$('input[type=text]').val(currency);
76 |
77 | storage.get('settings').then(function(settings) {
78 | settings.reminderThreshLocal = currency;
79 | settings.localReminded = false;
80 | return storage.set('settings', settings);
81 | }).catch(function(ex) {
82 | console.log(ex);
83 | console.log(ex.stack);
84 | });
85 | },
86 |
87 |
88 | afterRender: function() {
89 | // clear the checkboxes first to prevent flashing
90 | var span = this.$('.remindWhen');
91 | var input = this.$('input[type=text]');
92 | span.removeClass('active');
93 | input.prop('disabled', true);
94 | this.$('#threshLocalCheckbox').prop('checked', false);
95 | var component = this;
96 |
97 | var isSelected;
98 | storage.get('settings').then(function(settings) {
99 | input.val(settings.reminderThreshLocal || component.reminderThreshLocal);
100 | isSelected = settings.localThresholdReminderEnabled;
101 | component.localThresholdReminderEnabled = isSelected;
102 | if (isSelected === true || (typeof isSelected === 'undefined')) {
103 | component.$('#threshLocalCheckbox').prop('checked', true);
104 | component.selectedReminderThreshLocal(null);
105 | } else if(isSelected === false) {
106 | component.$('#threshLocalCheckbox').prop('checked', false);
107 | component.selectedReminderThreshLocal(null);
108 | } else {
109 | console.info('error reading if check selected from settings');
110 | }
111 | }).catch(function(ex) {
112 | console.log(ex);
113 | console.log(ex.stack);
114 | });
115 | }
116 | };
117 |
118 | ReminderThreshLocalComponent.prototype.__proto__ = Component.prototype;
119 |
120 | export default ReminderThreshLocalComponent;
121 |
--------------------------------------------------------------------------------
/shared/scripts/lib/components/reminders/reminder-thresh-local.styl:
--------------------------------------------------------------------------------
1 | select, input[type=text]
2 | padding 10px
3 | font-size 24px
4 | font-weight bold
5 | color #5F5F5F
6 | transition background linear .1s
7 | background rgba(255, 249, 184, 0.65)
8 | border: 2px solid #E0E0E0;
9 | border-radius: 5px;
10 | box-shadow: 4px 1px rgba(224, 224, 224, 0.25);
11 | box-sizing border-box
12 | width 15%
13 |
14 | &:focus
15 | outline none
16 |
17 | &:disabled
18 | opacity 0.30
19 |
20 |
21 | span.threshLocalCheckbox
22 | margin-left 10px
23 | margin-right 10px
24 | font-size 24px
25 | font-weight bold
26 | color #848484
27 |
28 | span.remindWhen
29 | margin-left 10px
30 | margin-right 10px
31 | font-size 24px
32 | font-weight bold
33 | color #E0E0E0
34 |
35 | &.active
36 | color #848484
--------------------------------------------------------------------------------
/shared/scripts/lib/components/user-agreement/user-agreement.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Check to help Tipsy grow and improve by sending completely anonymous data. Tipsy will never be able to identify you or distinguish you from other Tipsy users.
6 |
7 |
8 |
--------------------------------------------------------------------------------
/shared/scripts/lib/components/user-agreement/user-agreement.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Component from '../../component';
4 | import storage from '../../storage';
5 |
6 | function UserAgreementComponent() {
7 | Component.prototype.constructor.apply(this, arguments);
8 | var component = this;
9 | storage.get('settings').then(function(settings) {
10 | component.userAgrees = settings.userAgrees;
11 |
12 | }).catch(function(ex) {
13 | console.log(ex);
14 | console.log(ex.stack);
15 | });
16 | }
17 |
18 | UserAgreementComponent.prototype = {
19 | template: 'components/user-agreement/user-agreement.html',
20 |
21 | events: {
22 | 'change input[type=checkbox]' : 'agreementChanged'
23 | },
24 |
25 | agreementChanged: function(ev) {
26 | this.updateUserAgreement(this.$('#userAgreementCheckbox').prop('checked'));
27 | },
28 |
29 | updateUserAgreement: function(userAgrees) {
30 | if (userAgrees) {
31 | this.$('#userAgreementCheckbox').prop('checked', true);
32 | this.$('.userAgreementCheckbox').addClass('active');
33 | } else {
34 | this.$('#userAgreementCheckbox').prop('checked', false);
35 | this.$('.userAgreementCheckbox').removeClass('active');
36 | }
37 |
38 | storage.get('settings').then(function(settings) {
39 | settings.userAgrees = userAgrees;
40 | return storage.set('settings', settings);
41 | }).catch(function(ex) {
42 | console.log(ex);
43 | console.log(ex.stack);
44 | });
45 | },
46 |
47 | afterRender: function() {
48 | var component = this;
49 | storage.get('settings').then(function(settings) {
50 | component.userAgrees = settings.userAgrees;
51 |
52 | if (typeof component.userAgrees === 'undefined') {
53 | console.error('user agreement not set');
54 | }
55 |
56 | component.updateUserAgreement(component.userAgrees);
57 |
58 | }).catch(function(ex) {
59 | console.log(ex);
60 | console.log(ex.stack);
61 | });
62 |
63 | }
64 |
65 | };
66 |
67 | UserAgreementComponent.prototype.__proto__ = Component.prototype;
68 |
69 | export default UserAgreementComponent;
70 |
--------------------------------------------------------------------------------
/shared/scripts/lib/components/user-agreement/user-agreement.styl:
--------------------------------------------------------------------------------
1 | span.userAgreementCheckbox
2 | margin-left 10px
3 | margin-right 10px
4 | font-size 24px
5 | font-weight bold
6 | color #E0E0E0
7 |
8 | &.active
9 | color #848484
10 |
--------------------------------------------------------------------------------
/shared/scripts/lib/defaults.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import storage from './storage';
4 |
5 | export var defaults = {
6 | 'intervalsEnables' : true,
7 | 'dateIntervalEnabled': true,
8 | 'dayIntervalEnabled': false,
9 | 'days': 1,
10 | 'donationGoalBrowsingRate': '$0.01',
11 | 'donationGoalCalendarRate': '$10.0',
12 | 'donationIntervalCalendarRate': 43200, // donationInterval in minutes
13 | 'donationIntervalBrowsingRate': 1,
14 | 'globalThresholdReminderEnabled': false,
15 | 'localThresholdReminderEnabled': true,
16 | 'rateType': 'calendarRate', // 'calendarRate or browsingRate'
17 | 'reminderThreshGlobal': '$10.00',
18 | 'reminderThreshLocal': '$10.00',
19 | 'timeSpanNumber': '1',
20 | 'timeSpanType': '7', // in days, 1, 7 or 30
21 | 'timeSpanTime': '10:00',
22 | 'weekdayInterval': 'Sunday',
23 | 'weekdayIntervalTime': '10:00',
24 | 'userAgrees': false,
25 | 'idle': 20,
26 | 'maxDonationsTableSize': 3,
27 | 'estimate': {'minutes': 20160, 'amount': '2', 'type': 'weeks'}
28 | };
29 |
30 | export function setDefaults() {
31 | storage.get('settings').then(function(settings) {
32 | for (var key in defaults) {
33 | if (typeof settings[key] == 'undefined') {
34 | settings[key] = defaults[key];
35 | } else {
36 | }
37 | }
38 | return storage.set('settings', settings);
39 | }).catch(function(ex) {
40 | console.log(ex);
41 | console.log(ex.stack);
42 | });
43 | }
44 |
45 | export var intervals = {
46 | 1: 'minute',
47 | 60: 'hour',
48 | 1440: 'day',
49 | 10080: 'week',
50 | 43200: 'month',
51 | 525949: 'year'
52 | };
53 |
--------------------------------------------------------------------------------
/shared/scripts/lib/dom.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Coerces a NodeList to an Array.
5 | *
6 | * @return {Array} of elements.
7 | */
8 | function toArray() {
9 | return Array.prototype.slice.call(arguments[0]);
10 | }
11 |
12 | /**
13 | * Alias to `Document#querySelector` that optionally accepts a context to scope
14 | * lookups to.
15 | *
16 | * @param {string} selector - to match an element.
17 | * @return {Object} matched element.
18 | */
19 | export function select(selector, ctx) {
20 | ctx = ctx || document;
21 | return ctx.querySelector(selector);
22 | }
23 |
24 | /**
25 | * Alias to `Document#querySelectorAll` that optionally accepts a context to
26 | * scope lookups to.
27 | *
28 | * @param {string} selector - to match elements.
29 | * @param {Object} context - element to scope lookups to.
30 | * @return {Array} of matched elements.
31 | */
32 | export function selectAll(selector, ctx) {
33 | ctx = ctx || document;
34 | return toArray(ctx.querySelectorAll(selector));
35 | }
36 |
--------------------------------------------------------------------------------
/shared/scripts/lib/environment.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Default the environment to null.
4 | export var environment = 'chrome';
5 |
--------------------------------------------------------------------------------
/shared/scripts/lib/extension.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { environment } from './environment';
4 | import storage from './storage';
5 | import { start, stop } from './activity';
6 | import { listen } from './notifications';
7 | import { getCurrentTab, tabs } from './tabs';
8 | import { setDefaults } from './defaults';
9 | import { giveUniqueIdentifier } from './identifier';
10 |
11 | /**
12 | * Opens the extension in a new tab window.
13 | *
14 | * @param {Object} options - to specify configuration.
15 | */
16 | export function createExtension(options) {
17 | // In Chrome we only need to set up the icon click event to open the
18 | // extension.
19 |
20 | //console.log(defaults);
21 | if (environment === 'chrome') {
22 | // Listen for notifications.
23 | listen();
24 | //console.log(defaults);
25 | //setDefaults();
26 | giveUniqueIdentifier();
27 | chrome.browserAction.onClicked.addListener(function() {
28 | chrome.tabs.create({
29 | url: chrome.extension.getURL(options.indexUrl)
30 | });
31 | });
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/shared/scripts/lib/hardcoded-doms.js:
--------------------------------------------------------------------------------
1 | 'use-strict';
2 |
3 | export var domains = {
4 | 'theyeshivaworld.com' : {'name': 'Philippe',
5 | 'paypal': 'schilippe@paypal.com'
6 | }
7 | };
8 |
9 |
--------------------------------------------------------------------------------
/shared/scripts/lib/identifier.js:
--------------------------------------------------------------------------------
1 | import storage from './storage';
2 |
3 | export function giveUniqueIdentifier() {
4 | storage.get('settings').then(function(settings) {
5 | if (!settings.userId) {
6 | var token = createRandomToken(32);
7 | settings.userId = token;
8 | return storage.set('settings', settings);
9 | }
10 | }).catch(function(ex) {
11 | console.log(ex);
12 | console.log(ex.stack);
13 | });
14 | }
15 |
16 | function createRandomToken(nums) {
17 | var ar = new Uint8Array(nums);
18 | crypto.getRandomValues(ar);
19 | var token = '';
20 | for (var i = 0; i < ar.length; ++i) {
21 | token += ar[i].toString(16);
22 | }
23 | return token;
24 | }
25 |
--------------------------------------------------------------------------------
/shared/scripts/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { environment } from './environment';
4 | import storage from './storage';
5 | import Component from './component';
6 |
7 | // Pages.
8 | import GettingStartedPage from './pages/getting-started/getting-started';
9 | import LogPage from './pages/log/log';
10 | import SettingsPage from './pages/settings/settings';
11 | import DonationsPage from './pages/donations/donations';
12 |
13 | // Register all pages.
14 | var pages = {
15 | '#getting-started': GettingStartedPage,
16 | '#log': LogPage,
17 | '#settings': SettingsPage,
18 | '#donations': DonationsPage
19 | };
20 |
21 | // Register each page, and swap the pages object value from constructor to
22 | // instance.
23 | Object.keys(pages).forEach(function(selector) {
24 | pages[selector] = Component.registerPage(selector, pages[selector]);
25 | });
26 |
27 | /**
28 | * Sets the current tab in the extension.
29 | */
30 | function setTab() {
31 | var hash = location.hash;
32 | var page = pages[hash];
33 | var params = {};
34 |
35 | // Used for payment redirection.
36 | var host = localStorage.host;
37 | var url = localStorage.url;
38 |
39 | // When opening the extension without a hash determine where to route based
40 | // on if the end user has already configured the getting started page or not.
41 | if (!hash) {
42 | if (!location.search) {
43 | return storage.get('settings').then(function(settings) {
44 | // Update the hash fragment to change pages.
45 | location.href = settings.showLog ? '#donations' : '#getting-started';
46 | });
47 | }
48 |
49 | params = deparam(location.search.slice(1));
50 |
51 | delete window.localStorage.url;
52 | delete window.localStorage.host;
53 |
54 | // Coerce to an array, since error sometimes comes back as an array.
55 | if ([].concat(params.error).indexOf('failure') > -1) {
56 | // Remove the query string from the url.
57 | history.replaceState({}, "", location.href.split("?")[0]);
58 |
59 | // Display the error.
60 | window.alert(params.error_description);
61 |
62 | // Redirect to donations.
63 | location.href = '#donations';
64 |
65 | return;
66 | }
67 |
68 | // Otherwise we can assume the payment was successful. We can now
69 | // remove all the items from the storage.
70 | return storage.get('settings').then(function(settings) {
71 | return storage.get('log').then(function(resp) {
72 | // Filter out these items.
73 |
74 | resp[host].map(function(entry) {
75 | if (entry.tab && entry.tab.url === url) {
76 | entry.paid = true;
77 | }
78 | });
79 |
80 | return storage.set('log', resp).then(function() {
81 | // Remove the query string from the url.
82 | history.replaceState({}, "", location.href.split("?")[0]);
83 |
84 | var amount = Number(params.amount.slice(1));
85 | settings.totalPaid = settings.totalPaid + amount;
86 | storage.set('settings', settings);
87 |
88 | // Display a success message.
89 | window.alert('Payment of ' + params.amount + ' to ' + params.email +
90 | ' was successful');
91 |
92 | // Redirect to donations.
93 | location.href = '#donations';
94 | });
95 | });
96 | }).catch(function() {
97 | // Remove the query string from the url.
98 | history.replaceState({}, "", location.href.split("?")[0]);
99 |
100 | // Redirect to donations.
101 | location.href = '#donations';
102 | });
103 | }
104 | else {
105 | // Render the page we're currently on, if we haven't already.
106 | if (!page.__rendered__) {
107 | // Mark this page as rendered, so that we do not re-render.
108 | page.__rendered__ = true;
109 | page.render();
110 | }
111 |
112 | // Augment the navigation depending on which page we're on.
113 | $('nav a').each(function() {
114 | var link = $(this);
115 | var body = $('body');
116 | link.removeClass('active');
117 |
118 | if (hash.trim() !== '#getting-started') {
119 | body.removeClass('intro');
120 |
121 | // Add the new class to the tab link.
122 | if (link[0].hash === hash) {
123 | link.addClass('active');
124 | }
125 | }
126 | else {
127 | $('body').addClass('intro');
128 | }
129 | });
130 | }
131 | }
132 |
133 | // Set the correct active tab on load.
134 | setTab();
135 |
136 | // Ensure that the tab is changed whenever the hash value is updated.
137 | window.addEventListener('hashchange', setTab, true);
138 |
139 | // Use a more precise formatter for huamanize.
140 | // https://github.com/moment/moment/issues/348#issuecomment-6535794
141 | moment.lang('precise-en', {
142 | relativeTime : {
143 | future : "in %s",
144 | past : "%s ago",
145 | // See: https://github.com/timrwood/moment/pull/232#issuecomment-4699806
146 | s : "%d seconds",
147 | m : "a minute",
148 | mm : "%d minutes",
149 | h : "an hour",
150 | hh : "%d hours",
151 | d : "a day",
152 | dd : "%d days",
153 | M : "a month",
154 | MM : "%d months",
155 | y : "a year",
156 | yy : "%d years"
157 | }
158 | });
159 |
160 | // Set this precise formatter globally.
161 | moment.lang('precise-en');
162 |
--------------------------------------------------------------------------------
/shared/scripts/lib/notifications.js:
--------------------------------------------------------------------------------
1 | import { environment } from './environment';
2 | import storage from './storage';
3 | import calculate from './utils/calculate';
4 |
5 | export var toDays = [
6 | // Daily.
7 | 1,
8 | // Half weekly.
9 | 3.5,
10 | // Weekly.
11 | 7,
12 | // Bi-weekly.
13 | 14,
14 | // Monthly.
15 | 30
16 | ];
17 |
18 | /**
19 | * Schedule a new notification.
20 | *
21 | * @param {number} when - time as a moment object or unix timestamp.
22 | * @param {number} days - how many days until the next notification.
23 | */
24 | export function create(name, when, days) {
25 | // Convert the repeating days to minutes.
26 | var minutes = days * (24 * 60);
27 |
28 | // When using the same name, Chrome will automatically clear out the previous
29 | // notification.
30 | chrome.alarms.create(name, {
31 | when: Number(when),
32 | periodInMinutes: minutes
33 | });
34 | }
35 |
36 | export function clear(name) {
37 | if (environment === 'chrome') {
38 | chrome.alarms.clear(name);
39 | }
40 | }
41 |
42 | export function get(name) {
43 | return new Promise(function(resolve) {
44 | chrome.alarms.get(name, resolve);
45 | });
46 | }
47 |
48 | /**
49 | * Allows to create notification manually, used for thresholdreminders
50 | */
51 | export function notify(name, type, amount, url) {
52 | if (!url) {
53 | url = ".";
54 | } else {
55 | url = " to "+url+ ".";
56 | }
57 | chrome.notifications.create(name, {
58 | type: 'basic',
59 | iconUrl: '../img/logo64.png',
60 | title: 'Tipsy',
61 | message: 'Time to donate! You have reached your '+ type + ' threshold with an amount of $' + amount+ url
62 | }, function unhandledCallback() {});
63 |
64 | addClickable();
65 | }
66 |
67 | function addClickable() {
68 | chrome.notifications.onClicked.addListener(function(notificationId, buttonIndex) {
69 | chrome.tabs.create({url:chrome.extension.getURL('html/index.html')});
70 | });
71 | }
72 |
73 | /**
74 | * Polls and runs threshold logic on the background thread to avoid the need
75 | * for being in the extension to get notifications.
76 | */
77 | function pollForNotifications() {
78 | Promise.all([
79 | storage.get('settings'),
80 | storage.get('log')
81 | ]).then(function(resp) {
82 | var settings = resp[0];
83 | var log = resp[1];
84 | var totalOwed = 0;
85 | var u = "";
86 | // Iterate all visited pages.
87 | Object.keys(log).forEach(function(domain) {
88 | // Filter down to those with valid payment information.
89 | log[domain].filter(function(item) {
90 | // Has a valid author list.
91 | if (item && item.author && item.author.list.length) {
92 | return item.author.list.some(function(item) {
93 | return item.bitcoin || item.dwolla || item.paypal;
94 | });
95 | }
96 | }).forEach(function(hasPaymentInfo) {
97 | // Don't modify the existing object.
98 | var internal = Object.create(hasPaymentInfo);
99 | var calculated = calculate(settings, internal);
100 | var amountNum = parseFloat(calculated.estimatedAmount);
101 |
102 | totalOwed += amountNum;
103 |
104 | // let notifications know there is money to pay
105 | if (amountNum > 0) {
106 | settings.moneyIsOwed = true;
107 | }
108 | if (settings.reminderThreshLocal && (amountNum >= parseFloat(settings.reminderThreshLocal.slice(1))) && !settings.localReminded) {
109 | notify('tipsy-thersh-local', 'local', amountNum.toFixed(2), calculated.author.hostname);
110 | settings.localReminded = true;
111 | }
112 | });
113 | });
114 |
115 | if (settings.reminderThreshGlobal && (totalOwed >= parseFloat(settings.reminderThreshGlobal.slice(1))) && !settings.globalReminded) {
116 | notify('tipsy-thresh-global', 'global', totalOwed.toFixed(2), null);
117 | settings.globalReminded = true;
118 | }
119 | }).catch(function(ex) {
120 | console.log(ex, ex.message);
121 | });
122 | }
123 |
124 | /**
125 | * Listens for Chrome alarms to trigger the next notification.
126 | */
127 | export function listen(worker) {
128 | storage.get('settings').then(function(settings) {
129 | var createNotification = function(name) {
130 | // Once the alarm triggers, create a notification to dispaly to the
131 | // user.
132 | if (settings.moneyIsOwed) {
133 | console.log('creating');
134 | chrome.notifications.create("tipsy", {
135 | type: 'basic',
136 | iconUrl: '../img/logo64.png',
137 | title: 'Tipsy',
138 | message: 'Time to Donate!'
139 | }, function unhandledCallback() {
140 | // Reset the next notified in the storage engine.
141 | var days = toDays[settings.reminderLevel];
142 | var next = new Date(settings.nextNotified);
143 | next.setDate(next.getDate() + days);
144 | settings.nextNotified = Number(next);
145 |
146 | // Create the next alarm.
147 | create(name, next, days);
148 |
149 | storage.set('settings', settings);
150 | });
151 | addClickable();
152 | }
153 | };
154 | if (Number(settings.nextNotified) < Date.now()) {
155 | createNotification();
156 | }
157 |
158 | chrome.alarms.onAlarm.addListener(function(alarm) {
159 | createNotification(alarm.name);
160 | });
161 | });
162 |
163 | // Check for notifications and then poll every hour.
164 | pollForNotifications();
165 | setInterval(pollForNotifications, 3600000);
166 | }
167 |
--------------------------------------------------------------------------------
/shared/scripts/lib/pages/donations/donations.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Make a contribution
5 |
6 |
7 |
8 |
9 | Here you can pay your calculated contributions or customize the amount. Click on a payee's row to get a detailed list.
10 |
11 |
12 | {%if hidden%} Show more {%else%} Hide {%endif%}
13 |
14 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/shared/scripts/lib/pages/donations/donations.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Component from '../../component';
4 | import storage from '../../storage';
5 | import calculate from '../../utils/calculate';
6 | import { inject as injectDwolla } from '../../processors/dwolla';
7 | import { getDwollaButton as dwollaBtn } from '../../processors/dwolla';
8 | import { inject as injectPaypal } from '../../processors/paypal';
9 | import { defaults } from '../../defaults';
10 |
11 | function DonationsPage() {
12 | Component.prototype.constructor.apply(this, arguments);
13 |
14 | // Set the default table data.
15 | this.data = {
16 | entries: [],
17 | details: []
18 | };
19 |
20 | // Always attempt to render the inner table.
21 | this.renderTable();
22 |
23 | this.hidden = true;
24 |
25 | // Whenever the data changes re-render the table.
26 | storage.onChange(this.renderTable.bind(this));
27 |
28 | var component = this;
29 |
30 |
31 | this.entryDonation = this.fetch('pages/donations/entry-donation.html')
32 |
33 | .then(function(contents) {
34 | var template = combyne(contents);
35 |
36 | [].concat(component.filters).forEach(function(filter) {
37 | template.registerFilter(filter, component[filter]);
38 | });
39 | return template;
40 | });
41 | }
42 |
43 | DonationsPage.prototype = {
44 | template: 'pages/donations/donations.html',
45 |
46 | events: {
47 | 'keyup input': 'filterInput',
48 | 'blur .amount': 'formatAndSave',
49 | 'change .amount': 'formatAndSave',
50 | 'click .remove': 'remove',
51 | 'click .removeInner': 'remove',
52 | 'click .hide': 'toggleHidden',
53 | 'click tbody tr td.clickable ': 'toggleEntryDonation',
54 | 'mouseenter .remove': 'addHighlight',
55 | 'mouseenter .removeInner': 'addHighlight',
56 | 'mouseleave .remove': 'removeHighlight',
57 | 'mouseleave .removeInner': 'removeHighlight',
58 | 'mouseleave tr.entry': 'removeHighlight',
59 | 'mouseenter tr.entry': 'addHighlight',
60 | },
61 |
62 | filters: [
63 | 'timeSpent',
64 | 'or'
65 | ],
66 |
67 | or: function(one, other) {
68 | return one || other;
69 | },
70 |
71 | sorter: function(a, b) {
72 | if (Number(a.estimatedAmount) > Number(b.estimatedAmount)) {
73 | return -1;
74 | }
75 | if (Number(a.estimatedAmount) < Number(b.estimatedAmount)) {
76 | return 1;
77 | }
78 | return 0;
79 | },
80 |
81 | // had to add these events because of CSS limitations.
82 | addHighlight: function(ev) {
83 | var curr = $(ev.currentTarget);
84 | if (curr.hasClass('entry')) {
85 | curr.addClass("highlighted");
86 | return;
87 | }
88 | if (curr.hasClass("remove")) {
89 | curr.closest('tr.entry').addClass("highlighted");
90 | } else if (curr.hasClass("removeInner")) {
91 | curr.closest('tr.subentry').addClass("highlighted");
92 | }
93 | },
94 |
95 | removeHighlight: function(ev) {
96 | var curr = $(ev.currentTarget);
97 | if (curr.hasClass('entry')) {
98 | curr.removeClass("highlighted");
99 | return;
100 | }
101 | if (curr.hasClass("remove")) {
102 | curr.closest('tr.entry').removeClass("highlighted");
103 | } else if (curr.hasClass("removeInner")) {
104 | curr.closest('tr.subentry').removeClass("highlighted");
105 | }
106 | },
107 |
108 | toggleEntryDonation: function(ev) {
109 | var component = this;
110 | var tr = $(($(ev.currentTarget)).parents()[0]);
111 |
112 | if (tr.parents('th').length) {
113 | return false;
114 | }
115 |
116 | if (!tr.hasClass('entry')) {
117 | return false;
118 | }
119 |
120 | tr.toggleClass('active');
121 |
122 | this.entryDonation.then(function(template) {
123 | var host = tr.data('host');
124 |
125 | if (tr.is('.active') && component.data.details[0]) {
126 | var current = [];
127 | current.data = [];
128 | current.data.details = [];
129 | current.data.details = component.data.details.filter(function(entry) {
130 | return entry.author.hostname == host;
131 | });
132 | tr.after(template.render({entry: current.data}));
133 | new Tablesort(component.$('tr.entry-donation table')[0], {
134 | descending: true
135 | });
136 |
137 | component.$('tr.subentry').each(function() {
138 | var component = this;
139 | var $component = $(component);
140 | // Extract the estimated value.
141 | var amount = $component.find('.amount').val().slice(1);
142 |
143 | // The payment container.
144 | var payment = $component.find('.payment');
145 |
146 | var dwollaToken = $component.attr('data-dwolla');
147 | var paypalToken = $component.attr('data-paypal');
148 |
149 | // Hide the no processors text.
150 | if (dwollaToken || paypalToken) {
151 | payment.empty();
152 | }
153 |
154 | // Only inject if the author has dwolla.
155 | if (dwollaToken) {
156 | $component.data().dwolla = injectDwolla(payment, amount, dwollaToken);
157 | }
158 |
159 | // Only inject if the author has paypal.
160 | if (paypalToken) {
161 | $component.data().paypal = injectPaypal(payment, amount, paypalToken);
162 | }
163 |
164 | if (dwollaToken && !payment.hasClass("d-btn")) {
165 | payment.prepend(dwollaBtn(payment, amount, dwollaToken));
166 | }
167 | });
168 | } else {
169 | tr.next('tr.entry-donation').remove();
170 | }
171 | });
172 | },
173 |
174 | toggleHidden: function(ev) {
175 | this.hidden = !this.hidden;
176 | if (this.hidden === true) {
177 | $(".hide").text("Show more");
178 | } else if (this.hidden === false) {
179 | $(".hide").text("Hide") ;
180 | }
181 | //this.renderTable();
182 | if (this.hidden) {
183 | $(".hidden").hide();
184 | } else {
185 | $(".hidden").show();
186 | }
187 | },
188 |
189 | /**
190 | * timeSpent
191 | *
192 | * @param val
193 | * @return
194 | */
195 | timeSpent: function(val) {
196 | return moment.duration(val, 'milliseconds').humanize();
197 | },
198 |
199 | remove: function(ev) {
200 | if (window.confirm('Are you sure you want to remove this entry from your contributions? This action cannot be undone.')) {
201 | var el = $(ev.currentTarget).closest('tr');
202 | var row = el.data();
203 | var host = row.host;
204 | var url = row.url;
205 | if (el.hasClass('entry')) {
206 | storage.get('settings').then(function(settings) {
207 | storage.get('log').then(function(resp) {
208 | // Filter out these items.
209 | resp[host] = resp[host].filter(function(entry) {
210 | if (entry.tab) {
211 | return entry.author.hostname !== host;
212 | } else {
213 | return entry;
214 | }
215 | });
216 |
217 | return storage.set('log', resp);
218 | });
219 | });
220 | } else if (el.hasClass('subentry')) {
221 | storage.get('settings').then(function(settings) {
222 | storage.get('log').then(function(resp) {
223 | // Filter out these items.
224 | resp[host] = resp[host].filter(function(entry) {
225 | if (entry.tab) {
226 | return entry.tab.url !== url;
227 | } else {
228 | return entry;
229 | }
230 | });
231 |
232 | return storage.set('log', resp);
233 | });
234 | });
235 | }
236 | }
237 | },
238 |
239 | /**
240 | * Render the donations table.
241 | *
242 | * @return
243 | */
244 | renderTable: function() {
245 | var component = this;
246 |
247 | // Render with the data found from the log.
248 | storage.get('settings').then(function(settings) {
249 | storage.get('log').then(function(resp) {
250 | var filteredAndSorted = component
251 | // Convert the log Object to a filterable/sortable Array.
252 | .toArray(resp, settings, true)
253 | // Sort and filter passing along the log component instance as
254 | // context.
255 | .filter(component.filter, component);
256 |
257 | return filteredAndSorted;
258 | }).then(function(entries) {
259 | entries = entries.sort(component.sorter);
260 | var ents = entries;
261 | if (component.hidden) {
262 | var sortedNums = ents.map(function(o) {return Number(o.estimatedAmount);});
263 | for (var i = 0; i < entries.length; i++) {
264 | if (sortedNums.indexOf(Number(entries[i].estimatedAmount)) >= defaults.maxDonationsTableSize) {
265 | entries[i].hidden = true;
266 | }
267 | }
268 |
269 | }
270 | component.data.hidden = component.hidden;
271 | component.data.entries = entries;
272 | // This page hasn't been officially rendered yet.
273 | if (component.__rendered__) {
274 | component.render();
275 | }
276 |
277 | var tableSize = $('.pure-table tbody tr').length;
278 | if (tableSize <=1 ){
279 | $('#text').html("Nobody to pay yet, get browsing!");
280 | }
281 | }).catch(function(ex) {
282 | console.log(ex);
283 | console.log(ex.stack);
284 | });
285 | });
286 |
287 | storage.get('settings').then(function(settings) {
288 | storage.get('log').then(function(resp) {
289 | var filteredAndSorted = component
290 | // Convert the log Object to a filterable/sortable Array.
291 | .toArray(resp, settings, false)
292 | // Sort and filter passing along the log component instance as
293 | // context.
294 | .filter(component.filter, component);
295 |
296 | return filteredAndSorted;
297 | }).then(function(entries) {
298 | entries = entries.sort(component.sorter);
299 | var ents = entries;
300 |
301 | component.data.details = entries;
302 | // This page hasn't been officially rendered yet.
303 | /*
304 | if (component.__rendered__) {
305 | component.render();
306 | }
307 | */
308 |
309 | var tableSize = $('.pure-table tbody tr').length;
310 |
311 | }).catch(function(ex) {
312 | console.log(ex);
313 | console.log(ex.stack);
314 | });
315 | });
316 | },
317 |
318 | serialize: function() {
319 | return {
320 | entries: this.data.entries,
321 | hidden: this.data.hidden,
322 | details: this.data.details
323 | };
324 | },
325 |
326 | filterInput: function(ev) {
327 | var val = ev.target.value.replace(/[^0-9.]/g, '');
328 | this.$(ev.currentTarget).val('$' + val);
329 | },
330 |
331 | formatAndSave: function(ev) {
332 | var val = ev.target.value.replace(/[^0-9.]/g, '');
333 | var currency = '$' + parseFloat(val).toFixed(2);
334 |
335 | this.$(ev.currentTarget).val(currency);
336 |
337 | // Update any payment methods on this element.
338 | var row = $(ev.currentTarget).closest('tr.entry').data();
339 | if (!row) {
340 | row = $(ev.currentTarget).closest('tr.subentry').data();
341 | }
342 |
343 | if (row.dwolla) {
344 |
345 | row.dwolla.update(currency);
346 | }
347 |
348 | if (row.paypal) {
349 | row.paypal.update(currency);
350 | }
351 | },
352 |
353 | /**
354 | * toArray
355 | *
356 | * @param resp
357 | * @return
358 | */
359 | toArray: function(resp, settings, isDetailed) {
360 | var entries = [];
361 | var component = this;
362 |
363 | // Reset the data entries.
364 | if (isDetailed === false) {
365 | this.data.entries = [];
366 | } else {
367 | this.data.details = [];
368 | }
369 | // Resp is an object that is broken down by domain to list of entries
370 | // visited. The most useful way to
371 | Object.keys(resp).forEach(function(key) {
372 | var calculated = resp[key]
373 | // Condense down the page logic.
374 | .reduce(function(memo, current) {
375 | // Used to determine if we're adding for the first time, or updating
376 | // an existing entry.
377 | var isUpdated = false;
378 |
379 | // Check if this url was already added.
380 | memo.forEach(function(entry) {
381 | // make sure it's not the daysVisited
382 | entry.host = key;
383 | if (entry.tab && !entry.paid) {
384 | // If there is already an entry with the same url, update it.
385 | if (isDetailed === true) {
386 | if (entry.author.hostname === current.author.hostname) {
387 | entry.timeSpent += current.timeSpent;
388 | isUpdated = true;
389 | }
390 | } else if (isDetailed === false){
391 | if (entry.tab.url === current.tab.url) {
392 | entry.timeSpent += current.timeSpent;
393 | isUpdated = true;
394 | }
395 | }
396 | }
397 | });
398 |
399 | // If the current entry was not appended to a previous entry, push
400 | // it as a new item, since this is a page not yet tracked.
401 | if (!isUpdated) {
402 | memo.push(current);
403 | }
404 | return memo;
405 | }, [])
406 | // Calculate the estimated amount for each entry.
407 | .map(function(entry) {
408 | return calculate(settings, entry);
409 | })
410 | // Ensure we're only working with estimated amounts greater than `0`.
411 | .filter(function(entry) {
412 | return parseFloat(entry.estimatedAmount) > 0;
413 | });
414 |
415 | // Condense into a single array.
416 | var condensed = calculated.reduce(function(memo, current) {
417 | // Make sure there is author information.
418 | if (component.hasPaymentInfo(current)) {
419 | memo.push(current);
420 | }
421 |
422 | return memo;
423 | }, []);
424 |
425 | entries.push.apply(entries, condensed);
426 | }, this);
427 |
428 | return entries;
429 | },
430 |
431 | hasPaymentInfo: function(entry) {
432 | return (entry.author && entry.author.list && entry.author.list[0] &&
433 | (entry.author.list[0].bitcoin || entry.author.list[0].dwolla ||
434 | entry.author.list[0].paypal || entry.author.list[0].stripe));
435 | },
436 |
437 | filter: function(entry) {
438 | return entry.author && entry.author.list.length && !entry.paid;
439 | },
440 |
441 | afterRender: function() {
442 |
443 | var tableSize = $('.pure-table tbody tr').length;
444 | if (tableSize <= defaults.maxDonationsTableSize) {
445 | $("#moreButton").hide() ;
446 | } else {
447 | $("#moreButton").show();
448 | }
449 |
450 |
451 | this.$('.payment').on('mouseup', function(ev) {
452 | var tr = $(ev.currentTarget).closest('tr').data();
453 |
454 | // Need to synchronously save the current url & host.
455 | window.localStorage.url = tr.url;
456 | window.localStorage.host = tr.host;
457 |
458 | //window.alert('You will now be redirected to the payment site.');
459 | });
460 |
461 | // Inject payment information for each entry.
462 |
463 | var component_1 = this;
464 | storage.get('settings').then(function(settings) {
465 |
466 | component_1.$('tr.entry').each(function() {
467 | var component = this;
468 | var $component = $(component);
469 | // Extract the estimated value.
470 | var amount = $component.find('.amount').val().slice(1);
471 |
472 | // The payment container.
473 | var payment = $component.find('.payment');
474 |
475 | var dwollaToken = $component.attr('data-dwolla');
476 | var paypalToken = $component.attr('data-paypal');
477 |
478 | // Hide the no processors text.
479 | if (dwollaToken || paypalToken) {
480 | payment.empty();
481 | }
482 |
483 | // Only inject if the author has dwolla.
484 | if (dwollaToken) {
485 | $component.data().dwolla = injectDwolla(payment, amount, dwollaToken);
486 |
487 | }
488 |
489 | // Only inject if the author has paypal.
490 | if (paypalToken) {
491 | $component.data().paypal = injectPaypal(payment, amount, paypalToken);
492 | }
493 |
494 | });
495 |
496 | //return storage.set('settings', settings);
497 | }).catch(function(ex) {
498 | console.log(ex);
499 | console.log(ex.stack);
500 | });
501 |
502 | // Enable table sorting.
503 | this.tablesort = new Tablesort(this.$('table')[0], {
504 | descending: true
505 |
506 | });
507 |
508 | }
509 | };
510 |
511 | DonationsPage.prototype.__proto__ = Component.prototype;
512 |
513 | export default DonationsPage;
514 |
--------------------------------------------------------------------------------
/shared/scripts/lib/pages/donations/donations.styl:
--------------------------------------------------------------------------------
1 | .ratio
2 | float right
3 | color #655D4D
4 |
5 | .amount
6 | font-size 16px
7 | padding 8px
8 | font-weight bold
9 | transition background linear .1s
10 | background rgba(255, 249, 184, 0.65)
11 | color #5F5F5F
12 | border 2px solid #E0E0E0
13 | border-radius 5px
14 | box-shadow 4px 1px rgba(224, 224, 224, 0.25)
15 | width 100%
16 | box-sizing border-box
17 |
18 | .pay
19 | width auto
20 | height 32px
21 | margin-right 12px
22 |
23 | p
24 | margin-bottom 0
25 |
26 | p.info
27 | width 100%
28 |
29 | table
30 | //width calc(100% - 56px)
31 | margin-top 14px
32 | word-wrap break-word
33 | table-layout fixed
34 |
35 | th
36 | font-size 80%
37 |
38 | td
39 |
40 | border-top 1px solid #cbcbcb
41 | &.relative
42 | position relative
43 |
44 | &:last-child div
45 | min-width 300px
46 | display inline
47 |
48 | .remove
49 | position: absolute;
50 | top: 30px;
51 | right: -60px;
52 | margin-top: 0px;
53 |
54 | .removeInner
55 | position: absolute;
56 | top: 30px;
57 | right: -60px;
58 | margin-top: 0px;
59 |
60 | tr
61 | &:hover
62 | td
63 | //background-color #FCE6D2 !important
64 | td.clickable
65 | cursor pointer
66 |
67 | // Overrides for payment processors.
68 | .highlighted
69 | td
70 | background-color #FCE6D2 !important
71 | // Dwolla adjustments.
72 | .d-btn
73 | display inline-block
74 | margin-right 5px
75 |
76 | // PayPal adjustments.
77 | form[name=_xclick]
78 | display inline-block
79 | margin-right 5px
80 |
81 | .tooltip
82 | display: inline;
83 |
84 | .tooltip:hover:after
85 | color: #c00;
86 | text-decoration: none;
87 |
88 | tr.hidden
89 | display:none
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/shared/scripts/lib/pages/donations/entry-donation.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/shared/scripts/lib/pages/getting-started/getting-started.html:
--------------------------------------------------------------------------------
1 |
48 |
--------------------------------------------------------------------------------
/shared/scripts/lib/pages/getting-started/getting-started.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Component from '../../component';
4 | import storage from '../../storage';
5 | import { select } from '../../dom';
6 | import { setDefaults } from '../../defaults';
7 | import DonationGoalComponent from '../../components/donation-goal/donation-goal';
8 | import ReminderIntervalComponent from '../../components/reminders/reminder-interval';
9 | import ReminderThreshLocalComponent from '../../components/reminders/reminder-thresh-local';
10 | import ReminderThreshGlobalComponent from '../../components/reminders/reminder-thresh-global';
11 |
12 | function GettingStartedPage() {
13 | Component.prototype.constructor.apply(this, arguments);
14 |
15 | // Ensure that next is correctly bound.
16 | this.next = this.next.bind(this);
17 |
18 | // set the defaults
19 | setDefaults();
20 | }
21 |
22 | GettingStartedPage.prototype = {
23 | template: 'pages/getting-started/getting-started.html',
24 |
25 | events: {
26 | 'click .skip': 'skipConfiguration',
27 | 'click .next': 'next',
28 | 'click .previous': 'previous',
29 | 'click .track' : 'track'
30 | },
31 |
32 | // Determines which direction to go into.
33 | state: 0,
34 |
35 | // Test code to ensure parity between client and background scripts.
36 | skipConfiguration: function(ev) {
37 | ev.stopPropagation();
38 | ev.preventDefault();
39 |
40 | storage.get('settings').then(function(settings) {
41 | settings.showLog = true;
42 | storage.set('settings', settings);
43 |
44 | // Redirect the user to the log page after clicking skip.
45 | location.hash = '#donations';
46 | });
47 | },
48 |
49 | track: function(ev) {
50 | ev.stopPropagation();
51 | storage.get('settings').then(function(settings) {
52 | settings.userAgrees = true;
53 | settings.showLog = true;
54 | storage.set('settings', settings);
55 |
56 | // Redirect the user to the log page after clicking skip.
57 | location.hash = '#donations';
58 | });
59 | ev.preventDefault();
60 | this.move(++this.state);
61 | },
62 |
63 | previous: function(ev) {
64 | ev.preventDefault();
65 | this.move(--this.state);
66 | },
67 |
68 | next: function(ev) {
69 | ev.preventDefault();
70 | this.move(++this.state);
71 | },
72 |
73 | move: function(end) {
74 | var lis = this.$('ol li');
75 |
76 | lis.removeClass('collapse expand');
77 |
78 | // Collapse the previous and expand to the end.
79 | lis.slice(0, end).addClass('collapse');
80 | lis.eq(end).addClass('expand');
81 | },
82 |
83 | afterRender: function() {
84 | new DonationGoalComponent(select('set-donation-goal', this.el)).render();
85 | new ReminderIntervalComponent(select('set-reminder-interval', this.el)).render();
86 | new ReminderThreshLocalComponent(select('set-reminder-thresh-local', this.el)).render();
87 | new ReminderThreshGlobalComponent(select('set-reminder-thresh-global', this.el)).render();
88 |
89 | setTimeout(function() {
90 | select('form', this.el).classList.add('fade');
91 | }.bind(this), 250);
92 | }
93 | };
94 |
95 | GettingStartedPage.prototype.__proto__ = Component.prototype;
96 |
97 | export default GettingStartedPage;
98 |
--------------------------------------------------------------------------------
/shared/scripts/lib/pages/getting-started/getting-started.styl:
--------------------------------------------------------------------------------
1 | &
2 | height 100%
3 | padding-top 0px
4 | text-align center
5 |
6 | .leader
7 | width 100%
8 | font-size 19px
9 | font-style italic
10 | color #757575
11 | padding-bottom 20px
12 |
13 | strong
14 | font-size 16px
15 | color #333
16 | padding-right 18px
17 |
18 | hr.first
19 | padding-top 60px
20 |
21 | h2
22 | font-family 'Sansita One', sans-serif
23 | color #CCC
24 | font-size 200%
25 | font-weight normal
26 |
27 | h3
28 | font-size 24px
29 | font-weight bold
30 | color #848484
31 |
32 | p
33 | display block
34 | width 100%
35 |
36 | form
37 | opacity 0
38 | transition opacity linear 2s
39 | display table
40 | width 100%
41 | height calc(75% - 0.5px)
42 |
43 | &.fade
44 | opacity 1
45 |
46 | .centered
47 | margin auto
48 |
49 | input
50 | text-align center
51 |
52 | input[type=image]
53 | margin-right 20px
54 |
55 | button
56 | cursor pointer
57 | background #FFF
58 | border 1px solid #F5F5F5
59 | outline none
60 | color #CCC
61 | padding 14px
62 | font-weight bold
63 | padding-left 17px
64 | border-radius 30px
65 | margin-right 20px
66 |
67 |
68 | &.notrack
69 | background #661500 !important
70 | color #FFF
71 |
72 | &:hover
73 | background #A64D4D !important
74 |
75 | &.next
76 | background #75C276
77 | color #FFF
78 |
79 | &:hover
80 | background #99DB9A
81 | &.track
82 | background #75C276
83 | color #FFF
84 |
85 | &:hover
86 | background #99DB9A
87 | &:hover
88 | background #FCFCFC
89 |
90 | ol
91 | list-style-type none
92 | counter-reset li-counter -1
93 | position relative
94 | width 100%
95 | //display table-cell
96 | //vertical-align middle
97 | height 100%
98 |
99 | li
100 | position absolute
101 | padding-left 20px
102 | margin-bottom 60px
103 | left 0%
104 | transition all linear .2s
105 | opacity 1
106 | width 100%
107 |
108 | &.collapse
109 | opacity 0
110 | left -50% !important
111 |
112 | // Remove necessity for importants.
113 | &.expand
114 | opacity 1 !important
115 | visibility visible !important
116 | left 0% !important
117 |
118 | &:not(:first-child)
119 | visibility hidden
120 | opacity 0
121 | top auto
122 | left 100%
123 | width 100%
124 |
125 | .logo
126 | width 75px
127 | height 75px
128 | border-radius 70px
129 | border 3px solid #CCC
130 | background-color #FFF
131 | display block
132 | padding 15px
133 | margin 0 auto
134 | margin-top: 35px
135 |
136 | &:before
137 | content ''
138 | background url(../img/logo64.png) no-repeat center
139 | width 75px
140 | height 75px
141 | display block
142 |
143 | div.userAgreement
144 | margin-top: 200px
145 |
--------------------------------------------------------------------------------
/shared/scripts/lib/pages/log/log.html:
--------------------------------------------------------------------------------
1 |
2 |
Visit log
3 |
4 |
5 |
6 |
7 | This page displays a history of all sites you have accessed. Click on a site to expand and view each visit.
8 |
9 |
10 |
entries without payment information
11 |
Clear history
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/shared/scripts/lib/pages/log/log.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Component from '../../component';
4 | import { select, selectAll } from '../../dom';
5 | import storage from '../../storage';
6 | import LogTableComponent from '../../components/log-table/log-table';
7 |
8 | /**
9 | * LogPage
10 | *
11 | * @return
12 | */
13 | function LogPage() {
14 | Component.prototype.constructor.apply(this, arguments);
15 |
16 | // Whenever the storage engine changes, update the log table.
17 | storage.onChange(this.renderTable.bind(this));
18 |
19 | var component = this;
20 |
21 | // Save the log data for future access.
22 | this.data = { entries: [] };
23 |
24 | // Fetch the entry history template that will be used to show detailed log.
25 | this.entryHistory = this.fetch('components/log-table/entry-history.html')
26 | // Convert to a template, once fetched.
27 | .then(function(contents) {
28 | var template = combyne(contents);
29 |
30 | // Register all filters to the template.
31 | [].concat(component.filters).forEach(function(filter) {
32 | template.registerFilter(filter, component[filter]);
33 | });
34 |
35 | return template;
36 | });
37 | }
38 |
39 | LogPage.prototype = {
40 | template: 'pages/log/log.html',
41 | hideNoAuthor: true,
42 | hidePaid: true,
43 |
44 | events: {
45 | 'click .author': 'toggleNoAuthor',
46 | 'click .reset': 'resetLog',
47 | 'click tbody tr': 'toggleEntryHistory',
48 | 'click .entry-history': 'cancelEvent'
49 | },
50 |
51 | // Register these functions as filters.
52 | filters: [
53 | 'formatAccessTime'
54 | ],
55 |
56 | /**
57 | * formatAccessTime
58 | *
59 | * @param val
60 | * @return
61 | */
62 | formatAccessTime: function(val) {
63 | var date = new Date(val.accessTime);
64 | return moment(date).format("h:mmA - ddd, MMM Do, YYYY");
65 | },
66 |
67 | afterRender: function() {
68 | this.table = new LogTableComponent(select('log-table', this.el));
69 | this.renderTable();
70 | },
71 |
72 | handleMissingFavicon: function(ev) {
73 | console.log(ev);
74 | },
75 |
76 | /**
77 | * toggleNoAuthor
78 | *
79 | * @param ev
80 | * @return
81 | */
82 | toggleNoAuthor: function(ev) {
83 | this.hideNoAuthor = !this.hideNoAuthor;
84 | var showOrHide = this.hideNoAuthor ? 'show' : 'hide';
85 |
86 | this.$('.author').removeClass('show hide').addClass(showOrHide);
87 | this.renderTable();
88 | },
89 |
90 | toggleEntryHistory: function(ev) {
91 | var component = this;
92 | var tr = $(ev.currentTarget);
93 |
94 | // Inside a TH.
95 | if (tr.parents('th').length) {
96 | return false;
97 | }
98 |
99 | // Toggle the active class on click.
100 | tr.toggleClass('active');
101 |
102 | // Once the history template has been fetched, add the entry history.
103 | this.entryHistory.then(function(template) {
104 | var index = Number(tr.data('key'));
105 |
106 | // Only render the current entry.
107 | if (tr.is('.active') && component.data.entries[index]) {
108 | tr.after(template.render({ entry: component.data.entries[index] }));
109 | // Enable table sorting.
110 | new Tablesort(component.$('tr.entry-history table')[0], {
111 | descending: true
112 | });
113 | }
114 | else {
115 | tr.next('tr.entry-history').remove();
116 | }
117 | });
118 | },
119 |
120 | /**
121 | * toArray
122 | *
123 | * @param resp
124 | * @return
125 | */
126 | toArray: function(resp) {
127 | return Object.keys(resp).filter(function(key) {
128 | // Ensure at least one entry per host.
129 | return resp[key].length;
130 | }).map(function(key) {
131 | return {
132 | host: key,
133 | entries: resp[key],
134 | favicon: resp[key][0].favicon
135 | };
136 | });
137 | },
138 |
139 | /**
140 | * filter
141 | *
142 | * @param entries
143 | * @return
144 | */
145 | filter: function(entry) {
146 | var authorCount = entry.entries.filter(function(entry) {
147 | return entry.author && entry.author.list.length &&
148 | (entry.author.list[0].bitcoin || entry.author.list[0].dwolla ||
149 | entry.author.list[0].paypal || entry.author.list[0].stripe);
150 | }).length;
151 |
152 | // Attach the number of authors to the entry, now that it's calculated.
153 | entry.authorCount = authorCount;
154 |
155 | // Hide or show entries without any author information.
156 | if (this.hideNoAuthor && !authorCount) {
157 | return false;
158 | }
159 |
160 | // Hide or show entries based on paid status.
161 | if (this.hidePaid && entry.isPaid) {
162 | return false;
163 | }
164 |
165 | return true;
166 | },
167 |
168 | /**
169 | * sort
170 | *
171 | * @param entries
172 | * @return
173 | */
174 | sort: function(a, b) {
175 | a = a ? a.entries : [];
176 | b = b ? b.entries : [];
177 |
178 | var aAccessTime = a && a.length ? a[a.length - 1].accessTime : 0;
179 | var bAccessTime = b && b.length ? b[b.length - 1].accessTime : 0;
180 |
181 | return bAccessTime - aAccessTime;
182 | },
183 |
184 | resetLog: function() {
185 | if (window.confirm('Are you sure you want to wipe out your history?')) {
186 | storage.set('log', {});
187 | }
188 | },
189 |
190 | /**
191 | * renderTable
192 | *
193 | * @return
194 | */
195 | renderTable: function() {
196 | var log = this;
197 |
198 | // Save the log data for future access.
199 | log.data = {
200 | entries: []
201 | };
202 |
203 | // Render with the data found from the log.
204 | storage.get('log').then(function(resp) {
205 | var filteredAndSorted = log
206 | // Convert the log Object to a filterable/sortable Array.
207 | .toArray(resp)
208 | // Sort and filter passing along the log component instance as context.
209 | .filter(log.filter, log)
210 | .sort(log.sort, log);
211 |
212 | return filteredAndSorted;
213 | }).then(function(entries) {
214 | log.data.entries = entries;
215 |
216 | if (log.table) {
217 | log.table.render(log.data);
218 | }
219 | }).catch(function(ex) {
220 | console.log(ex);
221 | console.log(ex.stack);
222 | });
223 | }
224 | };
225 |
226 | LogPage.prototype.__proto__ = Component.prototype;
227 |
228 | export default LogPage;
229 |
--------------------------------------------------------------------------------
/shared/scripts/lib/pages/log/log.styl:
--------------------------------------------------------------------------------
1 | p.info
2 | margin-bottom 15px
3 |
4 | .reset
5 | cursor pointer
6 | background #FFC4C4
7 | -webkit-user-select none
8 | -moz-user-select none
9 |
10 | .author, .paid
11 | background #B2E1FF
12 | cursor pointer
13 | -webkit-user-select none
14 | -moz-user-select none
15 |
16 | &.show:before
17 | content 'Show'
18 |
19 | &.hide
20 | box-shadow 0 0 0 1px rgba(0, 0, 0, 0.15) inset, 0 0 6px rgba(0, 0, 0, 0.2) inset
21 |
22 | &:before
23 | content 'Hide'
24 |
25 | .entry-history
26 | table
27 | table-layout fixed
28 | word-wrap: break-word;
29 |
30 | table > tr > td
31 | background-color #FFF !important
32 | overflow hidden
33 | text-overflow ellipsis
34 |
35 | input.hide-no-author, input.hide-has-author
36 | -webkit-appearance none
37 | -moz-appearance none
38 |
39 | border none
40 | width 37px
41 | height 37px
42 | padding 10px
43 | float left
44 | position relative
45 | cursor pointer
46 | font-size 20px
47 | color #EFEFEF
48 | top -1px
49 |
50 | &.hide-no-author
51 | background-color #FFF7F0
52 | left -4px
53 |
54 | &:checked
55 | border 9px solid #FFF7F0
56 |
57 | &.hide-has-author
58 | background-color #F0FFF1
59 | left -6px
60 |
61 | &:checked
62 | border 9px solid #F0FFF1
63 |
64 | &:checked
65 | border 9px solid #FFF7F0
66 | background-color #EFEFEF
67 |
68 | &:after
69 | top -13px
70 | bottom 0
71 | left 0
72 | right 0
73 | text-align center
74 |
75 |
76 | input[type=search]
77 | padding 10px
78 | border 1px solid #EFEFEF
79 | width calc(100% - 100px)
80 | float right
81 |
--------------------------------------------------------------------------------
/shared/scripts/lib/pages/settings/settings.html:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/shared/scripts/lib/pages/settings/settings.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Component from '../../component';
4 | import { select, selectAll } from '../../dom';
5 | import DonationGoalComponent from '../../components/donation-goal/donation-goal';
6 | import ReminderIntervalComponent from '../../components/reminders/reminder-interval';
7 | import ReminderThreshGlobalComponent from '../../components/reminders/reminder-thresh-global';
8 | import ReminderThreshLocalComponent from '../../components/reminders/reminder-thresh-local';
9 | import UserAgreementComponent from '../../components/user-agreement/user-agreement';
10 |
11 |
12 | function SettingsPage() {
13 | Component.prototype.constructor.apply(this, arguments);
14 | }
15 |
16 | SettingsPage.prototype = {
17 | template: 'pages/settings/settings.html',
18 |
19 | events: {
20 | 'submit form': 'cancelForm'
21 | },
22 |
23 | cancelForm: function(ev) {
24 | ev.preventDefault();
25 | },
26 |
27 | afterRender: function() {
28 | new DonationGoalComponent(select('set-donation-goal', this.el)).render();
29 | new ReminderIntervalComponent(select('set-reminder-interval', this.el)).render();
30 | new ReminderThreshGlobalComponent(select('set-reminder-thresh-global', this.el)).render();
31 | new ReminderThreshLocalComponent(select('set-reminder-thresh-local', this.el)).render();
32 | new UserAgreementComponent(select('set-user-agreement', this.el)).render();
33 | }
34 | };
35 |
36 | SettingsPage.prototype.__proto__ = Component.prototype;
37 |
38 | export default SettingsPage;
39 |
--------------------------------------------------------------------------------
/shared/scripts/lib/pages/settings/settings.styl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haystack/tipsy/19cc86cc216dd3fb39f085d95317e9f0532d37b7/shared/scripts/lib/pages/settings/settings.styl
--------------------------------------------------------------------------------
/shared/scripts/lib/processors/dwolla.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export function inject($el, amount, token) {
4 | var root = 'http://tipsy.csail.mit.edu/redirect';
5 | var base = location.href.split('#')[0];
6 | base = base.replace(/\//g, '$');
7 | base = encodeURIComponent(base);
8 |
9 | var redirect = [root, base, 'Dwolla user', amount].join('/');
10 | var src = 'https://www.dwolla.com/scripts/button.min.js';
11 |
12 | // If we're in testing, swap the token with our test account.
13 | if (localStorage.testing === 'true') {
14 | token = 'lYNGTjRZRQAU4j32+qB4fBAPNDTQQGeZHF9cZFrH+83qm21sTL';
15 | src = base + '/../../vendor/dwolla.js';
16 | }
17 |
18 | var script = $('