├── .babelrc
├── .editorconfig
├── .gitignore
├── .travis.yml
├── .vscode
├── launch.json
└── settings.json
├── Dockerfile
├── Gulpfile.js
├── License.md
├── Readme.md
├── dev_config.example.json
├── docker-compose.yml
├── package.json
├── src
├── css
│ └── jquery.oembed.css
├── jade
│ ├── templates
│ │ ├── body.jade
│ │ ├── head.jade
│ │ ├── includes
│ │ │ ├── bareMessage.jade
│ │ │ ├── call.jade
│ │ │ ├── contactListItem.jade
│ │ │ ├── contactListItemResource.jade
│ │ │ ├── contactRequest.jade
│ │ │ ├── dayDivider.jade
│ │ │ ├── embeds.jade
│ │ │ ├── message.jade
│ │ │ ├── messageGroup.jade
│ │ │ ├── mucBareMessage.jade
│ │ │ ├── mucListItem.jade
│ │ │ ├── mucRosterItem.jade
│ │ │ ├── mucWrappedMessage.jade
│ │ │ └── wrappedMessage.jade
│ │ ├── misc
│ │ │ └── growlMessage.jade
│ │ └── pages
│ │ │ ├── chat.jade
│ │ │ ├── groupchat.jade
│ │ │ ├── settings.jade
│ │ │ └── signin.jade
│ └── views
│ │ ├── error.jade
│ │ ├── index.jade
│ │ ├── layout.jade
│ │ ├── login.jade
│ │ └── logout.jade
├── js
│ ├── app.ts
│ ├── helpers
│ │ ├── cache.js
│ │ ├── desktop.js
│ │ ├── embedIt.js
│ │ ├── fetchAvatar.js
│ │ ├── getOrCall.js
│ │ ├── htmlify.js
│ │ ├── pushNotifications.js
│ │ └── xmppEventHandlers.js
│ ├── libraries
│ │ ├── jquery.oembed.js
│ │ └── resampler.js
│ ├── models
│ │ ├── baseCollection.js
│ │ ├── call.js
│ │ ├── calls.js
│ │ ├── contact.js
│ │ ├── contactRequest.js
│ │ ├── contactRequests.js
│ │ ├── contacts.js
│ │ ├── me.js
│ │ ├── message.js
│ │ ├── messages.js
│ │ ├── muc.js
│ │ ├── mucs.js
│ │ ├── resource.js
│ │ ├── resources.js
│ │ └── state.js
│ ├── pages
│ │ ├── base.js
│ │ ├── chat.js
│ │ ├── groupchat.js
│ │ └── settings.js
│ ├── router.js
│ ├── storage
│ │ ├── archive.js
│ │ ├── avatars.js
│ │ ├── disco.js
│ │ ├── index.js
│ │ ├── profile.js
│ │ ├── roster.js
│ │ └── rosterver.js
│ └── views
│ │ ├── call.js
│ │ ├── contactListItem.js
│ │ ├── contactRequest.js
│ │ ├── main.js
│ │ ├── message.js
│ │ ├── mucListItem.js
│ │ ├── mucMessage.js
│ │ └── mucRosterItem.js
├── manifest
│ └── manifest.cache
├── resources
│ ├── images
│ │ ├── kaiwa.png
│ │ ├── logo-big.png
│ │ └── logo.png
│ ├── js
│ │ ├── login.js
│ │ └── logout.js
│ ├── manifest.webapp
│ └── sounds
│ │ ├── ding.wav
│ │ └── threetone-alert.wav
├── server.js
└── stylus
│ ├── _mixins.styl
│ ├── _normalize.styl
│ ├── _variables.styl
│ ├── client.styl
│ ├── components
│ ├── buttons.styl
│ ├── forms.styl
│ └── layout.styl
│ └── pages
│ ├── aucs.styl
│ ├── callbar.styl
│ ├── chat.styl
│ ├── header.styl
│ ├── login.styl
│ ├── me.styl
│ ├── roster.styl
│ ├── settings.styl
│ └── updateBar.styl
├── tsconfig.json
├── tsd.json
├── typings
├── async
│ └── async.d.ts
├── jquery
│ └── jquery.d.ts
├── lodash
│ └── lodash.d.ts
├── node
│ └── node.d.ts
└── tsd.d.ts
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-0", "stage-1"],
3 | "plugins": [
4 | "transform-decorators",
5 | "syntax-decorators",
6 | "transform-runtime",
7 | "transform-es2015-block-scoping"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 4
7 | insert_final_newline = true
8 |
9 | [*.json]
10 | indent_size = 2
11 |
12 | [*.styl]
13 | indent_size = 2
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 |
3 | /build/
4 | /src/js/templates.js
5 | /public/
6 | /dev_config.json
7 |
8 | *.log
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "8.11"
4 | sudo: false
5 | script:
6 | - cp dev_config.example.json dev_config.json
7 | - npm run compile
8 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.1.0",
3 | // List of configurations. Add new configurations or edit existing ones.
4 | // ONLY "node" and "mono" are supported, change "type" to switch.
5 | "configurations": [
6 | {
7 | // Name of configuration; appears in the launch configuration drop down menu.
8 | "name": "Launch src/server.js",
9 | // Type of configuration. Possible values: "node", "mono".
10 | "type": "node",
11 | // Workspace relative or absolute path to the program.
12 | "program": "src/server.js",
13 | // Automatically stop program after launch.
14 | "stopOnEntry": false,
15 | // Command line arguments passed to the program.
16 | "args": [],
17 | // Workspace relative or absolute path to the working directory of the program being debugged. Default is the current workspace.
18 | "cwd": ".",
19 | // Workspace relative or absolute path to the runtime executable to be used. Default is the runtime executable on the PATH.
20 | "runtimeExecutable": null,
21 | // Optional arguments passed to the runtime executable.
22 | "runtimeArgs": ["--nolazy"],
23 | // Environment variables passed to the program.
24 | "env": {
25 | "NODE_ENV": "development"
26 | },
27 | // Use JavaScript source maps (if they exist).
28 | "sourceMaps": false,
29 | // If JavaScript source maps are enabled, the generated code is expected in this directory.
30 | "outDir": null
31 | },
32 | {
33 | // Name of configuration; appears in the launch configuration drop down menu.
34 | "name": "Launch build",
35 | // Type of configuration. Possible values: "node", "mono".
36 | "type": "node",
37 | // Workspace relative or absolute path to the program.
38 | "program": "./node_modules/gulp/bin/gulp.js",
39 | // Automatically stop program after launch.
40 | "stopOnEntry": false,
41 | // Command line arguments passed to the program.
42 | "args": ["client"],
43 | // Workspace relative or absolute path to the working directory of the program being debugged. Default is the current workspace.
44 | "cwd": ".",
45 | // Workspace relative or absolute path to the runtime executable to be used. Default is the runtime executable on the PATH.
46 | "runtimeExecutable": "",
47 | // Optional arguments passed to the runtime executable.
48 | "runtimeArgs": ["--nolazy"],
49 | // Environment variables passed to the program.
50 | "env": {
51 | "NODE_ENV": "development"
52 | },
53 | // Use JavaScript source maps (if they exist).
54 | "sourceMaps": false,
55 | // If JavaScript source maps are enabled, the generated code is expected in this directory.
56 | "outDir": null
57 | },
58 | {
59 | "name": "Attach",
60 | "type": "node",
61 | // TCP/IP address. Default is "localhost".
62 | "address": "localhost",
63 | // Port to attach to.
64 | "port": 5858,
65 | "sourceMaps": false
66 | }
67 | ]
68 | }
69 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "files.exclude": {
4 | "**/.git": true,
5 | "**/.DS_Store": true,
6 | "**/node_modules": true,
7 | "/public": true,
8 | "/build": true,
9 | "/typings": true,
10 | "/src/js/templates.js": true
11 | },
12 | "search.exclude": {
13 | "**/node_modules": true,
14 | "**/bower_components": true,
15 | "/public": true,
16 | "/build": true,
17 | "/typings": true,
18 | "/src/js/templates.js": true
19 | }
20 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:9
2 |
3 | # Create app directory
4 | WORKDIR /usr/src/app
5 |
6 | # Install app dependencies
7 | COPY package*.json ./
8 |
9 | ENV NODE_ENV development
10 | RUN npm install
11 |
12 | COPY dev_config.json ./
13 |
14 | COPY . .
15 |
16 | RUN npm run compile
17 |
18 | EXPOSE 8000
19 | CMD [ "npm", "start" ]
20 |
--------------------------------------------------------------------------------
/Gulpfile.js:
--------------------------------------------------------------------------------
1 | var batch = require('gulp-batch');
2 | var browserify = require('browserify');
3 | var buffer = require('vinyl-buffer');
4 | var concat = require('gulp-concat');
5 | var concatCss = require('gulp-concat-css');
6 | var fs = require('fs');
7 | var gulp = require('gulp');
8 | var jade = require('gulp-jade');
9 | var merge = require('merge-stream');
10 | var mkdirp = require('mkdirp');
11 | var source = require('vinyl-source-stream');
12 | var stylus = require('gulp-stylus');
13 | var templatizer = require('templatizer');
14 | var watch = require('gulp-watch');
15 | var gitrev = require('git-rev');
16 | var webpack = require("webpack-stream");
17 | var gutil = require("gulp-util");
18 |
19 | function getConfig() {
20 | var config = fs.readFileSync('./dev_config.json');
21 | return JSON.parse(config);
22 | }
23 |
24 | gulp.task('compile', ['resources', 'client', 'config', 'manifest']);
25 |
26 | gulp.task('watch', function () {
27 | watch([
28 | './src/**',
29 | '!./src/js/templates.js',
30 | './dev_config.json'
31 | ], batch(function (events, done) {
32 | console.log('==> Recompiling Kaiwa');
33 | gulp.start('compile', done);
34 | }));
35 | });
36 |
37 | gulp.task('resources', function () {
38 | return gulp.src('./src/resources/**')
39 | .pipe(gulp.dest('./public'));
40 | });
41 |
42 | gulp.task('client', ['jade-templates', 'jade-views'], function (cb) {
43 | webpack(Object.assign({
44 | plugins: []
45 | }, require('./webpack.config.js')), null, function(err, stats) {
46 | if(err) return cb(JSON.stringify(err));
47 | gutil.log("[webpack]", stats.toString());
48 | return stats;
49 | })
50 | .pipe(gulp.dest('./public/js'))
51 | .on('end', cb);
52 | });
53 |
54 | gulp.task('config', function (cb) {
55 | var config = getConfig();
56 | gitrev.short(function (commit) {
57 | config.server.softwareVersion = {
58 | "name": config.server.name,
59 | "version": commit
60 | }
61 | config.server.baseUrl = config.http.baseUrl
62 | mkdirp('./public', function (error) {
63 | if (error) {
64 | cb(error);
65 | return;
66 | }
67 | fs.writeFile(
68 | './public/config.js',
69 | 'var SERVER_CONFIG = ' + JSON.stringify(config.server) + ';',
70 | cb);
71 | });
72 | })
73 | });
74 |
75 | gulp.task('manifest', function (cb) {
76 | var pkg = require('./package.json');
77 | var config = getConfig();
78 |
79 | fs.readFile('./src/manifest/manifest.cache', 'utf-8', function (error, content) {
80 | if (error) {
81 | cb(error);
82 | return;
83 | }
84 |
85 | mkdirp('./public', function (error) {
86 | if (error) {
87 | cb(error);
88 | return;
89 | }
90 |
91 | var manifest = content.replace(
92 | '#{version}',
93 | pkg.version + config.isDev ? ' ' + Date.now() : '');
94 | fs.writeFile('./public/manifest.cache', manifest, cb);
95 | });
96 | });
97 | });
98 |
99 | gulp.task('jade-templates', function (cb) {
100 | templatizer('./src/jade/templates', './src/js/templates.js', cb);
101 | });
102 |
103 | gulp.task('jade-views', ['css'], function () {
104 | var config = getConfig();
105 | return gulp.src([
106 | './src/jade/views/*',
107 | '!./src/jave/views/layout.jade'
108 | ])
109 | .pipe(jade({
110 | locals: {
111 | config: config
112 | }
113 | }))
114 | .pipe(gulp.dest('./public/'));
115 | });
116 |
117 | gulp.task('css', ['stylus'], function () {
118 | return gulp.src([
119 | './build/css/*.css',
120 | './src/css/*.css'
121 | ])
122 | .pipe(concatCss('app.css'))
123 | .pipe(gulp.dest('./public/css/'));
124 | });
125 |
126 | gulp.task('stylus', function () {
127 | return gulp.src('./src/stylus/client.styl')
128 | .pipe(stylus())
129 | .pipe(gulp.dest('./build/css'));
130 | });
131 |
--------------------------------------------------------------------------------
/License.md:
--------------------------------------------------------------------------------
1 | The MIT License
2 | ===============
3 | Copyright (C) 2015 jabber.ru developers
4 |
5 | Copyright (C) 2015 Digicoop
6 |
7 | Copyright (C) 2013 &yet, LLC
8 |
9 | Permission is hereby granted, free of charge, to any person obtaining a copy of
10 | this software and associated documentation files (the "Software"), to deal in
11 | the Software without restriction, including without limitation the rights to
12 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
13 | the Software, and to permit persons to whom the Software is furnished to do so,
14 | subject to the following conditions:
15 |
16 | The above copyright notice and this permission notice shall be included in all
17 | copies or substantial portions of the Software.
18 |
19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
21 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
22 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
23 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
24 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | Kaiwa [](https://travis-ci.org/ForNeVeR/kaiwa)
2 | =====
3 | Kaiwa is an open source web client for XMPP.
4 |
5 | Alpha version is always hosted on http://kaiwa.fornever.me (**warning: there may
6 | be highly unstable code there, you're recommended to use test accounts with this
7 | server**).
8 |
9 | Kaiwa is a fork of [Otalk][otalk], a prototype application created by &yet.
10 |
11 | 
12 |
13 | ## Installing
14 |
15 | First of all, clone the repository, install the dependencies and configure the
16 | application:
17 |
18 | $ git clone https://github.com/ForNeVeR/kaiwa.git
19 | $ cd kaiwa
20 | $ npm install
21 | $ cp dev_config.example.json dev_config.json # and edit the file
22 |
23 | After that compile the application:
24 |
25 | $ npm run compile
26 |
27 | And start the server:
28 |
29 | $ npm start
30 |
31 | For the development purposes you may use
32 |
33 | $ npm run devel-nix # or devel-win for Windows environment
34 |
35 | It will continously monitor the `src` directory for changes and recompile
36 | application on any change.
37 |
38 | It you want to publish the compiled application somewhere else, feel free to
39 | drop the `public` directory to any web server. You could need to setup MIME
40 | types, please consult `src/server.js` if you need to.
41 |
42 | *Note:* If you're running your own XMPP server, and aren't using something like
43 | HAProxy to terminate SSL, then you might get errors in certain browsers trying
44 | to establish a WebSocket connection because the XMPP server is requesting an
45 | optional client certificate which makes the browser terminate the socket. To
46 | resolve that, visit the XMPP over Websocket URL directly (eg,
47 | `example.com:5281/xmpp-websocket` for Prosody) so that a client cert choice can
48 | be made. After that, the Kaiwa client should connect fine.
49 |
50 | ## Installing using docker
51 |
52 | $ git clone https://github.com/ForNeVeR/kaiwa.git
53 | $ cd kaiwa
54 | $ cp dev_config.example.json dev_config.json # and edit the file
55 | $ docker-compose up
56 |
57 | ## Configuration
58 |
59 | Application configuration is taken from `dev_config.json` file.
60 |
61 | `server.sasl` is optional parameter that can be used to configure the
62 | authentication scheme. It can be a single string or a priority list. The default
63 | priorities as defined by [stanza.io][] are `['external', 'scram-sha-1',
64 | 'digest-md5', 'plain', 'anonymous']`.
65 |
66 | You may enable XMPP pings by setting the `server.keepalive.interval` (time
67 | between ping attempts) and `server.keepalive.timeout` (timeout to close the
68 | connection if pong was not received); both of these are in seconds. If
69 | `server.keepalive` is not defined, then XMPP ping will use the default settings
70 | (with interval of 5 minutes).
71 |
72 | Set `server.securePasswordStorage` to `false` if you want the users to save
73 | their *passwords* in the browser local storage. In secure mode with SCRAM
74 | authentication enabled Kaiwa will try to save only salted data. The secure mode
75 | *will not work* with `digest-md5` authentication.
76 |
77 | ## Troubleshooting
78 |
79 | Feel free to [report any issues][issues] you encounter with the project.
80 |
81 | ## What's included?
82 |
83 | Kaiwa comes with support for:
84 |
85 | ### Message History Syncing
86 |
87 | Using Message Archive Management (MAM, [XEP-0313](http://xmpp.org/extensions/xep-0313.html)), your conversations can be archived by your server and pulled down by the Kaiwa client on demand.
88 |
89 | ### Active Chat Syncing
90 |
91 | Ever used multiple IM clients at once, or swapped clients, and end up with disjointed conversations? Using Message Carbons [(XEP-0280)](http://xmpp.org/extensions/xep-0280.html) all of your active conversations will be synced to your Kaiwa client (and vice versa if you other clients support carbons too).
92 |
93 | ### Reliable Connections
94 |
95 | Sometimes you just lose your Internet connection, but with Stream Mangagement [XEP-0198](http://xmpp.org/extensions/xep-0198.html) your current session can be instantly resumed and caught up once you regain connection. Your messages will show as gray until they've been confirmed as received by your server.
96 |
97 | ### Message Correction
98 |
99 | Made a typo in a message? Using Message Correction [XEP-0308](http://xmpp.org/extensions/xep-0308.html) you can just double tap the up arrow to edit and send a corrected version. In other clients that support correction, your last message will be updated in place and marked as edited.
100 |
101 | ### Timezone Indications
102 |
103 | Working with someone in a different timezone? If the other person is using Kaiwa or another client that supports Entity Time ([XEP-0202](http://xmpp.org/extensions/xep-0202.html)) you'll see a reminder that they're 9 hours away where it's 4am and they're not likely to respond.
104 |
105 | [issues]: https://github.com/ForNeVeR/kaiwa/issues
106 | [otalk]: https://github.com/otalk
107 | [stanza.io]: https://github.com/otalk/stanza.io
108 |
--------------------------------------------------------------------------------
/dev_config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "isDev": true,
3 | "http": {
4 | "baseUrl": "http://localhost:8000",
5 | "port": 8000,
6 | "key": "./fakekeys/privatekey.pem",
7 | "cert": "./fakekeys/certificate.pem"
8 | },
9 | "session": {
10 | "secret": "shhhhhh don't tell anyone ok?"
11 | },
12 | "server": {
13 | "name": "Kaiwa",
14 | "domain": "example.com",
15 | "wss": "wss://example.com:5281/xmpp-websocket/",
16 | "sasl": "scram-sha-1",
17 | "securePasswordStorage": true,
18 | "muc": "chat.example.com",
19 | "startup": "groupchat/room%40chat.example.com",
20 | "admin": "admin",
21 | "keepalive": {
22 | "interval": 45,
23 | "timeout": 15
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 |
3 | services:
4 | web:
5 | build: .
6 | ports:
7 | - 8000:8000
8 | environment:
9 | NODE_ENV: development
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kaiwa.im",
3 | "description": "Kaiwa: XMPP Client in the Browser",
4 | "version": "1.1.0",
5 | "browser": {
6 | "crypto": "crypto-browserify"
7 | },
8 | "scripts": {
9 | "compile": "gulp compile",
10 | "start": "node ./src/server.js",
11 | "devel-nix": "gulp watch & node ./src/server.js",
12 | "devel-win": "powershell -Command \"Start-Process -NoNewWindow gulp watch; Start-Process -NoNewWindow -Wait node ./src/server.js\""
13 | },
14 | "dependencies": {
15 | "@lukekarrys/jade-runtime": "1.11.1",
16 | "andlog": "1.0.0",
17 | "attachmediastream": "1.3.5",
18 | "backbone": "1.0.0",
19 | "bluebird": "^2.3.2",
20 | "body-parser": "1.14.0",
21 | "bows": "1.4.8",
22 | "buffer": "^3.5.5",
23 | "crypto-browserify": "",
24 | "express": "4.13.3",
25 | "getusermedia": "1.3.5",
26 | "git-rev": "^0.2.1",
27 | "human-model": "2.6.2",
28 | "human-view": "1.8.0",
29 | "indexeddbshim": "^2.2.1",
30 | "jade": "1.11.0",
31 | "jquery": "^2.2.2",
32 | "json-loader": "^0.5.4",
33 | "node-uuid": "^1.4.1",
34 | "notify.js": "0.0.3",
35 | "semi-static": "1.0.0",
36 | "serve-static": "1.10.0",
37 | "sound-effect-manager": "1.0.1",
38 | "stanza.io": "^8.0.1",
39 | "staydown": "1.2.4",
40 | "sugar-date": "^1.5.1",
41 | "templatizer": "^2.0.2",
42 | "underscore": "1.8.3",
43 | "wildemitter": "^1.0.1"
44 | },
45 | "license": "MIT",
46 | "main": "src/server.js",
47 | "private": true,
48 | "repository": {
49 | "type": "git",
50 | "url": "git@github.com:ForNeVeR/kaiwa.git"
51 | },
52 | "devDependencies": {
53 | "async": "^1.4.2",
54 | "babel-core": "^6.3.10",
55 | "babel-loader": "^6.2.0",
56 | "babel-plugin-syntax-async-functions": "^6.1.18",
57 | "babel-plugin-syntax-decorators": "^6.1.18",
58 | "babel-plugin-transform-decorators": "^6.0.2",
59 | "babel-plugin-transform-es2015-block-scoping": "^6.3.13",
60 | "babel-plugin-transform-runtime": "^6.3.13",
61 | "babel-polyfill": "^6.3.14",
62 | "babel-preset-es2015": "^6.3.13",
63 | "babel-preset-stage-0": "^6.3.13",
64 | "babel-preset-stage-1": "^6.3.13",
65 | "babel-runtime": "^6.3.13",
66 | "browserify": "^11.1.0",
67 | "exports-loader": "^0.6.2",
68 | "expose-loader": "^0.7.1",
69 | "gulp": "^3.9.0",
70 | "gulp-batch": "^1.0.5",
71 | "gulp-concat": "^2.6.0",
72 | "gulp-concat-css": "^2.2.0",
73 | "gulp-jade": "^1.1.0",
74 | "gulp-minify-css": "^1.2.1",
75 | "gulp-sourcemaps": "^1.6.0",
76 | "gulp-stylus": "^2.0.6",
77 | "gulp-util": "^3.0.7",
78 | "gulp-watch": "^4.3.5",
79 | "imports-loader": "^0.6.5",
80 | "jade-loader": "^0.8.0",
81 | "lodash": "^3.10.1",
82 | "merge-stream": "^1.0.0",
83 | "mkdirp": "^0.5.1",
84 | "ts-loader": "^0.7.2",
85 | "typescript": "^1.7.3",
86 | "vinyl-buffer": "^1.0.0",
87 | "vinyl-source-stream": "^1.1.0",
88 | "webpack": "^1.12.9",
89 | "webpack-stream": "^3.1.0"
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/css/jquery.oembed.css:
--------------------------------------------------------------------------------
1 | div.oembedall-githubrepos {
2 | border: 1px solid #DDD;
3 | border-radius: 4px 4px 4px 4px;
4 | list-style-type: none;
5 | margin: 0 0 10px;
6 | padding: 8px 10px 0;
7 | font: 13.34px/1.4 helvetica,arial,freesans,clean,sans-serif;
8 | /*background: url("http://github.com/images/icons/public.png") no-repeat scroll 6px 9px transparent;*/
9 | width : 452px;
10 | height: 96px;
11 | background-color:#fff;
12 | }
13 |
14 | div.oembedall-githubrepos .oembedall-body {
15 | background: -moz-linear-gradient(center top , #FAFAFA, #EFEFEF) repeat scroll 0 0 transparent;
16 | background: -webkit-gradient(linear,left top,left bottom,from(#FAFAFA),to(#EFEFEF));;
17 | border-bottom-left-radius: 4px;
18 | border-bottom-right-radius: 4px;
19 | border-top: 1px solid #EEE;
20 | margin-left: -10px;
21 | margin-top: 8px;
22 | padding: 5px 10px;
23 | width: 100%;
24 | }
25 |
26 | div.oembedall-githubrepos h3 {
27 | font-size: 14px;
28 | margin: 0;
29 | padding-left: 18px;
30 | white-space: nowrap;
31 | }
32 |
33 | div.oembedall-githubrepos p.oembedall-description {
34 | color: #444;
35 | font-size: 12px;
36 | margin: 0 0 3px;
37 | }
38 |
39 | div.oembedall-githubrepos p.oembedall-updated-at {
40 | color: #888;
41 | font-size: 11px;
42 | margin: 0;
43 | }
44 |
45 | div.oembedall-githubrepos .oembedall-repo-stats {
46 | /*background: url("http://github.com/images/modules/pagehead/actions_fade.png") no-repeat scroll 0 0 transparent;*/
47 | border: medium none;
48 | /*float: right;*/
49 | font-size: 11px;
50 | font-weight: bold;
51 | position: relative;
52 | top: -98px;
53 | left: 345px;
54 | z-index: 5;
55 | margin:0;
56 | width: 100px;
57 | }
58 | div.oembedall-githubrepos .oembedall-repo-stats p {
59 | border: medium none;
60 | color: #666;
61 | background-color: transparent;
62 | list-style-type: none;
63 | margin: 0;
64 | width: 93px;
65 | height: 27px;
66 | padding: 0px 6px;
67 | }
68 | div.oembedall-githubrepos div.oembedall-repo-stats p a {
69 | background-color: transparent;
70 | background-position: 5px -2px;
71 | border: medium none;
72 | color: #666 !important;
73 | background-position: 5px -2px;
74 | background-repeat: no-repeat;
75 | border-left: 1px solid #DDD;
76 | display: inline-block;
77 | height: 21px;
78 | line-height: 21px;
79 | padding: 0 5px 0 23px;
80 | }
81 |
82 | div.oembedall-githubrepos div.oembedall-repo-stats p:first-child a {
83 | border-left: medium none;
84 | margin-right: -3px;
85 | }
86 | div.oembedall-githubrepos div.oembedall-repo-stats p a:hover {
87 | background: none no-repeat scroll 5px -27px #4183C4;
88 | color: #FFFFFF !important;
89 | text-decoration: none;
90 | }
91 | div.oembedall-githubrepos div.oembedall-repo-stats p:first-child a:hover {
92 | border-bottom-left-radius: 3px;
93 | border-top-left-radius: 3px;
94 | }
95 | div.oembedall-repo-stats p:last-child a:hover {
96 | border-bottom-right-radius: 3px;
97 | border-top-right-radius: 3px;
98 | }
99 |
100 | div.oembedall-githubrepos div.oembedall-repo-stats p.oembedall-language {
101 | margin-top: 5px;
102 | }
103 |
104 | div.oembedall-githubrepos div.oembedall-repo-stats p.oembedall-watchers {
105 | margin-top: 4px;
106 | }
107 |
108 | div.oembedall-githubrepos div.oembedall-repo-stats p.oembedall-forks {
109 | margin-top: 4px;
110 | }
111 |
112 | div.oembedall-githubrepos div.oembedall-repo-stats p.oembedall-watchers a {
113 | /*background-image: url("http://github.com/images/modules/pagehead/repostat_watchers.png");*/
114 | padding: 2px 3px;
115 | width: 100%;
116 | }
117 |
118 | div.oembedall-githubrepos div.oembedall-repo-stats p.oembedall-forks a {
119 | /*background-image: url("http://github.com/images/modules/pagehead/repostat_forks.png");*/
120 | padding: 2px 3px;
121 | width: 100%;
122 | }
123 |
124 |
125 | span.oembedall-closehide{
126 | background-color: #aaa;
127 | border-radius: 2px;
128 | cursor: pointer;
129 | margin-right: 3px;
130 | }
131 |
132 | div.oembedall-container {
133 | margin-top : 5px;
134 | text-align: left;
135 | }
136 |
137 | .oembedall-ljuser {
138 | font-weight: bold;
139 | }
140 |
141 | .oembedall-ljuser img {
142 | vertical-align: bottom;
143 | border: 0;
144 | padding-right: 1px;
145 | }
146 |
147 | .oembedall-stoqembed {
148 | border-bottom: 1px dotted #999999;
149 | float: left;
150 | overflow: hidden;
151 | padding: 11px 0;
152 | width: 730px;
153 | line-height: 1;
154 | background: none repeat scroll 0 0 #FFFFFF;
155 | color: #000000;
156 | font-family: Arial,Liberation Sans,DejaVu Sans,sans-serif;
157 | font-size: 80%;
158 | text-align: left;
159 | margin: 0;
160 | padding: 0;
161 | }
162 |
163 | .oembedall-stoqembed a {
164 | color: #0077CC;
165 | text-decoration: none;
166 | margin: 0;
167 | padding: 0;
168 | }
169 | .oembedall-stoqembed a:hover {
170 | text-decoration: underline;
171 | }
172 | .oembedall-stoqembed a:visited {
173 | color: #4A6B82;
174 | }
175 |
176 | .oembedall-stoqembed h3 {
177 | font-family: Trebuchet MS,Liberation Sans,DejaVu Sans,sans-serif;
178 | font-size: 130%;
179 | font-weight: bold;
180 | margin: 0;
181 | padding: 0;
182 | }
183 |
184 | .oembedall-stoqembed .oembedall-reputation-score {
185 | color: #444444;
186 | font-size: 120%;
187 | font-weight: bold;
188 | margin-right: 2px;
189 | }
190 |
191 |
192 | .oembedall-stoqembed .oembedall-user-info {
193 | height: 35px;
194 | width: 185px;
195 | }
196 | .oembedall-stoqembed .oembedall-user-info .oembedall-user-gravatar32 {
197 | float: left;
198 | height: 32px;
199 | width: 32px;
200 | }
201 |
202 | .oembedall-stoqembed .oembedall-user-info .oembedall-user-details {
203 | float: left;
204 | margin-left: 5px;
205 | overflow: hidden;
206 | white-space: nowrap;
207 | width: 145px;
208 | }
209 |
210 | .oembedall-stoqembed .oembedall-question-hyperlink {
211 | font-weight: bold;
212 | }
213 |
214 | .oembedall-stoqembed .oembedall-stats {
215 | background: none repeat scroll 0 0 #EEEEEE;
216 | margin: 0 0 0 7px;
217 | padding: 4px 7px 6px;
218 | width: 58px;
219 | }
220 | .oembedall-stoqembed .oembedall-statscontainer {
221 | float: left;
222 | margin-right: 8px;
223 | width: 86px;
224 | }
225 |
226 | .oembedall-stoqembed .oembedall-votes {
227 | color: #555555;
228 | padding: 0 0 7px;
229 | text-align: center;
230 | }
231 |
232 | .oembedall-stoqembed .oembedall-vote-count-post {
233 | display: block;
234 | font-size: 240%;
235 | color: #808185;
236 | display: block;
237 | font-weight: bold;
238 | }
239 |
240 |
241 | .oembedall-stoqembed .oembedall-views {
242 | color: #999999;
243 | padding-top: 4px;
244 | text-align: center;
245 | }
246 |
247 | .oembedall-stoqembed .oembedall-status {
248 | margin-top: -3px;
249 | padding: 4px 0;
250 | text-align: center;
251 | background: none repeat scroll 0 0 #75845C;
252 | color: #FFFFFF;
253 | }
254 |
255 | .oembedall-stoqembed .oembedall-status strong {
256 | color: #FFFFFF;
257 | display: block;
258 | font-size: 140%;
259 | }
260 |
261 |
262 | .oembedall-stoqembed .oembedall-summary {
263 | float: left;
264 | width: 635px;
265 | }
266 |
267 | .oembedall-stoqembed .oembedall-excerpt {
268 | line-height: 1.2;
269 | margin: 0;
270 | padding: 0 0 5px;
271 | }
272 |
273 | .oembedall-stoqembed .oembedall-tags {
274 | float: left;
275 | line-height: 18px;
276 | }
277 | .oembedall-stoqembed .oembedall-tags a:hover {
278 | text-decoration: none;
279 | }
280 |
281 | .oembedall-stoqembed .oembedall-post-tag {
282 | background-color: #E0EAF1;
283 | border-bottom: 1px solid #3E6D8E;
284 | border-right: 1px solid #7F9FB6;
285 | color: #3E6D8E;
286 | font-size: 90%;
287 | line-height: 2.4;
288 | margin: 2px 2px 2px 0;
289 | padding: 3px 4px;
290 | text-decoration: none;
291 | white-space: nowrap;
292 | }
293 | .oembedall-stoqembed .oembedall-post-tag:hover {
294 | background-color: #3E6D8E;
295 | border-bottom: 1px solid #37607D;
296 | border-right: 1px solid #37607D;
297 | color: #E0EAF1;
298 | }
299 |
300 |
301 | .oembedall-stoqembed .oembedall-fr {
302 | float: right;
303 | }
304 |
305 | .oembedall-stoqembed .oembedall-statsarrow {
306 | background-image: url("http://cdn.sstatic.net/stackoverflow/img/sprites.png?v=3");
307 | background-repeat: no-repeat;
308 | overflow: hidden;
309 | background-position: 0 -435px;
310 | float: right;
311 | height: 13px;
312 | margin-top: 12px;
313 | width: 7px;
314 | }
315 |
316 | .oembedall-facebook1 {
317 | border: #1A3C6C solid 1px;
318 | padding:0px;
319 | font: 13.34px/1.4 verdana;
320 | width : 500px;
321 |
322 | }
323 |
324 | .oembedall-facebook2 {
325 | background-color: #627add;
326 | }
327 | .oembedall-facebook2 a {
328 | color: #e8e8e8;
329 | text-decoration:none;
330 | }
331 |
332 | .oembedall-facebookBody {
333 | background-color: #fff;
334 | vertical-align: top;
335 | padding: 5px;
336 | }
337 |
338 | .oembedall-facebookBody .contents {
339 | display: inline-block;
340 | width: 100%;
341 | }
342 |
343 | .oembedall-facebookBody div img {
344 | float: left;
345 | margin-right: 5px;
346 | }
347 |
348 | div.oembedall-lanyard{
349 | -webkit-box-shadow: none;
350 | -webkit-transition-delay: 0s;
351 | -webkit-transition-duration: 0.4000000059604645s;
352 | -webkit-transition-property: width;
353 | -webkit-transition-timing-function: cubic-bezier(0.42, 0, 0.58, 1);
354 | background-attachment: scroll;
355 | background-clip: border-box;
356 | background-color: transparent;
357 | background-image: none;
358 | background-origin: padding-box;
359 | border-bottom-width: 0px;
360 | border-left-width: 0px;
361 | border-right-width: 0px;
362 | border-top-width: 0px;
363 | box-shadow: none;
364 | color: #112644;
365 | display: block;
366 | float: left;
367 | font-family: 'Trebuchet MS', Trebuchet, sans-serif;
368 | font-size: 16px;
369 | height: 253px;
370 | line-height: 19px;
371 | margin-bottom: 0px;
372 | margin-left: 0px;
373 | margin-right: 0px;
374 | margin-top: 0px;
375 | max-width: none;
376 | min-height: 0px;
377 | outline-color: #112644;
378 | outline-style: none;
379 | outline-width: 0px;
380 | overflow-x: visible;
381 | overflow-y: visible;
382 | padding-bottom: 0px;
383 | padding-left: 0px;
384 | padding-right: 0px;
385 | padding-top: 0px;
386 | position: relative;
387 | text-align: left;
388 | vertical-align: baseline;
389 | width: 804px;
390 | }
391 |
392 | div.oembedall-lanyard .tagline{
393 | font-size: 1.5em;
394 | }
395 |
396 | div.oembedall-lanyard .wrapper{
397 | overflow: hidden;
398 | clear: both;
399 | }
400 | div.oembedall-lanyard .split{
401 | float: left;
402 | display: inline;
403 |
404 | }
405 |
406 | div.oembedall-lanyard .prominent-place .flag:link, div.oembedall-lanyard .prominent-place .flag:visited,div.oembedall-lanyard .prominent-place .flag:hover
407 | ,div.oembedall-lanyard .prominent-place .flag:focus,div.oembedall-lanyard .prominent-place .flag:active {
408 | float: left;
409 | display: block;
410 | width: 48px;
411 | height: 48px;
412 | position: relative;
413 | top: -5px;
414 | margin-right: 10px;
415 | }
416 |
417 | div.oembedall-lanyard .place-context {
418 | font-size: 0.889em;
419 | }
420 |
421 | div.oembedall-lanyard .prominent-place .sub-place {
422 | display: block;
423 | }
424 |
425 | div.oembedall-lanyard .prominent-place{
426 | font-size: 1.125em;
427 | line-height: 1.1em;
428 | font-weight: normal;
429 |
430 | }
431 |
432 | div.oembedall-lanyard .main-date{
433 | color: #8CB4E0;
434 | font-weight: bold;
435 | line-height: 1.1;
436 |
437 | }
438 |
439 | div.oembedall-lanyard .first{
440 | margin-left: 0;
441 | width: 48.57%;
442 | margin: 0 0 0 2.857%;
443 |
444 | }
445 |
--------------------------------------------------------------------------------
/src/jade/templates/body.jade:
--------------------------------------------------------------------------------
1 | body
2 | #updateBar
3 | p Update available!
4 | button.primary.upgrade Upgrade
5 | #wrapper
6 | aside#menu
7 | section#organization
8 | span#orga_name
9 | a(href="/", class="button secondary settings")
10 | svg(id='settingssvg', version='1.1', xmlns='http://www.w3.org/2000/svg', xmlns:xlink='http://www.w3.org/1999/xlink', viewbox="0 0 25 25", height="25", width="25")
11 | g(transform='scale(0.4)')
12 | path(d='M37.418,34.3c-2.1-2.721-2.622-6.352-1.292-9.604c0.452-1.107,1.104-2.1,1.902-2.951 c-0.753-0.877-1.573-1.697-2.507-2.387l-2.609,1.408c-1.05-0.629-2.194-1.112-3.414-1.421l-0.845-2.833 c-0.75-0.112-1.512-0.188-2.287-0.188c-0.783,0-1.54,0.075-2.288,0.188l-0.851,2.833c-1.215,0.309-2.355,0.792-3.41,1.421 l-2.614-1.408c-1.229,0.912-2.318,2-3.228,3.231l1.404,2.612c-0.628,1.053-1.11,2.193-1.419,3.411l-2.832,0.849 c-0.114,0.75-0.187,1.508-0.187,2.287c0,0.778,0.073,1.537,0.187,2.286l2.832,0.848c0.309,1.22,0.791,2.36,1.419,3.413l-1.404,2.61 c0.909,1.231,1.999,2.321,3.228,3.231l2.614-1.406c1.055,0.628,2.195,1.11,3.41,1.42l0.851,2.832 c0.748,0.114,1.505,0.188,2.288,0.188c0.775,0,1.537-0.074,2.287-0.188l0.845-2.832c1.224-0.31,2.364-0.792,3.414-1.42l0.062,0.033 l2.045-3.02L37.418,34.3z M26.367,36.776c-2.777,0-5.027-2.253-5.027-5.027c0-2.775,2.25-5.028,5.027-5.028 c2.774,0,5.024,2.253,5.024,5.028C31.391,34.523,29.141,36.776,26.367,36.776z')
13 | path(d='M51.762,24.505l-1.125-0.459l-1.451,3.55c-0.814,1.993-2.832,3.054-4.505,2.37l-0.355-0.144 c-1.673-0.686-2.37-2.856-1.558-4.849l1.451-3.551l-1.125-0.46c-2.225,0.608-4.153,2.2-5.092,4.501 c-1.225,2.997-0.422,6.312,1.771,8.436l-2.958,6.812l-2.204,3.249l-0.007,2.281l5.275,2.154l1.593-1.633l0.7-3.861l2.901-6.836 c3.049,0.018,5.947-1.785,7.174-4.779C53.186,28.983,52.924,26.499,51.762,24.505z')
14 | div.viewport
15 | section#bookmarks
16 | h1 Rooms
17 | nav
18 | input(type="text", class="inline", placeholder="add a room")#joinmuc
19 | section#roster
20 | h1 Contacts
21 | ul#contactrequests
22 | nav
23 | input(type="text", class="inline", placeholder="add a contact")#addcontact
24 | section#kaiwaNotice
25 | | Powered by
26 | a(href="http://getkaiwa.com", target="_blank")
27 | img(src="images/logo.png", alt="Kaiwa")
28 | header#topbar
29 | #connectionStatus
30 | p
31 | | You're currently
32 | strong disconnected
33 | button.primary.reconnect Reconnect
34 | #me
35 | img.avatar
36 | div
37 | span.name
38 | span.status(contenteditable="true", spellcheck="false")
39 | main#pages
40 |
--------------------------------------------------------------------------------
/src/jade/templates/head.jade:
--------------------------------------------------------------------------------
1 | meta(name="viewport", content="width=device-width,initial-scale=1.0,maximum-scale=1.0")
2 | meta(name="apple-mobile-web-app-capable", content="yes")
3 | link(rel="stylesheet", type="text/css", href="//fonts.googleapis.com/css?family=Lato:400,700")
4 | link(rel="stylesheet", type="text/css", href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css")
5 |
--------------------------------------------------------------------------------
/src/jade/templates/includes/bareMessage.jade:
--------------------------------------------------------------------------------
1 | - var messageClasses = message.classList
2 | if firstEl
3 | - messageClasses += ' first'
4 |
5 | .message(id='chat'+message.cid, class=messageClasses)
6 | .date(title=messageDate.format('{Dow}, {MM}/{dd}/{yyyy} - {h}:{mm} {Tt}')) #{messageDate.format('{h}:{mm} {tt}')}
7 | p.body !{message.processedBody}
8 | - var urls = message.urls
9 | section.embeds
10 | each item in urls
11 | if item.source == 'body'
12 | section.embed.hidden
13 | a.source(href=item.href)= item.desc
14 | else
15 | section.embed
16 | a.source(href=item.href)= item.desc
17 |
--------------------------------------------------------------------------------
/src/jade/templates/includes/call.jade:
--------------------------------------------------------------------------------
1 | .call
2 | img.callerAvatar
3 | h1.caller
4 | span.callerName
5 | span.callerNumber
6 | h2.callTime
7 | .callActions
8 | button.answer Answer
9 | button.ignore Ignore
10 | button.cancel Cancel
11 | button.end End
12 | button.mute Mute
13 | button.unmute Unmute
14 |
--------------------------------------------------------------------------------
/src/jade/templates/includes/contactListItem.jade:
--------------------------------------------------------------------------------
1 | li.contact.joined
2 | .wrap
3 | i.remove.fa.fa-times-circle(title='remove from contacts')
4 | i.presence.fa.fa-circle
5 | .user
6 | img.avatar
7 | span.name=contact.displayName
8 | span.idleTime=contact.idleSince
9 | .unread=contact.unreadCount
10 |
--------------------------------------------------------------------------------
/src/jade/templates/includes/contactListItemResource.jade:
--------------------------------------------------------------------------------
1 | li
2 | p.jid=resource.jid
3 | p.status=resource.status
4 |
--------------------------------------------------------------------------------
/src/jade/templates/includes/contactRequest.jade:
--------------------------------------------------------------------------------
1 | li
2 | .jid
3 | .response
4 | button.primary.small.approve Approve
5 | button.secondary.small.deny Deny
6 |
--------------------------------------------------------------------------------
/src/jade/templates/includes/dayDivider.jade:
--------------------------------------------------------------------------------
1 | li.day_divider
2 | hr
3 | div.day_divider_name #{day_name}
4 |
--------------------------------------------------------------------------------
/src/jade/templates/includes/embeds.jade:
--------------------------------------------------------------------------------
1 | - if (locals.type === 'photo')
2 | section.embed.active
3 | a.photo(href=locals.original, target="_blank")
4 | img.embedded(width=locals.width, height=locals.height, src=locals.url, alt=locals.title)
5 | -if (locals.title || locals.description)
6 | .description
7 | -if (locals.title)
8 | h3= locals.title
9 | -if (locals.description)
10 | p= locals.description
11 | - else if (locals.type === 'video' && locals.thumbnail_url)
12 | section.embed.active
13 | a.preview(href=locals.original, target="_blank")
14 | img.embedded(width=locals.width, height=locals.height, src=locals.thumbnail_url, alt=locals.title)
15 | -if (locals.title || locals.description)
16 | .description
17 | -if (locals.title)
18 | h3= locals.title
19 | -if (locals.description)
20 | p= locals.description
21 | //- else if (locals.type === 'link' && locals.provider_name === 'Twitter')
22 | section.embed.active
23 | a.twitter(href=locals.original, target="_blank")
24 | -if (locals.thumbnail_url)
25 | img.embedded(width=100, src=locals.thumbnail_url, alt=locals.title)
26 | -if (locals.title || locals.description)
27 | .description
28 | h4= '@' + locals.author_name
29 | p= locals.description
30 | //- else if (locals.type === 'link' && locals.description && locals.thumbnail_url)
31 | section.embed.active
32 | a.link(href=locals.original, target="_blank")
33 | -if (locals.thumbnail_url)
34 | img.embedded(width=locals.thumbnail_width, height=locals.thumbnail_height, src=locals.thumbnail_url, alt=locals.title)
35 | -if (locals.title || locals.description)
36 | .description
37 | -if (locals.title)
38 | h3= locals.title
39 | -if (locals.description)
40 | p= locals.description
41 | //- else if (locals.type === 'rich')
42 | section.embed.active
43 | iframe(width=locals.width, height=300, seamless="seamless", frameborder="no", scolling="no", srcdoc=locals.html, sandbox="allow-scripts allow-same-origin allow-forms")
44 |
--------------------------------------------------------------------------------
/src/jade/templates/includes/message.jade:
--------------------------------------------------------------------------------
1 | li
2 | .message
3 | span.timestamp=message.timestamp
4 | p.body=message.body
5 |
--------------------------------------------------------------------------------
/src/jade/templates/includes/messageGroup.jade:
--------------------------------------------------------------------------------
1 | li
2 |
--------------------------------------------------------------------------------
/src/jade/templates/includes/mucBareMessage.jade:
--------------------------------------------------------------------------------
1 | - var messageClasses = message.classList
2 | if firstEl
3 | - messageClasses += ' first'
4 |
5 | .message(id='chat'+message.cid, class=messageClasses)
6 | .date(title=messageDate.format('{Dow}, {MM}/{dd}/{yyyy} - {h}:{mm} {Tt}')) #{messageDate.format('{h}:{mm} {tt}')}
7 | p.body !{message.processedBody}
8 | - var urls = message.urls
9 | section.embeds
10 | each item in urls
11 | if item.source == 'body'
12 | section.embed.hidden
13 | a.source(href=item.href)= item.desc
14 | else
15 | section.embed
16 | a.source(href=item.href)= item.desc
17 |
--------------------------------------------------------------------------------
/src/jade/templates/includes/mucListItem.jade:
--------------------------------------------------------------------------------
1 | li.contact
2 | .wrap
3 | i.remove.fa.fa-times-circle(title='remove from bookmarks')
4 | i.join.fa.fa-sign-in
5 | .unread=contact.unreadCount
6 | span.name=contact.displayName
7 |
--------------------------------------------------------------------------------
/src/jade/templates/includes/mucRosterItem.jade:
--------------------------------------------------------------------------------
1 | li.online
2 | .name
3 |
--------------------------------------------------------------------------------
/src/jade/templates/includes/mucWrappedMessage.jade:
--------------------------------------------------------------------------------
1 | li
2 | .sender
3 | a.messageAvatar(href='#')
4 | img(src=message.sender.getAvatar(message.from.full), alt=message.from.resource, data-placement="below")
5 | .messageWrapper
6 | .message_header
7 | .name #{message.sender.getName(message.from.full)}
8 | .nickname #{message.sender.getNickname(message.from.full)}
9 | .date(title=messageDate.format('{Dow}, {MM}/{dd}/{yyyy} - {h}:{mm} {Tt}')) #{messageDate.format('{h}:{mm} {tt}')}
10 | include mucBareMessage
11 |
--------------------------------------------------------------------------------
/src/jade/templates/includes/wrappedMessage.jade:
--------------------------------------------------------------------------------
1 | li
2 | .sender
3 | a.messageAvatar(href='#')
4 | img(src=message.sender.avatar, alt=message.sender.displayName, data-placement="below")
5 | .messageWrapper
6 | .message_header
7 | .name #{message.sender.displayName}
8 | .date(title=messageDate.format('{Dow}, {MM}/{dd}/{yyyy} - {h}:{mm} {Tt}')) #{messageDate.format('{h}:{mm} {tt}')}
9 | include bareMessage
10 |
--------------------------------------------------------------------------------
/src/jade/templates/misc/growlMessage.jade:
--------------------------------------------------------------------------------
1 | .growlMessage
2 | if icon
3 | img(src= icon, height="30", width="30")
4 | if title
5 | h1= title
6 | if description
7 | p= description
8 |
--------------------------------------------------------------------------------
/src/jade/templates/pages/chat.jade:
--------------------------------------------------------------------------------
1 | section.page.chat
2 | section.conversation
3 | header
4 | .title
5 | span.name
6 | i.user_presence.fa.fa-circle
7 | span.status
8 | .tzo
9 | ul.messages.scroll-container
10 | .activeCall
11 | .container
12 | video.remote(autoplay)
13 | video.local(autoplay, muted)
14 | aside.button-wrap
15 | button.accept.primary Accept
16 | button.end.secondary End
17 | .button-group.outlined
18 | button.mute Mute
19 | button.unmute Unmute
20 | .chatBox
21 | .contactState
22 | form.formwrap
23 | textarea(name='chatInput', type='text', placeholder='Send a message...', autocomplete='off')
24 | button.primary.small.call Call
25 |
--------------------------------------------------------------------------------
/src/jade/templates/pages/groupchat.jade:
--------------------------------------------------------------------------------
1 | section.page.chat
2 | section.group.conversation
3 | header.online
4 | .title
5 | span.name
6 | i.channel_actions.fa.fa-comments-o
7 | span.status(contenteditable="true", spellcheck="false")
8 | ul.messages
9 | a#members_toggle
10 | i.fa.fa-user
11 | span#members_toggle_count
12 | ul.groupRoster
13 | .chatBox
14 | ul.autoComplete
15 | form.formwrap
16 | textarea(name='chatInput', type='text', placeholder='Send a message...', autocomplete='off')
17 |
--------------------------------------------------------------------------------
/src/jade/templates/pages/settings.jade:
--------------------------------------------------------------------------------
1 | section.page.main
2 |
3 | h1#title Settings
4 |
5 | div#avatarChanger
6 | h4 Change Avatar
7 | div.uploadRegion
8 | p Drag and drop a new avatar here
9 | img
10 | form
11 | input#uploader(type="file")
12 |
13 | div
14 | h4 Desktop Integration
15 | button.enableAlerts Enable alerts
16 | button.primary.installFirefox Install app
17 | button.soundNotifs sound notifications
18 |
19 | div
20 | button.disconnect Disconnect
21 | button.primary.logout Logout
22 |
--------------------------------------------------------------------------------
/src/jade/templates/pages/signin.jade:
--------------------------------------------------------------------------------
1 | section.page.signin
2 | div#loginForm
3 | form
4 | label JID:
5 | input(type="text", id="jid", placeholder="you@aweso.me")
6 | label Password:
7 | input(type="password", id="password")
8 | label WebSocket URL:
9 | input(type="text", id="wsURL", placeholder="wss://aweso.me:5281/xmpp-websocket")
10 | input(type="submit", value="Connect", class="button primary")
11 |
--------------------------------------------------------------------------------
/src/jade/views/error.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | section#errorBox.content.box
5 | .head
6 | h2 Oops, something went wrong!
7 |
8 | .content
9 | p #{message}
10 | if stack
11 | pre #{stack}
--------------------------------------------------------------------------------
/src/jade/views/index.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title Kaiwa
5 | link(rel='stylesheet', href='css/app.css')
6 | script(src='js/0-babel-polyfill.js')
7 | script(src='js/1-vendor.js')
8 | script(src='js/app.js')
9 | link(rel="shortcut icon", type="image/png", href="images/kaiwa.png")
10 | script(src="config.js")
11 | body.aux
12 | header
13 | img(id="logo", src="images/logo-big.png", width="250", height="77", alt="Kaiwa")
14 | section.box.connect
15 | h2 Connecting...
16 | a.button.secondary(href="logout.html") Cancel
17 |
--------------------------------------------------------------------------------
/src/jade/views/layout.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title Kaiwa
5 | meta(content='text/html', charset='utf-8')
6 | meta(name="viewport", content="width=device-width, initial-scale=1, user-scalable=no")
7 | link(rel="stylesheet", href="css/app.css")
8 | link(rel="shortcut icon", type="image/png", href="images/kaiwa.png")
9 | block head
10 | body.aux
11 | header
12 | img#logo(src="images/logo-big.png", width="250", height="77", alt="Kaiwa")
13 | block content
14 |
15 | block scripts
16 |
--------------------------------------------------------------------------------
/src/jade/views/login.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | section#loginbox.content.box
5 | .head
6 | h2
7 | | Log in
8 |
9 | section#auth-failed.content.box
10 | h2
11 | | Incorrect username/password pair
12 |
13 | .content
14 | form#login-form
15 | .fieldContainer
16 | label(for='username') Username
17 | input(type='text', id='jid', name='jid', placeholder='you', tabindex='1', autofocus)
18 | .fieldContainer
19 | label(for='password') Password
20 | input(type='password', id='password', name='password', placeholder='•••••••••••••', tabindex='2')
21 | .fieldContainer.fieldContainerWSS
22 | label(for='wsURL') WebSocket or BOSH URL
23 | input(type='text', id='connURL', name='connURL', placeholder='wss://aweso.me:5281/xmpp-websocket', tabindex='3')
24 | .fieldContainer.checkbox(title='Do not remember password')
25 | input(type='checkbox', id='public-computer', tabindex='4')
26 | label(for='public-computer') Public computer
27 |
28 | button(type='submit', tabindex='5', class="primary") Go!
29 |
30 | block scripts
31 | script(src="config.js")
32 | script(src="js/login.js")
33 | script.
34 | if ("#{config.server.wss}".length == 0) {
35 | document.getElementsByClassName('fieldContainerWSS').forEach(function (e) {
36 | e.style.display = 'block';
37 | });
38 | }
39 |
--------------------------------------------------------------------------------
/src/jade/views/logout.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block scripts
4 | script(src="js/logout.js")
5 |
--------------------------------------------------------------------------------
/src/js/app.ts:
--------------------------------------------------------------------------------
1 | ///
2 | 'use strict';
3 |
4 | global.Buffer = global.Buffer || require('buffer').Buffer;
5 |
6 | declare const SERVER_CONFIG: any
7 | declare const client: any
8 | declare const me: any
9 |
10 | interface Storage {}
11 | interface StorageConstructor {
12 | new(): Storage
13 | prototype: Storage
14 | }
15 |
16 | var $: JQueryStatic = require('jquery')
17 | var _: _.LoDashStatic = require('lodash')
18 | var Backbone = require('backbone')
19 |
20 | Backbone.$ = $
21 | const asyncjs: Async = require('async')
22 | var StanzaIO = require('stanza.io')
23 |
24 | var AppState = require('./models/state')
25 | var MeModel = require('./models/me')
26 | const MainView = require('./views/main')
27 | var Router = require('./router')
28 | var Storage: StorageConstructor = require('./storage')
29 | var xmppEventHandlers = require('./helpers/xmppEventHandlers')
30 | var pushNotifications = require('./helpers/pushNotifications')
31 | const Notify = require('notify.js')
32 | var Desktop = require('./helpers/desktop')
33 | var AppCache = require('./helpers/cache')
34 | const url = require('url')
35 |
36 | var SoundEffectManager = require('sound-effect-manager')
37 | let app = null
38 |
39 | class App {
40 | launch() {
41 | function parseConfig(json) {
42 | var config = JSON.parse(json)
43 | var credentials = config.credentials
44 | if (!credentials) return config
45 |
46 | for (var property in credentials) {
47 | if (!credentials.hasOwnProperty(property)) continue
48 |
49 | var value = credentials[property]
50 | if (value.type === 'Buffer') {
51 | credentials[property] = new Buffer(value)
52 | }
53 | }
54 |
55 | return config
56 | }
57 |
58 | var self = window['app'] = this
59 | var config = localStorage['config']
60 |
61 | if (!config) {
62 | console.log('missing config')
63 | window.location = 'login.html'
64 | return
65 | }
66 |
67 | app.config = parseConfig(config)
68 | app.config.useStreamManagement = false // Temporary solution because this feature is bugged on node 4.0
69 |
70 | if (SERVER_CONFIG.sasl) {
71 | app.config.sasl = SERVER_CONFIG.sasl
72 | }
73 |
74 | _.extend(this, Backbone.Events)
75 |
76 | var profile = {}
77 | asyncjs.series([
78 | function (cb) {
79 | app.notifications = new Notify()
80 | app.soundManager = new SoundEffectManager()
81 | app.desktop = new Desktop()
82 | app.cache = new AppCache()
83 | app.storage = new Storage()
84 | app.storage.open(cb)
85 | app.composing = {}
86 | app.timeInterval = 0
87 | app.mucInfos = []
88 | },
89 | function (cb) {
90 | app.storage.profiles.get(app.config.jid, function (err, res) {
91 | if (res) {
92 | profile = res
93 | profile['jid'] = {full: app.config.jid, bare: app.config.jid}
94 | app.config.rosterVer = res.rosterVer
95 | }
96 | cb()
97 | })
98 | },
99 | function (cb) {
100 | app.state = new AppState()
101 | app.me = window['me'] = new MeModel(profile)
102 |
103 | window.onbeforeunload = function () {
104 | if (app.api.sessionStarted) {
105 | app.api.disconnect()
106 | }
107 | }
108 |
109 | self.api = window['client'] = StanzaIO.createClient(app.config)
110 | client.use(pushNotifications)
111 | xmppEventHandlers(self.api, self)
112 |
113 | self.api.once('session:started', function () {
114 | app.state.hasConnected = true
115 | cb()
116 | })
117 | self.api.connect()
118 | },
119 | function (cb) {
120 | app.soundManager.loadFile('sounds/ding.wav', 'ding')
121 | app.soundManager.loadFile('sounds/threetone-alert.wav', 'threetone-alert')
122 | cb()
123 | },
124 | function (cb) {
125 | app.whenConnected(function () {
126 | function getInterval() {
127 | if (client.sessionStarted) {
128 | client.getTime(self.id, function (err, res) {
129 | if (err) return
130 | self.timeInterval = res.time.utc - Date.now()
131 | })
132 | setTimeout(getInterval, 600000)
133 | }
134 | }
135 | getInterval()
136 | })
137 | cb()
138 | },
139 | function (cb) {
140 | app.whenConnected(function () {
141 | me.publishAvatar()
142 | })
143 |
144 | function start() {
145 | // start our router and show the appropriate page
146 | var baseUrl = url.parse(SERVER_CONFIG.baseUrl)
147 | app.history.start({pushState: false, root: baseUrl.pathname})
148 | if (app.history.fragment == '' && SERVER_CONFIG.startup)
149 | app.navigate(SERVER_CONFIG.startup)
150 | cb()
151 | }
152 |
153 | new Router()
154 | app.history = Backbone.history
155 | app.history.on("route", function(route, params) {
156 | app.state.pageChanged = params
157 | })
158 |
159 | self.view = new MainView({
160 | model: app.state,
161 | el: document.body
162 | })
163 | self.view.render()
164 |
165 | if (me.contacts.length) {
166 | start()
167 | } else {
168 | me.contacts.once('loaded', start)
169 | }
170 | }
171 | ])
172 | }
173 | whenConnected(func) {
174 | if (app.api.sessionStarted) {
175 | func()
176 | } else {
177 | app.api.once('session:started', func)
178 | }
179 | }
180 | navigate(page) {
181 | var url = (page.charAt(0) === '/') ? page.slice(1) : page
182 | app.state.markActive()
183 | app.history.navigate(url, true)
184 | }
185 | renderPage(view, animation) {
186 | var container = $('#pages')
187 |
188 | if (app.currentPage) {
189 | app.currentPage.hide(animation)
190 | }
191 | // we call render, but if animation is none, we want to tell the view
192 | // to start with the active class already before appending to DOM.
193 | container.append(view.render(animation === 'none').el)
194 | view.show(animation)
195 | }
196 | serverConfig() {
197 | return SERVER_CONFIG
198 | }
199 | // TODO: add typings
200 | private view: any
201 | private api: any
202 | private id: any
203 | private timeInterval: any
204 | }
205 | app = new App()
206 | module.exports = app
207 |
208 | $(()=> app.launch())
209 |
--------------------------------------------------------------------------------
/src/js/helpers/cache.js:
--------------------------------------------------------------------------------
1 | var WildEmitter = require('wildemitter');
2 | var STATES = [
3 | 'uncached',
4 | 'idle',
5 | 'checking',
6 | 'downloading',
7 | 'updateReady',
8 | 'obsolete'
9 | ];
10 |
11 | function AppCache() {
12 | WildEmitter.call(this);
13 |
14 | var self = this;
15 | this.cache = window.applicationCache;
16 | this.state = STATES[this.cache.status];
17 | this.emit('change', this.state);
18 |
19 | function mapevent(name, altName) {
20 | self.cache.addEventListener(name, function (e) {
21 | var newState = STATES[self.cache.status];
22 | if (newState !== self.state) {
23 | self.state = newState;
24 | self.emit('change', newState);
25 | }
26 | self.emit(altName || name, e);
27 | }, false);
28 | }
29 | mapevent('cached');
30 | mapevent('checking');
31 | mapevent('downloading');
32 | mapevent('error');
33 | mapevent('noupdate', 'noUpdate');
34 | mapevent('obsolete');
35 | mapevent('progress');
36 | mapevent('updateready', 'updateReady');
37 | }
38 |
39 | AppCache.prototype = Object.create(WildEmitter.prototype, {
40 | constructor: {
41 | value: AppCache
42 | }
43 | });
44 |
45 | AppCache.prototype.update = function () {
46 | this.cache.update();
47 | };
48 |
49 |
50 | module.exports = AppCache;
51 |
--------------------------------------------------------------------------------
/src/js/helpers/desktop.js:
--------------------------------------------------------------------------------
1 | var WildEmitter = require('wildemitter');
2 |
3 |
4 | function DesktopApp(opts) {
5 | WildEmitter.call(this);
6 |
7 | var self = this;
8 | opts = opts || {};
9 |
10 | this.mozAppManifest = opts.manifest || window.location.origin + '/manifest.webapp';
11 |
12 | this.installed = !!window.macgap || !!window.fluid;
13 | this.installable = !!window.macgap || !!window.fluid || !!navigator.mozApps;
14 | this.uninstallable = false;
15 |
16 | if (window.macgap || this.fluid) {
17 | this.installed = true;
18 | } else if (navigator.mozApps) {
19 | var req = navigator.mozApps.getSelf();
20 | req.onsuccess = function (e) {
21 | self.mozApp = e.result;
22 | if (e.result) {
23 | self.installed = true;
24 | self.uninstallable = true;
25 | }
26 | };
27 | }
28 |
29 | if (window.macgap) {
30 | document.addEventListener('sleep', function () {
31 | self.emit('sleep');
32 | }, true);
33 |
34 | document.addEventListener('wake', function () {
35 | self.emit('wake');
36 | }, true);
37 | }
38 | }
39 |
40 | DesktopApp.prototype = Object.create(WildEmitter.prototype, {
41 | constructor: {
42 | value: DesktopApp
43 | }
44 | });
45 |
46 | DesktopApp.prototype.isRunning = function () {
47 | return !!window.macgap || !!window.fluid || !!this.mozApp;
48 | };
49 |
50 | DesktopApp.prototype.install = function (cb) {
51 | if (navigator.mozApps) {
52 | var req = navigator.mozApps.install(this.mozAppManifest);
53 | req.onsuccess = function (e) {
54 | cb(null, e);
55 | };
56 | req.onerror = function (e) {
57 | cb(e);
58 | };
59 | }
60 | };
61 |
62 | DesktopApp.prototype.uninstall = function () {
63 | if (this.mozApp) {
64 | return this.mozApp.uninstall();
65 | }
66 | };
67 |
68 | DesktopApp.prototype.updateBadge = function (badge) {
69 | if (window.macgap) {
70 | window.macgap.dock.badge = badge || '';
71 | } else if (window.fluid) {
72 | window.fluid.dockBadge = badge || '';
73 | }
74 | };
75 |
76 |
77 | module.exports = DesktopApp;
78 |
--------------------------------------------------------------------------------
/src/js/helpers/embedIt.js:
--------------------------------------------------------------------------------
1 | /*global $, app*/
2 |
3 | module.exports = function ($html, cb) {
4 | cb = cb || function () {};
5 |
6 | $($html).find("a.source").oembed(null, {
7 | fallback : false,
8 | includeHandle: false,
9 | maxWidth: 500,
10 | maxHeight: 350,
11 | afterEmbed: function(container, oembedData) {
12 | this.parent().parent().parent().show();
13 | },
14 | onProviderNotFound: function() {
15 | var link = $($html).find("a.source");
16 | var resourceURL = link.attr("href");
17 | if (resourceURL.match(/\.(jpg|png|gif)\b/)) {
18 | link.parent().append("");
19 | this.parent().parent().show();
20 | }
21 | }
22 | });
23 | };
24 |
--------------------------------------------------------------------------------
/src/js/helpers/fetchAvatar.js:
--------------------------------------------------------------------------------
1 | /*global app*/
2 | "use strict";
3 | var crypto = require('crypto');
4 |
5 | function fallback(jid) {
6 | var gID = crypto.createHash('md5').update(jid).digest('hex');
7 | return {
8 | uri: 'https://gravatar.com/avatar/' + gID + '?s=80&d=mm'
9 | };
10 | };
11 |
12 | module.exports = function (jid, id, type, source, cb) {
13 | if (!id) {
14 | return cb(fallback(jid));
15 | }
16 |
17 | app.storage.avatars.get(id, function (err, avatar) {
18 | if (!err) {
19 | return cb(avatar);
20 | }
21 |
22 | if (!type) {
23 | return cb(fallback(jid));
24 | }
25 |
26 | app.whenConnected(function () {
27 | if (source == 'vcard') {
28 | app.api.getVCard(jid, function (err, resp) {
29 | if (err) {
30 | return cb(fallback(jid));
31 | }
32 |
33 | if (!resp.vCardTemp.photo) return cb(fallback(jid));
34 |
35 | type = resp.vCardTemp.photo.type || type;
36 |
37 | var data = resp.vCardTemp.photo.data;
38 | var uri = 'data:' + type + ';base64,' + data;
39 |
40 | avatar = {
41 | id: id,
42 | type: type,
43 | uri: uri
44 | };
45 |
46 | app.storage.avatars.add(avatar);
47 | return cb(avatar);
48 | });
49 | } else {
50 | app.api.getAvatar(jid, id, function (err, resp) {
51 | if (err) {
52 | return;
53 | }
54 |
55 | var data = resp.pubsub.retrieve.item.avatarData;
56 | var uri = 'data:' + type + ';base64,' + data;
57 |
58 | avatar = {
59 | id: id,
60 | type: type,
61 | uri: uri
62 | };
63 |
64 | app.storage.avatars.add(avatar);
65 | return cb(avatar);
66 | });
67 | }
68 | });
69 | });
70 | };
71 |
--------------------------------------------------------------------------------
/src/js/helpers/getOrCall.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // get a property that's a function or direct property
4 | module.exports = function (obj, propName) {
5 | if (obj[propName] instanceof Function) {
6 | return obj[propName]();
7 | } else {
8 | return obj[propName] || '';
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/src/js/helpers/htmlify.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _ = require('underscore');
4 |
5 | /*
6 | * JavaScript Linkify - v0.3 - 6/27/2009
7 | * http://benalman.com/projects/javascript-linkify/
8 | *
9 | * Copyright (c) 2009 "Cowboy" Ben Alman
10 | * Dual licensed under the MIT and GPL licenses.
11 | * http://benalman.com/about/license/
12 | *
13 | * Some regexps adapted from http://userscripts.org/scripts/review/7122
14 | */
15 | var parseLinks = (function(){var k="[a-z\\d.-]+://",h="(?:(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])\\.){3}(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])",c="(?:(?:[^\\s!@#$%^&*()_=+[\\]{}\\\\|;:'\",.<>/?]+)\\.)+",n="(?:ac|ad|aero|ae|af|ag|ai|al|am|an|ao|aq|arpa|ar|asia|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|biz|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|cat|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|coop|com|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|edu|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gov|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|info|int|in|io|iq|ir|is|it|je|jm|jobs|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mil|mk|ml|mm|mn|mobi|mo|mp|mq|mr|ms|mt|museum|mu|mv|mw|mx|my|mz|name|na|nc|net|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|org|pa|pe|pf|pg|ph|pk|pl|pm|pn|pro|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tel|tf|tg|th|tj|tk|tl|tm|tn|to|tp|travel|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|xn--0zwm56d|xn--11b5bs3a9aj6g|xn--80akhbyknj4f|xn--9t4b11yi5a|xn--deba0ad|xn--g6w251d|xn--hgbk6aj7f53bba|xn--hlcj6aya9esc7a|xn--jxalpdlp|xn--kgbechtv|xn--zckzah|ye|yt|yu|za|zm|zw)",f="(?:"+c+n+"|"+h+")",o="(?:[;/][^#?<>\\s]*)?",e="(?:\\?[^#<>\\s]*)?(?:#[^<>\\s]*)?",d="\\b"+k+"[^<>\\s]+",a="\\b"+f+o+e+"(?!\\w)",m="mailto:",j="(?:"+m+")?[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@"+f+e+"(?!\\w)",l=new RegExp("(?:"+d+"|"+a+"|"+j+")","ig"),g=new RegExp("^"+k,"i"),b={"'":"`",">":"<",")":"(","]":"[","}":"{","B;":"B+","b:":"b9"},i={callback:function(q,p){return p?''+q+"":q},punct_regexp:/(?:[!?.,:;'"]|(?:&|&)(?:lt|gt|quot|apos|raquo|laquo|rsaquo|lsaquo);)$/};return function(u,z){z=z||{};var w,v,A,p,x="",t=[],s,E,C,y,q,D,B,r;for(v in i){if(z[v]===undefined){z[v]=i[v]}}while(w=l.exec(u)){A=w[0];E=l.lastIndex;C=E-A.length;if(/[\/:]/.test(u.charAt(C-1))){continue}do{y=A;r=A.substr(-1);B=b[r];if(B){q=A.match(new RegExp("\\"+B+"(?!$)","g"));D=A.match(new RegExp("\\"+r,"g"));if((q?q.length:0)<(D?D.length:0)){A=A.substr(0,A.length-1);E--}}if(z.punct_regexp){A=A.replace(z.punct_regexp,function(F){E-=F.length;return""})}}while(A.length&&A!==y);p=A;if(!g.test(p)){p=(p.indexOf("@")!==-1?(!p.indexOf(m)?"":m):!p.indexOf("irc.")?"irc://":!p.indexOf("ftp.")?"ftp://":"http://")+p}if(s!=C){t.push([u.slice(s,C)]);s=E}t.push([A,p])}t.push([u.substr(s)]);for(v=0;v' + text + '' : text;
27 | }
28 | });
29 | },
30 | collectLinks: function (text) {
31 | var links = [];
32 | parseLinks(text, {
33 | callback: function (text, href) {
34 | if (!href) return;
35 | links.push(href);
36 | }
37 | });
38 | return links;
39 | },
40 | escapeHTML: function(s) {
41 | var re = /[&\"'<>]/g, // "
42 | map = {"&": "&", "\"": """, "'": "'", "<": "<", ">": ">"};
43 | return s.replace(re, function(c) { return map[c]; });
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/src/js/helpers/pushNotifications.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = function (client, stanzas) {
4 | var types = stanzas.utils;
5 |
6 | var PushNotification = stanzas.define({
7 | name: 'pushNotification',
8 | namespace: 'urn:xmpp:push:0',
9 | element: 'push',
10 | fields: {
11 | body: types.subText('urn:xmpp:push:0', 'body')
12 | }
13 | });
14 |
15 | stanzas.withMessage(function (Message) {
16 | stanzas.extend(Message, PushNotification);
17 | });
18 |
19 | var RegisterPush = stanzas.define({
20 | name: 'registerPush',
21 | namespace: 'urn:xmpp:push:0',
22 | element: 'register',
23 | fields: {
24 | service: types.text()
25 | }
26 | });
27 |
28 | var UnregisterPush = stanzas.define({
29 | name: 'unregisterPush',
30 | namespace: 'urn:xmpp:push:0',
31 | element: 'unregister',
32 | fields: {
33 | service: types.text()
34 | }
35 | });
36 |
37 | var OtalkRegister = stanzas.define({
38 | name: 'otalkRegister',
39 | namespace: 'http://otalk.im/protocol/push',
40 | element: 'register',
41 | fields: {
42 | deviceID: types.text()
43 | }
44 | });
45 |
46 | stanzas.withIq(function (Iq) {
47 | stanzas.extend(Iq, RegisterPush);
48 | stanzas.extend(Iq, UnregisterPush);
49 | stanzas.extend(Iq, OtalkRegister);
50 | });
51 |
52 | client.registerPushService = function (jid, cb) {
53 | return client.sendIq({
54 | type: 'set',
55 | registerPush: {
56 | service: jid
57 | }
58 | }, cb);
59 | };
60 |
61 | client.getPushServices = function (cb) {
62 | return client.getDiscoItems('', 'urn:xmpp:push', cb);
63 | };
64 |
65 | client.unregisterPushService = function (jid, cb) {
66 | return client.sendIq({
67 | type: 'set',
68 | unregisterPush: {
69 | service: jid
70 | }
71 | }, cb);
72 | };
73 |
74 | client.otalkRegister = function (deviceID, cb) {
75 | return client.sendIq({
76 | type: 'set',
77 | to: 'push@push.otalk.im/prod',
78 | otalkRegister: {
79 | deviceID: deviceID
80 | }
81 | }, cb);
82 | };
83 | };
84 |
--------------------------------------------------------------------------------
/src/js/libraries/resampler.js:
--------------------------------------------------------------------------------
1 | var Resample = (function (canvas) {
2 |
3 | // (C) WebReflection Mit Style License
4 |
5 | // Resample function, accepts an image
6 | // as url, base64 string, or Image/HTMLImgElement
7 | // optional width or height, and a callback
8 | // to invoke on operation complete
9 | function Resample(img, width, height, onresample) {
10 | var
11 | // check the image type
12 | load = typeof img == "string",
13 | // Image pointer
14 | i = load || img
15 | ;
16 | // if string, a new Image is needed
17 | if (load) {
18 | i = new Image;
19 | // with propers callbacks
20 | i.onload = onload;
21 | i.onerror = onerror;
22 | }
23 | // easy/cheap way to store info
24 | i._onresample = onresample;
25 | i._width = width;
26 | i._height = height;
27 | // if string, we trust the onload event
28 | // otherwise we call onload directly
29 | // with the image as callback context
30 | load ? (i.src = img) : onload.call(img);
31 | }
32 |
33 | // just in case something goes wrong
34 | function onerror() {
35 | throw ("not found: " + this.src);
36 | }
37 |
38 | // called when the Image is ready
39 | function onload() {
40 | var
41 | // minifier friendly
42 | img = this,
43 | // the desired width, if any
44 | width = img._width,
45 | // the desired height, if any
46 | height = img._height,
47 | // the callback
48 | onresample = img._onresample
49 | ;
50 | // if width and height are both specified
51 | // the resample uses these pixels
52 | // if width is specified but not the height
53 | // the resample respects proportions
54 | // accordingly with orginal size
55 | // same is if there is a height, but no width
56 | width == null && (width = round(img.width * height / img.height));
57 | height == null && (height = round(img.height * width / img.width));
58 | // remove (hopefully) stored info
59 | delete img._onresample;
60 | delete img._width;
61 | delete img._height;
62 | // when we reassign a canvas size
63 | // this clears automatically
64 | // the size should be exactly the same
65 | // of the final image
66 | // so that toDataURL ctx method
67 | // will return the whole canvas as png
68 | // without empty spaces or lines
69 | canvas.width = width;
70 | canvas.height = height;
71 | // drawImage has different overloads
72 | // in this case we need the following one ...
73 | context.drawImage(
74 | // original image
75 | img,
76 | // starting x point
77 | 0,
78 | // starting y point
79 | 0,
80 | // image width
81 | img.width,
82 | // image height
83 | img.height,
84 | // destination x point
85 | 0,
86 | // destination y point
87 | 0,
88 | // destination width
89 | width,
90 | // destination height
91 | height
92 | );
93 | // retrieve the canvas content as
94 | // base4 encoded PNG image
95 | // and pass the result to the callback
96 | onresample(canvas.toDataURL("image/png"));
97 | }
98 |
99 | var
100 | // point one, use every time ...
101 | context = canvas.getContext("2d"),
102 | // local scope shortcut
103 | round = Math.round
104 | ;
105 |
106 | return Resample;
107 |
108 | }(
109 | // lucky us we don't even need to append
110 | // and render anything on the screen
111 | // let's keep this DOM node in RAM
112 | // for all resizes we want
113 | this.document.createElement("canvas"))
114 | );
115 |
--------------------------------------------------------------------------------
/src/js/models/baseCollection.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // our base collection
4 | var Backbone = require('backbone');
5 |
6 |
7 | module.exports = Backbone.Collection.extend({
8 | // ###next
9 | // returns next item when given an item in the collection
10 | next: function (item, filter, start) {
11 | var i = this.indexOf(item),
12 | newItem;
13 |
14 | if (i === -1) {
15 | i = 0;
16 | } else if (i + 1 >= this.length) {
17 | i = 0;
18 | } else {
19 | i = i + 1;
20 | }
21 | newItem = this.at(i);
22 | if (filter && newItem !== start) {
23 | if (!filter(newItem)) {
24 | return this.next(newItem, filter, start || item);
25 | }
26 | }
27 | return newItem;
28 | },
29 |
30 | // ###prev
31 | // returns previous item when given an item in the collection
32 | prev: function (item, filter, start) {
33 | var i = this.indexOf(item),
34 | newItem;
35 | if (i === -1) {
36 | i = 0;
37 | } else if (i === 0) {
38 | i = this.length - 1;
39 | } else {
40 | i = i - 1;
41 | }
42 | newItem = this.at(i);
43 | if (filter && newItem !== start) {
44 | if (!filter(newItem)) {
45 | return this.prev(newItem, filter, start || item);
46 | }
47 | }
48 | return this.at(i);
49 | }
50 | });
51 |
--------------------------------------------------------------------------------
/src/js/models/call.js:
--------------------------------------------------------------------------------
1 | /*global app, me, client*/
2 | "use strict";
3 |
4 | var _ = require('underscore');
5 | var HumanModel = require('human-model');
6 | var logger = require('andlog');
7 |
8 |
9 | module.exports = HumanModel.define({
10 | type: 'call',
11 | initialize: function (attrs) {
12 | this.contact.onCall = true;
13 | // temporary, this won't stay here
14 | app.navigate('/chat/' + encodeURIComponent(this.contact.jid));
15 | },
16 | session: {
17 | contact: 'object',
18 | jingleSession: 'object',
19 | state: ['string', true, 'inactive'],
20 | multiUser: ['boolean', true, false]
21 | },
22 | end: function (reasonForEnding) {
23 | var reason = reasonForEnding || 'success';
24 | this.contact.onCall = false;
25 | if (this.jingleSession) {
26 | this.jingleSession.end(reasonForEnding);
27 | }
28 | this.collection.remove(this);
29 | }
30 | });
31 |
--------------------------------------------------------------------------------
/src/js/models/calls.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var BaseCollection = require('./baseCollection');
4 | var Call = require('./call');
5 |
6 |
7 | module.exports = BaseCollection.extend({
8 | type: 'calls',
9 | model: Call
10 | });
11 |
--------------------------------------------------------------------------------
/src/js/models/contactRequest.js:
--------------------------------------------------------------------------------
1 | /*global app, me*/
2 | "use strict";
3 |
4 | var HumanModel = require('human-model');
5 |
6 |
7 | module.exports = HumanModel.define({
8 | type: 'contactRequest',
9 | props: {
10 | jid: ['string', true, '']
11 | }
12 | });
13 |
--------------------------------------------------------------------------------
/src/js/models/contactRequests.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var BaseCollection = require('./baseCollection');
4 | var ContactRequest = require('./contactRequest');
5 |
6 |
7 | module.exports = BaseCollection.extend({
8 | type: 'contactRequests',
9 | model: ContactRequest
10 | });
11 |
--------------------------------------------------------------------------------
/src/js/models/contacts.js:
--------------------------------------------------------------------------------
1 | /*global app*/
2 | "use strict";
3 |
4 | var async = require('async');
5 | var BaseCollection = require('./baseCollection');
6 | var Contact = require('./contact');
7 |
8 |
9 | module.exports = BaseCollection.extend({
10 | type: 'contacts',
11 | model: Contact,
12 | comparator: function (model1, model2) {
13 | var show1 = model1.show;
14 | var show2 = model2.show;
15 |
16 | var name1 = model1.displayName.toLowerCase();
17 | var name2 = model2.displayName.toLowerCase();
18 |
19 | if (show1 === show2) {
20 |
21 | if (name1 === name2) {
22 | return 0;
23 | }
24 | if (name1 < name2) {
25 | return -1;
26 | }
27 | return 1;
28 | } else {
29 | if (show1 === 'offline') {
30 | return 1;
31 | }
32 | if (show2 === 'offline') {
33 | return -1;
34 | }
35 |
36 | if (name1 === name2) {
37 | return 0;
38 | }
39 | if (name1 < name2) {
40 | return -1;
41 | }
42 |
43 | return 1;
44 | }
45 | },
46 | initialize: function (model, options) {
47 | this.bind('change', this.sort, this);
48 | }
49 | });
50 |
--------------------------------------------------------------------------------
/src/js/models/me.js:
--------------------------------------------------------------------------------
1 | /*global app, client, URL, me*/
2 | "use strict";
3 |
4 | var HumanModel = require('human-model');
5 | var getUserMedia = require('getusermedia');
6 | var Contacts = require('./contacts');
7 | var Calls = require('./calls');
8 | var Contact = require('./contact');
9 | var MUCs = require('./mucs');
10 | var MUC = require('./muc');
11 | var ContactRequests = require('./contactRequests');
12 | var fetchAvatar = require('../helpers/fetchAvatar');
13 | var crypto = require('crypto');
14 | var StanzaIo = require('stanza.io');
15 |
16 | module.exports = HumanModel.define({
17 | initialize: function (opts) {
18 | this.setAvatar(opts ? opts.avatarID : null);
19 |
20 | this.bind('change:jid', this.load, this);
21 | this.bind('change:hasFocus', function () {
22 | this.setActiveContact(this._activeContact);
23 | }, this);
24 | this.calls.bind('add remove reset', this.updateActiveCalls, this);
25 | this.bind('change:avatarID', this.save, this);
26 | this.bind('change:status', this.save, this);
27 | this.bind('change:rosterVer', this.save, this);
28 | this.bind('change:soundEnabled', this.save, this);
29 | this.contacts.bind('change:unreadCount', this.updateUnreadCount, this);
30 | this.mucs.bind('change:unreadHlCount', this.updateUnreadCount, this);
31 | app.state.bind('change:active', this.updateIdlePresence, this);
32 | app.state.bind('change:deviceIDReady', this.registerDevice, this);
33 | },
34 | props: {
35 | jid: ['object', true],
36 | status: 'string',
37 | avatarID: 'string',
38 | rosterVer: 'string',
39 | nick: 'string'
40 | },
41 | session: {
42 | avatar: 'string',
43 | connected: ['bool', false, false],
44 | shouldAskForAlertsPermission: ['bool', false, false],
45 | hasFocus: ['bool', false, false],
46 | _activeContact: 'string',
47 | stream: 'object',
48 | soundEnabled: ['bool', false, true],
49 | },
50 | collections: {
51 | contacts: Contacts,
52 | contactRequests: ContactRequests,
53 | mucs: MUCs,
54 | calls: Calls
55 | },
56 | derived: {
57 | displayName: {
58 | deps: ['nick', 'jid'],
59 | fn: function () {
60 | return this.nick || this.jid.bare;
61 | }
62 | },
63 | streamUrl: {
64 | deps: ['stream'],
65 | fn: function () {
66 | if (!this.stream) return '';
67 | return URL.createObjectURL(this.stream);
68 | }
69 | },
70 | organization: {
71 | deps: ['orga'],
72 | fn: function () {
73 | return app.serverConfig().name || 'Kaiwa';
74 | }
75 | },
76 | soundEnabledClass: {
77 | deps: ['soundEnabled'],
78 | fn: function () {
79 | return this.soundEnabled ? "primary" : "secondary";
80 | }
81 | },
82 | isAdmin: {
83 | deps: ['jid'],
84 | fn: function () {
85 | return this.jid.local === SERVER_CONFIG.admin ? 'meIsAdmin' : '';
86 | }
87 | }
88 | },
89 | setActiveContact: function (jid) {
90 | var prev = this.getContact(this._activeContact);
91 | if (prev) {
92 | prev.activeContact = false;
93 | }
94 | var curr = this.getContact(jid);
95 | if (curr) {
96 | curr.activeContact = true;
97 | curr.unreadCount = 0;
98 | if ("unreadHlCount" in curr)
99 | curr.unreadHlCount = 0;
100 | this._activeContact = curr.id;
101 | }
102 | },
103 | getName: function () {
104 | return this.displayName;
105 | },
106 | getNickname: function () {
107 | return this.displayName != this.nick ? this.nick : '';
108 | },
109 | getAvatar: function () {
110 | return this.avatar;
111 | },
112 | setAvatar: function (id, type, source) {
113 | var self = this;
114 | fetchAvatar('', id, type, source, function (avatar) {
115 | self.avatarID = avatar.id;
116 | self.avatar = avatar.uri;
117 | });
118 | },
119 | publishAvatar: function (data) {
120 | if (!data) data = this.avatar;
121 | if (!data || data.indexOf('https://') != -1) return;
122 |
123 | var resampler = new Resample(data, 80, 80, function (data) {
124 | var b64Data = data.split(',')[1];
125 | var id = crypto.createHash('sha1').update(atob(b64Data)).digest('hex');
126 | app.storage.avatars.add({id: id, uri: data});
127 | client.publishAvatar(id, b64Data, function (err, res) {
128 | if (err) return;
129 | client.useAvatars([{
130 | id: id,
131 | width: 80,
132 | height: 80,
133 | type: 'image/png',
134 | bytes: b64Data.length
135 | }]);
136 | });
137 | });
138 | },
139 | setSoundNotification: function(enable) {
140 | this.soundEnabled = enable;
141 | },
142 | getContact: function (jid, alt) {
143 | if (typeof jid === 'string') {
144 | if (SERVER_CONFIG.domain && jid.indexOf('@') == -1) jid += '@' + SERVER_CONFIG.domain;
145 | jid = new StanzaIo.JID(jid);
146 | }
147 | if (typeof alt === 'string') alt = new StanzaIo.JID(alt);
148 |
149 | if (this.isMe(jid)) {
150 | jid = alt || jid;
151 | }
152 |
153 | if (!jid) return;
154 |
155 | return this.contacts.get(jid.bare) ||
156 | this.mucs.get(jid.bare) ||
157 | this.calls.findWhere('jid', jid);
158 | },
159 | setContact: function (data, create) {
160 | var contact = this.getContact(data.jid);
161 | data.jid = data.jid.bare;
162 |
163 | if (contact) {
164 | contact.set(data);
165 | contact.save();
166 | } else if (create) {
167 | contact = new Contact(data);
168 | contact.inRoster = true;
169 | contact.owner = this.jid.bare;
170 | contact.save();
171 | this.contacts.add(contact);
172 | }
173 | },
174 | removeContact: function (jid) {
175 | var self = this;
176 | client.removeRosterItem(jid, function(err, res) {
177 | var contact = self.getContact(jid);
178 | self.contacts.remove(contact.jid);
179 | app.storage.roster.remove(contact.storageId);
180 | });
181 | },
182 | load: function () {
183 | if (!this.jid.bare) return;
184 |
185 | var self = this;
186 |
187 | app.storage.profiles.get(this.jid.bare, function (err, profile) {
188 | if (!err) {
189 | self.nick = self.jid.local;
190 | self.status = profile.status;
191 | self.avatarID = profile.avatarID;
192 | self.soundEnabled = profile.soundEnabled;
193 | }
194 | self.save();
195 | app.storage.roster.getAll(self.jid.bare, function (err, contacts) {
196 | if (err) return;
197 |
198 | contacts.forEach(function (contact) {
199 | contact = new Contact(contact);
200 | contact.owner = self.jid.bare;
201 | contact.inRoster = true;
202 | if (contact.jid.indexOf("@" + SERVER_CONFIG.domain) > -1)
203 | contact.persistent = true;
204 | contact.save();
205 | self.contacts.add(contact);
206 | });
207 | });
208 | });
209 |
210 | this.mucs.once('loaded', function () {
211 | self.contacts.trigger('loaded');
212 | });
213 | },
214 | isMe: function (jid) {
215 | return jid && (jid.bare === this.jid.bare);
216 | },
217 | updateJid: function(newJid) {
218 | if (this.jid.domain && this.isMe(newJid)) {
219 | this.jid.full = newJid.full;
220 | this.jid.resource = newJid.resource;
221 | this.jid.unescapedFull = newJid.unescapedFull;
222 | this.jid.prepped = newJid.prepped;
223 | } else {
224 | this.jid = newJid;
225 | this.nick = this.jid.local;
226 | }
227 | },
228 | updateIdlePresence: function () {
229 | var update = {
230 | status: this.status,
231 | show: this.show,
232 | caps: app.api.disco.caps
233 | };
234 |
235 | if (!app.state.active) {
236 | update.idle = {since: app.state.idleSince};
237 | }
238 |
239 | app.api.sendPresence(update);
240 | },
241 | updateUnreadCount: function () {
242 | var sum = function (a, b) {
243 | return a + b;
244 | };
245 |
246 | var pmCount = this.contacts.pluck('unreadCount')
247 | .reduce(sum);
248 | pmCount = pmCount ? pmCount + ' • ' : '';
249 |
250 | var hlCount = this.mucs.pluck('unreadHlCount')
251 | .reduce(sum);
252 | hlCount = hlCount ? 'H' + hlCount + ' • ' : '';
253 |
254 | app.state.badge = pmCount + hlCount;
255 | },
256 | updateActiveCalls: function () {
257 | app.state.hasActiveCall = !!this.calls.length;
258 | },
259 | save: function () {
260 | var data = {
261 | jid: this.jid.bare,
262 | avatarID: this.avatarID,
263 | status: this.status,
264 | rosterVer: this.rosterVer,
265 | soundEnabled: this.soundEnabled
266 | };
267 | app.storage.profiles.set(data);
268 | },
269 | cameraOn: function () {
270 | var self = this;
271 | getUserMedia(function (err, stream) {
272 | if (err) {
273 | console.error(err);
274 | } else {
275 | self.stream = stream;
276 | }
277 | });
278 | },
279 | cameraOff: function () {
280 | if (this.stream) {
281 | this.stream.stop();
282 | this.stream = null;
283 | }
284 | },
285 | registerDevice: function () {
286 | var deviceID = app.state.deviceID;
287 | if (!!deviceID && deviceID !== undefined && deviceID !== 'undefined') {
288 | client.otalkRegister(deviceID).then(function () {
289 | client.registerPush('push@push.otalk.im/prod');
290 | }).catch(function (err) {
291 | console.log('Could not enable push notifications');
292 | });
293 | }
294 | }
295 | });
296 |
--------------------------------------------------------------------------------
/src/js/models/message.js:
--------------------------------------------------------------------------------
1 | /*global app, me*/
2 | "use strict";
3 |
4 | var _ = require('underscore');
5 | var uuid = require('node-uuid');
6 | var HumanModel = require('human-model');
7 | var templates = require('../templates');
8 | var htmlify = require('../helpers/htmlify');
9 |
10 | var ID_CACHE = {};
11 |
12 | var Message = module.exports = HumanModel.define({
13 | initialize: function (attrs) {
14 | this._created = new Date(Date.now() + app.timeInterval);
15 | },
16 | type: 'message',
17 | props: {
18 | mid: 'string',
19 | owner: 'string',
20 | to: 'object',
21 | from: 'object',
22 | body: 'string',
23 | type: ['string', false, 'normal'],
24 | acked: ['bool', false, false],
25 | requestReceipt: ['bool', false, false],
26 | receipt: ['bool', false, false],
27 | archivedId: 'string',
28 | oobURIs: 'array'
29 | },
30 | derived: {
31 | mine: {
32 | deps: ['from', '_mucMine'],
33 | fn: function () {
34 | return this._mucMine || me.isMe(this.from);
35 | }
36 | },
37 | sender: {
38 | deps: ['from', 'mine'],
39 | fn: function () {
40 | if (this.mine) {
41 | return me;
42 | } else {
43 | return me.getContact(this.from);
44 | }
45 | }
46 | },
47 | delayed: {
48 | deps: ['delay'],
49 | fn: function () {
50 | return !!this.delay;
51 | }
52 | },
53 | created: {
54 | deps: ['delay', '_created', '_edited'],
55 | fn: function () {
56 | if (this.delay && this.delay.stamp) {
57 | return this.delay.stamp;
58 | }
59 | return this._created;
60 | }
61 | },
62 | timestamp: {
63 | deps: ['created', '_edited'],
64 | fn: function () {
65 | if (this._edited && !isNaN(this._edited.valueOf())) {
66 | return this._edited;
67 | }
68 | return this.created;
69 | }
70 | },
71 | formattedTime: {
72 | deps: ['created'],
73 | fn: function () {
74 | if (this.created) {
75 | var month = this.created.getMonth() + 1;
76 | var day = this.created.getDate();
77 | var hour = this.created.getHours();
78 | var minutes = this.created.getMinutes();
79 |
80 | var m = (hour >= 12) ? 'p' : 'a';
81 | var strDay = (day < 10) ? '0' + day : day;
82 | var strHour = (hour < 10) ? '0' + hour : hour;
83 | var strMin = (minutes < 10) ? '0' + minutes: minutes;
84 |
85 | return '' + month + '/' + strDay + ' ' + strHour + ':' + strMin + m;
86 | }
87 | return undefined;
88 | }
89 | },
90 | pending: {
91 | deps: ['acked'],
92 | fn: function () {
93 | return !this.acked;
94 | }
95 | },
96 | nick: {
97 | deps: ['mine', 'type'],
98 | fn: function () {
99 | if (this.type === 'groupchat') {
100 | return this.from.resource;
101 | }
102 | if (this.mine) {
103 | return 'me';
104 | }
105 | return me.getContact(this.from.bare).displayName;
106 | }
107 | },
108 | processedBody: {
109 | deps: ['body', 'meAction', 'mentions'],
110 | fn: function () {
111 | var body = this.body;
112 | if (this.meAction) {
113 | body = body.substr(4);
114 | }
115 | body = htmlify.toHTML(body);
116 | for (var i = 0; i < this.mentions.length; i++) {
117 | var existing = htmlify.toHTML(this.mentions[i]);
118 | var parts = body.split(existing);
119 | body = parts.join('' + existing + '');
120 | }
121 | return body;
122 | }
123 | },
124 | partialTemplateHtml: {
125 | deps: ['edited', 'pending', 'body', 'urls'],
126 | cache: false,
127 | fn: function () {
128 | return this.bareMessageTemplate(false);
129 | }
130 | },
131 | templateHtml: {
132 | deps: ['edited', 'pending', 'body', 'urls'],
133 | cache: false,
134 | fn: function () {
135 | if (this.type === 'groupchat') {
136 | return templates.includes.mucWrappedMessage({message: this, messageDate: Date.create(this.timestamp), firstEl: true});
137 | } else {
138 | return templates.includes.wrappedMessage({message: this, messageDate: Date.create(this.timestamp), firstEl: true});
139 | }
140 | }
141 | },
142 | classList: {
143 | cache: false,
144 | fn: function () {
145 | var res = [];
146 |
147 | if (this.mine) res.push('mine');
148 | if (this.pending) res.push('pending');
149 | if (this.delayed) res.push('delayed');
150 | if (this.edited) res.push('edited');
151 | if (this.requestReceipt) res.push('pendingReceipt');
152 | if (this.receiptReceived) res.push('delivered');
153 | if (this.meAction) res.push('meAction');
154 |
155 | return res.join(' ');
156 | }
157 | },
158 | meAction: {
159 | deps: ['body'],
160 | fn: function () {
161 | return this.body.indexOf('/me') === 0;
162 | }
163 | },
164 | urls: {
165 | deps: ['body', 'oobURIs'],
166 | fn: function () {
167 | var self = this;
168 | var result = [];
169 | var urls = htmlify.collectLinks(this.body);
170 | var oobURIs = _.pluck(this.oobURIs || [], 'url');
171 | var uniqueURIs = _.unique(result.concat(urls).concat(oobURIs));
172 |
173 | _.each(uniqueURIs, function (url) {
174 | var oidx = oobURIs.indexOf(url);
175 | if (oidx >= 0) {
176 | result.push({
177 | href: url,
178 | desc: self.oobURIs[oidx].desc,
179 | source: 'oob'
180 | });
181 | } else {
182 | result.push({
183 | href: url,
184 | desc: url,
185 | source: 'body'
186 | });
187 | }
188 | });
189 |
190 | return result;
191 | }
192 | }
193 | },
194 | session: {
195 | _created: 'date',
196 | _edited: 'date',
197 | _mucMine: 'bool',
198 | receiptReceived: ['bool', true, false],
199 | edited: ['bool', true, false],
200 | delay: 'object',
201 | mentions: ['array', false, []]
202 | },
203 | correct: function (msg) {
204 | if (this.from.full !== msg.from.full) return false;
205 |
206 | delete msg.id;
207 |
208 | this.set(msg);
209 | this._edited = new Date(Date.now() + app.timeInterval);
210 | this.edited = true;
211 |
212 | this.save();
213 |
214 | return true;
215 | },
216 | bareMessageTemplate: function (firstEl) {
217 | if (this.type === 'groupchat') {
218 | return templates.includes.mucBareMessage({message: this, messageDate: Date.create(this.timestamp), firstEl: firstEl});
219 | } else {
220 | return templates.includes.bareMessage({message: this, messageDate: Date.create(this.timestamp), firstEl: firstEl});
221 | }
222 | },
223 | save: function () {
224 | if (this.mid) {
225 | var from = this.type == 'groupchat' ? this.from.full : this.from.bare;
226 | Message.idStore(from, this.mid, this);
227 | }
228 |
229 | var data = {
230 | archivedId: this.archivedId || uuid.v4(),
231 | owner: this.owner,
232 | to: this.to,
233 | from: this.from,
234 | created: this.created,
235 | body: this.body,
236 | type: this.type,
237 | delay: this.delay,
238 | edited: this.edited
239 | };
240 | app.storage.archive.add(data);
241 | },
242 | shouldGroupWith: function (previous) {
243 | if (this.type === 'groupchat') {
244 | return previous && previous.from.full === this.from.full && Math.round((this.created - previous.created) / 1000) <= 300 && previous.created.toLocaleDateString() === this.created.toLocaleDateString();
245 | } else {
246 | return previous && previous.from.bare === this.from.bare && Math.round((this.created - previous.created) / 1000) <= 300 && previous.created.toLocaleDateString() === this.created.toLocaleDateString();
247 | }
248 | }
249 | });
250 |
251 | Message.idLookup = function (jid, mid) {
252 | var cache = ID_CACHE[jid] || (ID_CACHE[jid] = {});
253 | return cache[mid];
254 | };
255 |
256 | Message.idStore = function (jid, mid, msg) {
257 | var cache = ID_CACHE[jid] || (ID_CACHE[jid] = {});
258 | cache[mid] = msg;
259 | };
260 |
--------------------------------------------------------------------------------
/src/js/models/messages.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var BaseCollection = require('./baseCollection');
4 | var Message = require('./message');
5 |
6 |
7 | module.exports = BaseCollection.extend({
8 | type: 'messages',
9 | model: Message,
10 | comparator: 'created'
11 | });
12 |
--------------------------------------------------------------------------------
/src/js/models/muc.js:
--------------------------------------------------------------------------------
1 | /*global app, me, client*/
2 | "use strict";
3 |
4 | var _ = require('underscore');
5 | var async = require('async');
6 | var uuid = require('node-uuid');
7 | var htmlify = require('../helpers/htmlify');
8 | var HumanModel = require('human-model');
9 | var Resources = require('./resources');
10 | var Messages = require('./messages');
11 | var Message = require('./message');
12 | var fetchAvatar = require('../helpers/fetchAvatar');
13 |
14 | module.exports = HumanModel.define({
15 | initialize: function (attrs) {
16 | if (attrs.jid) {
17 | this.id = attrs.jid.full;
18 | }
19 | var self = this;
20 | this.resources.bind("add remove reset", function(){
21 | self.membersCount = self.resources.length;
22 | });
23 | },
24 | type: 'muc',
25 | props: {
26 | id: ['string', true],
27 | name: 'string',
28 | autoJoin: ['bool', false, false],
29 | nick: 'string',
30 | jid: 'object'
31 | },
32 | session: {
33 | subject: 'string',
34 | activeContact: ['bool', false, false],
35 | lastInteraction: 'date',
36 | lastSentMessage: 'object',
37 | unreadCount: ['number', false, 0],
38 | unreadHlCount: ['number', false, 0],
39 | persistent: ['bool', false, false],
40 | joined: ['bool', true, false],
41 | membersCount: ['number', false, 0],
42 | },
43 | derived: {
44 | displayName: {
45 | deps: ['name', 'jid'],
46 | fn: function () {
47 | var disp = this.name;
48 | if (!disp) disp = this.jid.bare;
49 | return disp.split('@')[0];
50 | }
51 | },
52 | displayUnreadCount: {
53 | deps: ['unreadCount'],
54 | fn: function () {
55 | if (this.unreadCount > 0) {
56 | if (this.unreadCount < 100)
57 | return this.unreadCount.toString();
58 | else
59 | return '99+'
60 | }
61 | return '';
62 | }
63 | },
64 | displaySubject: {
65 | deps: ['subject'],
66 | fn: function () {
67 | return htmlify.toHTML(this.subject);
68 | }
69 | },
70 | hasUnread: {
71 | deps: ['unreadCount'],
72 | fn: function () {
73 | return this.unreadCount > 0;
74 | }
75 | }
76 | },
77 | collections: {
78 | resources: Resources,
79 | messages: Messages
80 | },
81 | getName: function (jid) {
82 | var nickname = jid.split('/')[1];
83 | var name = nickname;
84 | var xmppContact = me.getContact(nickname);
85 | if (xmppContact) {
86 | name = xmppContact.displayName;
87 | }
88 | return name != '' ? name : nickname;
89 | },
90 | getNickname: function (jid) {
91 | var nickname = jid.split('/')[1];
92 | return nickname != this.getName(jid) ? nickname : '';
93 | },
94 | getAvatar: function (jid) {
95 | var resource = this.resources.get(jid);
96 | if (resource && resource.avatar) {
97 | return resource.avatar;
98 | }
99 | return "https://www.gravatar.com/avatar/00000000000000000000000000000000?s=80&d=mm"
100 | },
101 | addMessage: function (message, notify) {
102 | message.owner = me.jid.bare;
103 |
104 | var self = this;
105 |
106 | var mentions = [];
107 | var toMe = false;
108 | if (message.body.toLowerCase().indexOf(self.nick) >= 0) {
109 | mentions.push(self.nick);
110 | toMe = true;
111 | }
112 | if (message.body.toLowerCase().indexOf('all: ') >= 0) {
113 | mentions.push('all:');
114 | }
115 | message.mentions = mentions;
116 |
117 | var mine = message.from.resource === this.nick;
118 |
119 | if (mine) {
120 | message._mucMine = true;
121 | }
122 |
123 | if (notify && (!this.activeContact || (this.activeContact && !app.state.focused)) && !mine) {
124 | this.unreadCount++;
125 | if (toMe) {
126 | this.unreadHlCount += 1;
127 | app.notifications.create(this.displayName, {
128 | body: message.body,
129 | icon: this.avatar,
130 | tag: this.id,
131 | onclick: _.bind(app.navigate, app, '/groupchat/' + encodeURIComponent(this.jid))
132 | });
133 | if (me.soundEnabled)
134 | app.soundManager.play('threetone-alert');
135 | }
136 | else
137 | {
138 | if (me.soundEnabled)
139 | app.soundManager.play('ding');
140 | }
141 | }
142 |
143 | message.acked = true;
144 |
145 | if (mine) {
146 | this.lastSentMessage = message;
147 | }
148 |
149 | var existing = Message.idLookup(message.from['full'], message.mid);
150 | if (existing) {
151 | existing.set(message);
152 | existing.save();
153 | } else {
154 | this.messages.add(message);
155 | message.save();
156 | }
157 |
158 | var newInteraction = new Date(message.created);
159 | if (!this.lastInteraction || this.lastInteraction < newInteraction) {
160 | this.lastInteraction = newInteraction;
161 | }
162 | },
163 | join: function (manual) {
164 | if (!this.nick) {
165 | this.nick = me.jid.local;
166 | }
167 | this.messages.reset();
168 | this.resources.reset();
169 |
170 | client.joinRoom(this.jid, this.nick, {
171 | joinMuc: {
172 | history: {
173 | maxstanzas: 50
174 | }
175 | }
176 | });
177 |
178 | if (manual) {
179 | var form = {
180 | fields: [
181 | {
182 | type: 'hidden',
183 | name: 'FORM_TYPE',
184 | value: 'http://jabber.org/protocol/muc#roomconfig'
185 | },
186 | {
187 | type: 'boolean',
188 | name: 'muc#roomconfig_changesubject',
189 | value: true
190 | },
191 | {
192 | type: 'boolean',
193 | name: 'muc#roomconfig_persistentroom',
194 | value: true
195 | },
196 | ]
197 | };
198 | client.configureRoom(this.jid, form, function(err, resp) {
199 | if (err) return;
200 | });
201 |
202 | if (SERVER_CONFIG.domain && SERVER_CONFIG.admin) {
203 | var self = this;
204 | client.setRoomAffiliation(this.jid, SERVER_CONFIG.admin + '@' + SERVER_CONFIG.domain, 'owner', 'administration', function(err, resp) {
205 | if (err) return;
206 | client.setRoomAffiliation(self.jid, me.jid, 'none', 'administration');
207 | });
208 | }
209 | }
210 |
211 | var self = this;
212 | // After a reconnection
213 | client.on('muc:join', function (pres) {
214 | if (self.messages.length) {
215 | self.fetchHistory(true);
216 | }
217 | });
218 | },
219 | fetchHistory: function(allInterval) {
220 | var self = this;
221 | app.whenConnected(function () {
222 | var filter = {
223 | 'to': self.jid,
224 | rsm: {
225 | max: 40,
226 | before: !allInterval
227 | }
228 | };
229 |
230 | if (allInterval) {
231 | var lastMessage = self.messages.last();
232 | if (lastMessage && lastMessage.created) {
233 | var start = new Date(lastMessage.created);
234 | filter.start = start.toISOString();
235 | }
236 | } else {
237 | var firstMessage = self.messages.first();
238 | if (firstMessage && firstMessage.created) {
239 | var end = new Date(firstMessage.created);
240 | filter.end = end.toISOString();
241 | }
242 | }
243 |
244 | client.searchHistory(filter, function (err, res) {
245 | if (err) return;
246 |
247 | var results = res.mamResult.items || [];
248 |
249 | results.forEach(function (result) {
250 | var msg = result.forwarded.message;
251 |
252 | msg.mid = msg.id;
253 | delete msg.id;
254 |
255 | if (!msg.delay) {
256 | msg.delay = result.forwarded.delay;
257 | }
258 |
259 | if (msg.replace) {
260 | var original = Message.idLookup(msg.from[msg.type == 'groupchat' ? 'full' : 'bare'], msg.replace);
261 | // Drop the message if editing a previous, but
262 | // keep it if it didn't actually change an
263 | // existing message.
264 | if (original && original.correct(msg)) return;
265 | }
266 |
267 | var message = new Message(msg);
268 | message.archivedId = result.id;
269 | message.acked = true;
270 |
271 | self.addMessage(message, false);
272 | });
273 |
274 | if (allInterval) {
275 | self.trigger('refresh');
276 | if (results.length == 40)
277 | self.fetchHistory(true);
278 | }
279 | });
280 | });
281 | },
282 | leave: function () {
283 | this.resources.reset();
284 | client.leaveRoom(this.jid, this.nick);
285 | }
286 | });
287 |
--------------------------------------------------------------------------------
/src/js/models/mucs.js:
--------------------------------------------------------------------------------
1 | /*global app, client*/
2 | "use strict";
3 |
4 | var async = require('async');
5 | var BaseCollection = require('./baseCollection');
6 | var MUC = require('./muc');
7 |
8 |
9 | module.exports = BaseCollection.extend({
10 | type: 'mucs',
11 | model: MUC,
12 | comparator: function (model1, model2) {
13 | var name1 = model1.displayName.toLowerCase();
14 | var name2 = model2.displayName.toLowerCase();
15 | if (name1 === name2) {
16 | return 0;
17 | }
18 | if (name1 < name2) {
19 | return -1;
20 | }
21 | return 1;
22 | },
23 | initialize: function (model, options) {
24 | this.bind('change', this.sort, this);
25 | },
26 | fetch: function () {
27 | var self = this;
28 | app.whenConnected(function () {
29 | client.getBookmarks(function (err, res) {
30 | if (err) return;
31 |
32 | var mucs = res.privateStorage.bookmarks.conferences || [];
33 | mucs.forEach(function (muc) {
34 | self.add(muc);
35 | if (muc.autoJoin) {
36 | self.get(muc.jid).join();
37 | }
38 | });
39 |
40 | self.trigger('loaded');
41 | });
42 | });
43 | },
44 | save: function (cb) {
45 | var self = this;
46 | app.whenConnected(function () {
47 | var models = [];
48 | self.models.forEach(function (model) {
49 | models.push({
50 | name: model.name,
51 | jid: model.jid,
52 | nick: model.nick,
53 | autoJoin: model.autoJoin
54 | });
55 | });
56 | client.setBookmarks({conferences: models}, cb);
57 | });
58 | }
59 | });
60 |
--------------------------------------------------------------------------------
/src/js/models/resource.js:
--------------------------------------------------------------------------------
1 | /*global app, client*/
2 | "use strict";
3 |
4 | var HumanModel = require('human-model');
5 | var fetchAvatar = require('../helpers/fetchAvatar');
6 |
7 |
8 | module.exports = HumanModel.define({
9 | initialize: function () {},
10 | type: 'resource',
11 | props: {
12 | avatarID: ['string', false, '']
13 | },
14 | session: {
15 | id: ['string', true],
16 | status: 'string',
17 | show: 'string',
18 | priority: ['number', false, 0],
19 | chatState: ['string', false, 'gone'],
20 | idleSince: 'date',
21 | discoInfo: 'object',
22 | timezoneOffset: 'number',
23 | avatar: 'string',
24 | avatarSource: 'string'
25 | },
26 | derived: {
27 | mucDisplayName: {
28 | deps: ['id'],
29 | fn: function () {
30 | return this.id.split('/')[1] || '';
31 | }
32 | },
33 | idle: {
34 | deps: ['idleSince'],
35 | fn: function () {
36 | return this.idleSince && !isNaN(this.idleSince.valueOf());
37 | }
38 | },
39 | supportsReceipts: {
40 | deps: ['discoInfo'],
41 | fn: function () {
42 | if (!this.discoInfo) return false;
43 | var features = this.discoInfo.features || [];
44 | return features.indexOf('urn:xmpp:receipts') >= 0;
45 | }
46 | },
47 | supportsChatStates: {
48 | deps: ['discoInfo'],
49 | fn: function () {
50 | if (!this.discoInfo) return false;
51 | var features = this.discoInfo.features || [];
52 | return features.indexOf('http://jabber.org/protocol/chatstate') >= 0;
53 | }
54 | },
55 | supportsJingleMedia: {
56 | deps: ['discoInfo'],
57 | fn: function () {
58 | if (!this.discoInfo) return false;
59 | var features = this.discoInfo.features || [];
60 | if (features.indexOf('urn:xmpp:jingle:1') === -1) {
61 | return false;
62 | }
63 |
64 | if (features.indexOf('urn:xmpp:jingle:apps:rtp:1') === -1) {
65 | return false;
66 | }
67 |
68 | if (features.indexOf('urn:xmpp:jingle:apps:rtp:audio') === -1) {
69 | return false;
70 | }
71 |
72 | if (features.indexOf('urn:xmpp:jingle:apps:rtp:video') === -1) {
73 | return false;
74 | }
75 |
76 | return true;
77 | }
78 | },
79 | supportsJingleFiletransfer: {
80 | deps: ['discoInfo'],
81 | fn: function () {
82 | if (!this.discoInfo) return false;
83 | var features = this.discoInfo.features || [];
84 | if (features.indexOf('urn:xmpp:jingle:1') === -1) {
85 | return false;
86 | }
87 |
88 | if (features.indexOf('urn:xmpp:jingle:apps:file-transfer:3') === -1) {
89 | return false;
90 | }
91 |
92 | if (features.indexOf('urn:xmpp:jingle:transports:ice-udp:1') === -1) {
93 | return false;
94 | }
95 |
96 | if (features.indexOf('urn:xmpp:jingle:transports:dtls-sctp:1') === -1) {
97 | return false;
98 | }
99 |
100 | return true;
101 | }
102 | }
103 | },
104 | fetchTimezone: function () {
105 | var self = this;
106 |
107 | if (self.timezoneOffset) return;
108 |
109 | app.whenConnected(function () {
110 | client.getTime(self.id, function (err, res) {
111 | if (err) return;
112 | self.timezoneOffset = res.time.tzo;
113 | });
114 | });
115 | },
116 | fetchDisco: function () {
117 | var self = this;
118 |
119 | if (self.discoInfo) return;
120 |
121 | app.whenConnected(function () {
122 | client.getDiscoInfo(self.id, '', function (err, res) {
123 | if (err) return;
124 | self.discoInfo = res.discoInfo;
125 | });
126 | });
127 | },
128 | setAvatar: function (id, type, source) {
129 | var self = this;
130 | fetchAvatar(this.id, id, type, source, function (avatar) {
131 | if (source == 'vcard' && self.avatarSource == 'pubsub') return;
132 | self.avatarID = avatar.id;
133 | self.avatar = avatar.uri;
134 | self.avatarSource = source;
135 | });
136 | },
137 |
138 | });
139 |
--------------------------------------------------------------------------------
/src/js/models/resources.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var BaseCollection = require('./baseCollection');
4 | var Resource = require('./resource');
5 |
6 | module.exports = BaseCollection.extend({
7 | type: 'resources',
8 | model: Resource,
9 | comparator: function (res1, res2) {
10 | var name1 = res1.mucDisplayName.toLowerCase(),
11 | name2 = res2.mucDisplayName.toLowerCase();
12 | return (name1 > name2) ? 1 :
13 | (name1 < name2) ? -1 : 0;
14 | },
15 | search : function (letters, removeMe, addAll) {
16 | if(letters == "" && !removeMe) return this;
17 |
18 | var collection = new module.exports(this.models);
19 | if (addAll)
20 | collection.add({id: this.parent.jid.bare + '/all'});
21 |
22 | var pattern = new RegExp('^' + letters + '.*$', "i");
23 | var filtered = collection.filter(function(data) {
24 | var nick = data.get("mucDisplayName");
25 | if (nick === me.nick) return false;
26 | return pattern.test(nick);
27 | });
28 | return new module.exports(filtered);
29 | }
30 | });
31 |
--------------------------------------------------------------------------------
/src/js/models/state.js:
--------------------------------------------------------------------------------
1 | /*global app, $, me*/
2 | "use strict";
3 |
4 | var HumanModel = require('human-model');
5 |
6 | module.exports = HumanModel.define({
7 | initialize: function () {
8 | var self = this;
9 | $(window).blur(function () {
10 | self.focused = false;
11 | });
12 | $(window).focus(function () {
13 | self.focused = true;
14 | if (me._activeContact) {
15 | me.setActiveContact(me._activeContact);
16 | }
17 | self.markActive();
18 | });
19 |
20 | app.desktop.on('sleep', function () {
21 | clearTimeout(this.idleTimer);
22 | console.log('went to sleep');
23 | self.markInactive();
24 | });
25 |
26 | self.cacheStatus = app.cache.state;
27 | app.cache.on('change', function (state) {
28 | self.cacheStatus = state;
29 | });
30 |
31 | document.addEventListener('deviceid', function (event) {
32 | self.deviceID = event.deviceid;
33 | });
34 |
35 | this.markActive();
36 | },
37 | session: {
38 | focused: ['bool', false, true],
39 | active: ['bool', false, false],
40 | connected: ['bool', false, false],
41 | hasConnected: ['bool', false, false],
42 | idleTimeout: ['number', false, 600000],
43 | idleSince: 'date',
44 | allowAlerts: ['bool', false, false],
45 | badge: 'string',
46 | pageTitle: 'string',
47 | hasActiveCall: ['boolean', false, false],
48 | cacheStatus: 'string',
49 | deviceID: ['string', false, ''],
50 | pageChanged: ['string', false, '']
51 | },
52 | derived: {
53 | title: {
54 | deps: ['pageTitle', 'badge'],
55 | fn: function () {
56 | return this.badge
57 | + 'Kaiwa'
58 | + (this.pageTitle ? ' - ' + this.pageTitle : '');
59 | }
60 | },
61 | deviceIDReady: {
62 | deps: ['connected', 'deviceID'],
63 | fn: function () {
64 | return (this.connected && !!this.deviceID);
65 | }
66 | },
67 | currentPageIsSettings: {
68 | deps: ['pageChanged'],
69 | fn: function () {
70 | return this.pageChanged === 'settings' ? 'active' : '';
71 | }
72 | }
73 | },
74 | markActive: function () {
75 | clearTimeout(this.idleTimer);
76 |
77 | var wasInactive = !this.active;
78 | this.active = true;
79 | this.idleSince = new Date(Date.now());
80 |
81 | this.idleTimer = setTimeout(this.markInactive.bind(this), this.idleTimeout);
82 | },
83 | markInactive: function () {
84 | if (this.focused) {
85 | return this.markActive();
86 | }
87 |
88 | this.active = false;
89 | this.idleSince = new Date(Date.now());
90 | }
91 | });
92 |
--------------------------------------------------------------------------------
/src/js/pages/base.js:
--------------------------------------------------------------------------------
1 | /*global $, app, me*/
2 | "use strict";
3 |
4 | var _ = require('underscore');
5 | var HumanView = require('human-view');
6 |
7 |
8 | module.exports = HumanView.extend({
9 | show: function (animation) {
10 | var self = this;
11 |
12 | $('body').scrollTop(0);
13 |
14 | if (this.detached) {
15 | this.$('#pages').append(this.el);
16 | this.detached = false;
17 | } else {
18 | this.render();
19 | }
20 |
21 | this.$el.addClass('active');
22 |
23 | app.currentPage = this;
24 |
25 | app.state.pageTitle = _.result(self, 'title');
26 |
27 | this.trigger('pageloaded');
28 |
29 | if (this.model.jid) {
30 | me.setActiveContact(this.model.jid);
31 | }
32 |
33 | return this;
34 | },
35 | hide: function () {
36 | var self = this;
37 |
38 | this.$el.removeClass('active');
39 |
40 | this.trigger('pageunloaded');
41 |
42 | if (this.cache) {
43 | this.$el.detach();
44 | this.detached = true;
45 | } else {
46 | this.animateRemove();
47 | }
48 |
49 | me.setActiveContact('');
50 |
51 | return this;
52 | }
53 | });
54 |
--------------------------------------------------------------------------------
/src/js/pages/chat.js:
--------------------------------------------------------------------------------
1 | /*global $, app, me, client*/
2 | "use strict";
3 |
4 | var _ = require('underscore');
5 | var StanzaIo = require('stanza.io');
6 | var StayDown = require('staydown');
7 | var BasePage = require('./base');
8 | var templates = require('../templates');
9 | var Message = require('../views/message');
10 | var MessageModel = require('../models/message');
11 | var embedIt = require('../helpers/embedIt');
12 | var htmlify = require('../helpers/htmlify');
13 | var attachMediaStream = require('attachmediastream');
14 |
15 | module.exports = BasePage.extend({
16 | template: templates.pages.chat,
17 | initialize: function (spec) {
18 | this.editMode = false;
19 |
20 | this.listenTo(this, 'pageloaded', this.handlePageLoaded);
21 | this.listenTo(this, 'pageunloaded', this.handlePageUnloaded);
22 |
23 | this.listenTo(this.model.messages, 'change', this.refreshModel);
24 | this.listenTo(this.model.messages, 'reset', this.renderCollection);
25 | this.listenTo(this.model, 'refresh', this.renderCollection);
26 |
27 | app.state.bind('change:connected', this.connectionChange, this);
28 | this.model.bind('change:avatar', this.handleAvatarChanged, this);
29 |
30 | this.render();
31 | },
32 | events: {
33 | 'keydown textarea': 'handleKeyDown',
34 | 'keyup textarea': 'handleKeyUp',
35 | 'click .call': 'handleCallClick',
36 | 'click .accept': 'handleAcceptClick',
37 | 'click .end': 'handleEndClick',
38 | 'click .mute': 'handleMuteClick'
39 | },
40 | srcBindings: {
41 | streamUrl: 'video.remote'
42 | },
43 | textBindings: {
44 | displayName: 'header .name',
45 | formattedTZO: 'header .tzo',
46 | status: 'header .status',
47 | chatStateText: '.chatBox .contactState'
48 | },
49 | classBindings: {
50 | chatState: 'header',
51 | idle: '.user_presence',
52 | show: '.user_presence',
53 | onCall: '.conversation'
54 | },
55 | show: function (animation) {
56 | BasePage.prototype.show.apply(this, [animation]);
57 | this.sendChatState('active');
58 |
59 | this.firstChanged = true;
60 | var self = this;
61 | $('.messages').scroll(function() {
62 | if (self.firstChanged && $(".messages li:first-child").offset().top > 0) {
63 | self.firstChanged = false;
64 | self.model.fetchHistory();
65 | }
66 | });
67 |
68 | this.$chatInput.focus();
69 | },
70 | hide: function () {
71 | BasePage.prototype.hide.apply(this);
72 | this.sendChatState('inactive');
73 | },
74 | render: function () {
75 | if (this.rendered) return this;
76 | var self = this;
77 |
78 | this.rendered = true;
79 |
80 | this.renderAndBind();
81 |
82 | this.$chatInput = this.$('.chatBox textarea');
83 | this.$chatInput.val(app.composing[this.model.jid] || '');
84 | this.$chatBox = this.$('.chatBox');
85 | this.$messageList = this.$('.messages');
86 |
87 | this.staydown = new StayDown({target: this.$messageList[0], interval: 500});
88 | this.renderCollection();
89 |
90 | this.listenTo(this.model.messages, 'add', this.handleChatAdded);
91 | this.listenToAndRun(this.model, 'change:jingleResources', this.handleJingleResourcesChanged);
92 |
93 | $(window).on('resize', _.bind(this.resizeInput, this));
94 |
95 | this.registerBindings(me, {
96 | srcBindings: {
97 | streamUrl: 'video.local'
98 | }
99 | });
100 |
101 | return this;
102 | },
103 | handlePageLoaded: function () {
104 | this.staydown.checkdown();
105 | this.resizeInput();
106 | },
107 | handleCallClick: function (e) {
108 | e.preventDefault();
109 | this.model.call();
110 | return false;
111 | },
112 | renderCollection: function () {
113 | var self = this;
114 |
115 | this.$messageList.empty();
116 | delete this.firstModel;
117 | delete this.firstDate;
118 | delete this.lastModel;
119 | delete this.lastDate;
120 |
121 | this.model.messages.each(function (model, i) {
122 | self.appendModel(model);
123 | });
124 | this.staydown.checkdown();
125 | },
126 | handleKeyDown: function (e) {
127 | if (e.which === 13 && !e.shiftKey) {
128 | app.composing[this.model.jid] = '';
129 | this.sendChat();
130 | this.sendChatState('active');
131 | e.preventDefault();
132 | return false;
133 | } else if (e.which === 38 && this.$chatInput.val() === '' && this.model.lastSentMessage) {
134 | this.editMode = true;
135 | this.$chatInput.addClass('editing');
136 | this.$chatInput.val(this.model.lastSentMessage.body);
137 | e.preventDefault();
138 | return false;
139 | } else if (e.which === 40 && this.editMode) {
140 | this.editMode = false;
141 | this.$chatInput.removeClass('editing');
142 | e.preventDefault();
143 | return false;
144 | } else if (!e.ctrlKey && !e.metaKey) {
145 | if (!this.typing || this.paused) {
146 | this.typing = true;
147 | this.paused = false;
148 | this.$chatInput.addClass('typing');
149 | this.sendChatState('composing');
150 | }
151 | }
152 | },
153 | handleKeyUp: function (e) {
154 | this.resizeInput();
155 | app.composing[this.model.jid] = this.$chatInput.val();
156 | if (this.typing && this.$chatInput.val().length === 0) {
157 | this.typing = false;
158 | this.$chatInput.removeClass('typing');
159 | this.sendChatState('active');
160 | } else if (this.typing) {
161 | this.pausedTyping();
162 | }
163 | },
164 | pausedTyping: _.debounce(function () {
165 | if (this.typing && !this.paused) {
166 | this.paused = true;
167 | this.sendChatState('paused');
168 | }
169 | }, 3000),
170 | sendChatState: function (state) {
171 | //if (!this.model.supportsChatStates) return;
172 | client.sendMessage({
173 | to: this.model.lockedResource || this.model.jid,
174 | chatState: state
175 | });
176 | },
177 | sendChat: function () {
178 | var message;
179 | var val = this.$chatInput.val();
180 |
181 | if (val) {
182 | this.staydown.intend_down = true;
183 |
184 | var links = _.map(htmlify.collectLinks(val), function (link) {
185 | return {url: link};
186 | });
187 |
188 | message = {
189 | id: client.nextId(),
190 | to: new StanzaIo.JID(this.model.lockedResource || this.model.jid),
191 | type: 'chat',
192 | body: val,
193 | requestReceipt: true,
194 | oobURIs: links
195 | };
196 | if (this.model.supportsChatStates) {
197 | message.chatState = 'active';
198 | }
199 | if (this.editMode) {
200 | message.replace = this.model.lastSentMessage.id;
201 | }
202 |
203 | client.sendMessage(message);
204 |
205 | // Prep message to create a Message model
206 | message.from = me.jid;
207 | message.mid = message.id;
208 | delete message.id;
209 |
210 | if (this.editMode) {
211 | this.model.lastSentMessage.correct(message);
212 | } else {
213 | var msgModel = new MessageModel(message);
214 | this.model.addMessage(msgModel, false);
215 | this.model.lastSentMessage = msgModel;
216 | }
217 | }
218 | this.editMode = false;
219 | this.typing = false;
220 | this.paused = false;
221 | this.$chatInput.removeClass('typing');
222 | this.$chatInput.removeClass('editing');
223 | this.$chatInput.val('');
224 | },
225 | handleChatAdded: function (model) {
226 | this.appendModel(model, true);
227 | },
228 | refreshModel: function (model) {
229 | var existing = this.$('#chat' + model.cid);
230 | existing.replaceWith(model.bareMessageTemplate(existing.prev().hasClass('message_header')));
231 | existing = this.$('#chat' + model.cid);
232 | embedIt(existing);
233 | },
234 | handleJingleResourcesChanged: function (model, val) {
235 | var resources = val || this.model.jingleResources;
236 | this.$('button.call').prop('disabled', !resources.length);
237 | },
238 | handleAvatarChanged: function (contact, uri) {
239 | if (!me.isMe(contact.jid)) {
240 | $('.' + contact.jid.substr(0, contact.jid.indexOf('@')) + ' .messageAvatar img').attr('src', uri);
241 | }
242 | },
243 | appendModel: function (model, preload) {
244 | var newEl, first, last;
245 | var msgDate = Date.create(model.timestamp);
246 | var messageDay = msgDate.format('{month} {ord}, {yyyy}');
247 |
248 | if (this.firstModel === undefined || msgDate > Date.create(this.firstModel.timestamp)) {
249 | if (this.firstModel === undefined) {
250 | this.firstModel = model;
251 | this.firstDate = messageDay;
252 | }
253 |
254 | if (messageDay !== this.lastDate) {
255 | var dayDivider = $(templates.includes.dayDivider({day_name: messageDay}));
256 | this.staydown.append(dayDivider[0]);
257 | this.lastDate = messageDay;
258 | }
259 |
260 | var isGrouped = model.shouldGroupWith(this.lastModel);
261 | if (isGrouped) {
262 | newEl = $(model.partialTemplateHtml);
263 | last = this.$messageList.find('li').last();
264 | last.find('.messageWrapper').append(newEl);
265 | last.addClass('chatGroup');
266 | this.staydown.checkdown();
267 | } else {
268 | newEl = $(model.templateHtml);
269 | if (!me.isMe(model.sender.jid)) newEl.addClass(model.sender.jid.substr(0, model.sender.jid.indexOf('@')));
270 | this.staydown.append(newEl[0]);
271 | this.lastModel = model;
272 | }
273 | if (!model.pending) embedIt(newEl);
274 | }
275 | else {
276 | var scrollDown = this.$messageList.prop('scrollHeight') - this.$messageList.scrollTop();
277 | var firstEl = this.$messageList.find('li').first();
278 |
279 | if (messageDay !== this.firstDate) {
280 | var dayDivider = $(templates.includes.dayDivider({day_name: messageDay}));
281 | firstEl.before(dayDivider[0]);
282 | var firstEl = this.$messageList.find('li').first();
283 | this.firstDate = messageDay;
284 | }
285 |
286 | var isGrouped = model.shouldGroupWith(this.firstModel);
287 | if (isGrouped) {
288 | newEl = $(model.partialTemplateHtml);
289 | first = this.$messageList.find('li').first().next();
290 | first.find('.messageWrapper div:first').after(newEl);
291 | first.addClass('chatGroup');
292 | } else {
293 | newEl = $(model.templateHtml);
294 | if (!me.isMe(model.sender.jid)) newEl.addClass(model.sender.jid.substr(0, model.sender.jid.indexOf('@')));
295 | firstEl.after(newEl[0]);
296 | this.firstModel = model;
297 | }
298 | if (!model.pending) embedIt(newEl);
299 |
300 | this.$messageList.scrollTop(this.$messageList.prop('scrollHeight') - scrollDown);
301 | this.firstChanged = true;
302 | }
303 | },
304 | handleAcceptClick: function (e) {
305 | e.preventDefault();
306 | var self = this;
307 |
308 | this.$('button.accept').prop('disabled', true);
309 | if (this.model.jingleCall.jingleSession.state == 'pending') {
310 | if (!client.jingle.localStream) {
311 | client.jingle.startLocalMedia(null, function (err) {
312 | if (err) {
313 | self.model.jingleCall.end({
314 | condition: 'decline'
315 | });
316 | } else {
317 | client.sendPresence({ to: new StanzaIo.JID(self.model.jingleCall.jingleSession.peer) });
318 | self.model.jingleCall.jingleSession.accept();
319 | }
320 | });
321 | } else {
322 | client.sendPresence({ to: new StanzaIo.JID(this.model.jingleCall.jingleSession.peer) });
323 | this.model.jingleCall.jingleSession.accept();
324 | }
325 | }
326 | return false;
327 | },
328 | handleEndClick: function (e) {
329 | e.preventDefault();
330 | var condition = 'success';
331 | if (this.model.jingleCall) {
332 | if (this.model.jingleCall.jingleSession && this.model.jingleCall.jingleSession.state == 'pending') {
333 | condition = 'decline';
334 | }
335 | this.model.jingleCall.end({
336 | condition: condition
337 | });
338 | }
339 | return false;
340 | },
341 | handleMuteClick: function (e) {
342 | return false;
343 | },
344 | resizeInput: _.throttle(function () {
345 | var height;
346 | var scrollHeight;
347 | var heightDiff;
348 | var newHeight;
349 | var newMargin;
350 | var marginDelta;
351 | var maxHeight = parseInt(this.$chatInput.css('max-height'), 10);
352 |
353 | this.$chatInput.removeAttr('style');
354 | height = this.$chatInput.outerHeight(),
355 | scrollHeight = this.$chatInput.get(0).scrollHeight,
356 | newHeight = Math.max(height, scrollHeight);
357 | heightDiff = height - this.$chatInput.innerHeight();
358 |
359 | if (newHeight > maxHeight) newHeight = maxHeight;
360 | if (newHeight > height) {
361 | this.$chatInput.css('height', newHeight+heightDiff);
362 | this.$chatInput.scrollTop(this.$chatInput[0].scrollHeight - this.$chatInput.height());
363 | newMargin = newHeight - height + heightDiff;
364 | marginDelta = newMargin - parseInt(this.$messageList.css('marginBottom'), 10);
365 | if (!!marginDelta) {
366 | this.$messageList.css('marginBottom', newMargin);
367 | }
368 | } else {
369 | this.$messageList.css('marginBottom', 0);
370 | }
371 | }, 300),
372 | connectionChange: function () {
373 | if (app.state.connected) {
374 | this.$chatInput.attr("disabled", false);
375 | } else {
376 | this.$chatInput.attr("disabled", "disabled");
377 | }
378 | }
379 | });
380 |
--------------------------------------------------------------------------------
/src/js/pages/settings.js:
--------------------------------------------------------------------------------
1 | /*global app, me, client, Resample*/
2 | "use strict";
3 |
4 | var BasePage = require('./base');
5 | var templates = require('../templates');
6 |
7 | module.exports = BasePage.extend({
8 | template: templates.pages.settings,
9 | classBindings: {
10 | shouldAskForAlertsPermission: '.enableAlerts',
11 | soundEnabledClass: '.soundNotifs'
12 | },
13 | srcBindings: {
14 | avatar: '#avatarChanger img'
15 | },
16 | textBindings: {
17 | status: '.status'
18 | },
19 | events: {
20 | 'click .enableAlerts': 'enableAlerts',
21 | 'click .installFirefox': 'installFirefox',
22 | 'click .soundNotifs': 'handleSoundNotifs',
23 | 'dragover': 'handleAvatarChangeDragOver',
24 | 'drop': 'handleAvatarChange',
25 | 'change #uploader': 'handleAvatarChange',
26 | 'click .disconnect': 'handleDisconnect'
27 | },
28 | render: function () {
29 | this.renderAndBind();
30 | return this;
31 | },
32 | enableAlerts: function () {
33 | if (app.notifications.permissionNeeded()) {
34 | app.notifications.requestPermission(function (perm) {
35 | if (perm === 'granted') {
36 | app.notifications.create('Ok, sweet!', {
37 | body: "You'll now be notified of stuff that happens."
38 | });
39 | }
40 | });
41 | }
42 | },
43 | installFirefox: function () {
44 | if (!app.desktop.installed) {
45 | app.desktop.install();
46 | } else {
47 | app.desktop.uninstall();
48 | }
49 | },
50 | handleAvatarChangeDragOver: function (e) {
51 | e.preventDefault();
52 | return false;
53 | },
54 | handleAvatarChange: function (e) {
55 | var file;
56 |
57 | e.preventDefault();
58 |
59 | if (e.dataTransfer) {
60 | file = e.dataTransfer.files[0];
61 | } else if (e.target.files) {
62 | file = e.target.files[0];
63 | } else {
64 | return;
65 | }
66 |
67 | if (file.type.match('image.*')) {
68 | var fileTracker = new FileReader();
69 | fileTracker.onload = function () {
70 | me.publishAvatar(this.result);
71 | };
72 | fileTracker.readAsDataURL(file);
73 | }
74 | },
75 | handleSoundNotifs: function (e) {
76 | this.model.setSoundNotification(!this.model.soundEnabled);
77 | },
78 | handleDisconnect: function (e) {
79 | client.disconnect();
80 | }
81 | });
82 |
--------------------------------------------------------------------------------
/src/js/router.js:
--------------------------------------------------------------------------------
1 | /*global app, me, client*/
2 | "use strict";
3 |
4 | var Backbone = require('backbone');
5 | var SettingsPage = require('./pages/settings');
6 | var ChatPage = require('./pages/chat');
7 | var GroupChatPage = require('./pages/groupchat');
8 |
9 |
10 | module.exports = Backbone.Router.extend({
11 | routes: {
12 | '': 'settings',
13 | 'chat/:jid': 'chat',
14 | 'chat/:jid/:resource': 'chat',
15 | 'groupchat/:jid': 'groupchat',
16 | 'logout': 'logout'
17 | },
18 | // ------- ROUTE HANDLERS ---------
19 | settings: function () {
20 | app.renderPage(new SettingsPage({
21 | model: me
22 | }));
23 | },
24 | chat: function (jid) {
25 | var contact = me.contacts.get(decodeURIComponent(jid));
26 | if (contact) {
27 | app.renderPage(new ChatPage({
28 | model: contact
29 | }));
30 | } else {
31 | app.navigate('/');
32 | }
33 | },
34 | groupchat: function (jid) {
35 | var contact = me.mucs.get(decodeURIComponent(jid));
36 | if (contact) {
37 | app.renderPage(new GroupChatPage({
38 | model: contact
39 | }));
40 | } else {
41 | app.navigate('/');
42 | }
43 | },
44 | logout: function () {
45 | if (client.sessionStarted) {
46 | client.disconnect();
47 | }
48 | localStorage.clear();
49 | window.location = 'login.html';
50 | }
51 | });
52 |
--------------------------------------------------------------------------------
/src/js/storage/archive.js:
--------------------------------------------------------------------------------
1 | /*global, IDBKeyRange*/
2 | "use strict";
3 |
4 | function ArchiveStorage(storage) {
5 | this.storage = storage;
6 | }
7 |
8 | ArchiveStorage.prototype = {
9 | constructor: {
10 | value: ArchiveStorage
11 | },
12 | setup: function (db) {
13 | if (db.objectStoreNames.contains('archive')) {
14 | db.deleteObjectStore('archive');
15 | }
16 | var store = db.createObjectStore('archive', {
17 | keyPath: 'archivedId'
18 | });
19 | store.createIndex("owner", "owner", {unique: false});
20 | },
21 | transaction: function (mode) {
22 | var trans = this.storage.db.transaction('archive', mode);
23 | return trans.objectStore('archive');
24 | },
25 | add: function (message, cb) {
26 | cb = cb || function () {};
27 | var request = this.transaction('readwrite').put(message);
28 | request.onsuccess = function () {
29 | cb(false, message);
30 | };
31 | request.onerror = cb;
32 | },
33 | get: function (id, cb) {
34 | cb = cb || function () {};
35 | if (!id) {
36 | return cb('not-found');
37 | }
38 | var request = this.transaction('readonly').get(id);
39 | request.onsuccess = function (e) {
40 | var res = request.result;
41 | if (res === undefined) {
42 | return cb('not-found');
43 | }
44 | request.result.acked = true;
45 | cb(false, request.result);
46 | };
47 | request.onerror = cb;
48 | },
49 | getAll: function (owner, cb) {
50 | cb = cb || function () {};
51 | var results = [];
52 |
53 | var store = this.transaction('readonly');
54 | var request = store.index('owner').openCursor(IDBKeyRange.only(owner));
55 | request.onsuccess = function (e) {
56 | var cursor = e.target.result;
57 | if (cursor) {
58 | cursor.value.acked = true;
59 | results.push(cursor.value);
60 | cursor.continue();
61 | } else {
62 | cb(false, results);
63 | }
64 | };
65 | request.onerror = cb;
66 | },
67 |
68 | };
69 |
70 |
71 | module.exports = ArchiveStorage;
72 |
--------------------------------------------------------------------------------
/src/js/storage/avatars.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // SCHEMA
4 | // id: 'sha1 hash',
5 | // dataURI: '...'
6 |
7 |
8 | function AvatarStorage(storage) {
9 | this.storage = storage;
10 | }
11 |
12 | AvatarStorage.prototype = {
13 | constructor: {
14 | value: AvatarStorage
15 | },
16 | setup: function (db) {
17 | if (db.objectStoreNames.contains('avatars')) {
18 | db.deleteObjectStore('avatars');
19 | }
20 | db.createObjectStore('avatars', {
21 | keyPath: 'id'
22 | });
23 | },
24 | transaction: function (mode) {
25 | var trans = this.storage.db.transaction('avatars', mode);
26 | return trans.objectStore('avatars');
27 | },
28 | add: function (avatar, cb) {
29 | cb = cb || function () {};
30 | var request = this.transaction('readwrite').put(avatar);
31 | request.onsuccess = function () {
32 | cb(false, avatar);
33 | };
34 | request.onerror = cb;
35 | },
36 | get: function (id, cb) {
37 | cb = cb || function () {};
38 | if (!id) {
39 | return cb('not-found');
40 | }
41 | var request = this.transaction('readonly').get(id);
42 | request.onsuccess = function (e) {
43 | var res = request.result;
44 | if (res === undefined) {
45 | return cb('not-found');
46 | }
47 | cb(false, request.result);
48 | };
49 | request.onerror = cb;
50 | },
51 | remove: function (id, cb) {
52 | cb = cb || function () {};
53 | var request = this.transaction('readwrite')['delete'](id);
54 | request.onsuccess = function (e) {
55 | cb(false, request.result);
56 | };
57 | request.onerror = cb;
58 | }
59 | };
60 |
61 |
62 | module.exports = AvatarStorage;
63 |
--------------------------------------------------------------------------------
/src/js/storage/disco.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | function DiscoStorage(storage) {
4 | this.storage = storage;
5 | }
6 |
7 | DiscoStorage.prototype = {
8 | constructor: {
9 | value: DiscoStorage
10 | },
11 | setup: function (db) {
12 | if (db.objectStoreNames.contains('disco')) {
13 | db.deleteObjectStore('disco');
14 | }
15 | db.createObjectStore('disco', {
16 | keyPath: 'ver'
17 | });
18 | },
19 | transaction: function (mode) {
20 | var trans = this.storage.db.transaction('disco', mode);
21 | return trans.objectStore('disco');
22 | },
23 | add: function (ver, disco, cb) {
24 | cb = cb || function () {};
25 | var data = {
26 | ver: ver,
27 | disco: disco
28 | };
29 | var request = this.transaction('readwrite').put(data);
30 | request.onsuccess = function () {
31 | cb(false, data);
32 | };
33 | request.onerror = cb;
34 | },
35 | get: function (ver, cb) {
36 | cb = cb || function () {};
37 | if (!ver) {
38 | return cb('not-found');
39 | }
40 | var request = this.transaction('readonly').get(ver);
41 | request.onsuccess = function (e) {
42 | var res = request.result;
43 | if (res === undefined) {
44 | return cb('not-found');
45 | }
46 | cb(false, res.disco);
47 | };
48 | request.onerror = cb;
49 | }
50 | };
51 |
52 |
53 | module.exports = DiscoStorage;
54 |
--------------------------------------------------------------------------------
/src/js/storage/index.js:
--------------------------------------------------------------------------------
1 | /*global indexedDB*/
2 | "use strict";
3 |
4 | var AvatarStorage = require('./avatars');
5 | var RosterStorage = require('./roster');
6 | var DiscoStorage = require('./disco');
7 | var ArchiveStorage = require('./archive');
8 | var ProfileStorage = require('./profile');
9 |
10 |
11 | function Storage() {
12 | this.db = null;
13 | this.init = [];
14 |
15 | this.avatars = new AvatarStorage(this);
16 | this.roster = new RosterStorage(this);
17 | this.disco = new DiscoStorage(this);
18 | this.archive = new ArchiveStorage(this);
19 | this.profiles = new ProfileStorage(this);
20 | }
21 | Storage.prototype = {
22 | constructor: {
23 | value: Storage
24 | },
25 | version: 3,
26 | open: function (cb) {
27 | cb = cb || function () {};
28 |
29 | var self = this;
30 | var request = indexedDB.open('datastorage', this.version);
31 | request.onsuccess = function (e) {
32 | self.db = e.target.result;
33 | cb(false, self.db);
34 | };
35 | request.onupgradeneeded = function (e) {
36 | var db = e.target.result;
37 | self.avatars.setup(db);
38 | self.roster.setup(db);
39 | self.disco.setup(db);
40 | self.archive.setup(db);
41 | self.profiles.setup(db);
42 | };
43 | request.onerror = cb;
44 | }
45 | };
46 |
47 |
48 | module.exports = Storage;
49 |
--------------------------------------------------------------------------------
/src/js/storage/profile.js:
--------------------------------------------------------------------------------
1 | /*global, IDBKeyRange*/
2 | "use strict";
3 |
4 | // SCHEMA
5 | // jid: string
6 | // name: string
7 | // avatarID: string
8 | // status: string
9 | // rosterVer: string
10 |
11 |
12 | function ProfileStorage(storage) {
13 | this.storage = storage;
14 | }
15 |
16 | ProfileStorage.prototype = {
17 | constructor: {
18 | value: ProfileStorage
19 | },
20 | setup: function (db) {
21 | if (db.objectStoreNames.contains('profiles')) {
22 | db.deleteObjectStore('profiles');
23 | }
24 | var store = db.createObjectStore('profiles', {
25 | keyPath: 'jid'
26 | });
27 | },
28 | transaction: function (mode) {
29 | var trans = this.storage.db.transaction('profiles', mode);
30 | return trans.objectStore('profiles');
31 | },
32 | set: function (profile, cb) {
33 | cb = cb || function () {};
34 | var request = this.transaction('readwrite').put(profile);
35 | request.onsuccess = function () {
36 | cb(false, profile);
37 | };
38 | request.onerror = cb;
39 | },
40 | get: function (id, cb) {
41 | cb = cb || function () {};
42 | if (!id) {
43 | return cb('not-found');
44 | }
45 | var request = this.transaction('readonly').get(id);
46 | request.onsuccess = function (e) {
47 | var res = request.result;
48 | if (res === undefined) {
49 | return cb('not-found');
50 | }
51 | cb(false, request.result);
52 | };
53 | request.onerror = cb;
54 | },
55 | remove: function (id, cb) {
56 | cb = cb || function () {};
57 | var request = this.transaction('readwrite')['delete'](id);
58 | request.onsuccess = function (e) {
59 | cb(false, request.result);
60 | };
61 | request.onerror = cb;
62 | }
63 | };
64 |
65 |
66 | module.exports = ProfileStorage;
67 |
--------------------------------------------------------------------------------
/src/js/storage/roster.js:
--------------------------------------------------------------------------------
1 | /*global, IDBKeyRange*/
2 | "use strict";
3 |
4 | // SCHEMA
5 | // jid: string
6 | // name: string
7 | // subscription: string
8 | // groups: array
9 | // rosterID: string
10 |
11 |
12 | function RosterStorage(storage) {
13 | this.storage = storage;
14 | }
15 |
16 | RosterStorage.prototype = {
17 | constructor: {
18 | value: RosterStorage
19 | },
20 | setup: function (db) {
21 | if (db.objectStoreNames.contains('roster')) {
22 | db.deleteObjectStore('roster');
23 | }
24 | var store = db.createObjectStore('roster', {
25 | keyPath: 'storageId'
26 | });
27 | store.createIndex("owner", "owner", {unique: false});
28 | },
29 | transaction: function (mode) {
30 | var trans = this.storage.db.transaction('roster', mode);
31 | return trans.objectStore('roster');
32 | },
33 | add: function (contact, cb) {
34 | cb = cb || function () {};
35 | var request = this.transaction('readwrite').put(contact);
36 | request.onsuccess = function () {
37 | cb(false, contact);
38 | };
39 | request.onerror = cb;
40 | },
41 | get: function (id, cb) {
42 | cb = cb || function () {};
43 | if (!id) {
44 | return cb('not-found');
45 | }
46 | var request = this.transaction('readonly').get(id);
47 | request.onsuccess = function (e) {
48 | var res = request.result;
49 | if (res === undefined) {
50 | return cb('not-found');
51 | }
52 | cb(false, request.result);
53 | };
54 | request.onerror = cb;
55 | },
56 | getAll: function (owner, cb) {
57 | cb = cb || function () {};
58 | var results = [];
59 |
60 | var store = this.transaction('readonly');
61 | var request = store.index('owner').openCursor(IDBKeyRange.only(owner));
62 | request.onsuccess = function (e) {
63 | var cursor = e.target.result;
64 | if (cursor) {
65 | results.push(cursor.value);
66 | cursor.continue();
67 | } else {
68 | cb(false, results);
69 | }
70 | };
71 | request.onerror = cb;
72 | },
73 | remove: function (id, cb) {
74 | cb = cb || function () {};
75 | var request = this.transaction('readwrite')['delete'](id);
76 | request.onsuccess = function (e) {
77 | cb(false, request.result);
78 | };
79 | request.onerror = cb;
80 | },
81 | clear: function (cb) {
82 | cb = cb || function () {};
83 | var request = this.transaction('readwrite').clear();
84 | request.onsuccess = function () {
85 | cb(false, request.result);
86 | };
87 | request.onerror = cb;
88 | }
89 | };
90 |
91 |
92 | module.exports = RosterStorage;
93 |
--------------------------------------------------------------------------------
/src/js/storage/rosterver.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // SCHEMA
4 | // jid: 'string',
5 | // ver: 'string'
6 |
7 |
8 | function RosterVerStorage(storage) {
9 | this.storage = storage;
10 | }
11 |
12 | RosterVerStorage.prototype = {
13 | constructor: {
14 | value: RosterVerStorage
15 | },
16 | setup: function (db) {
17 | if (db.objectStoreNames.contains('rosterver')) {
18 | db.deleteObjectStore('rosterver');
19 | }
20 | db.createObjectStore('rosterver', {
21 | keyPath: 'jid'
22 | });
23 | },
24 | transaction: function (mode) {
25 | var trans = this.storage.db.transaction('rosterver', mode);
26 | return trans.objectStore('rosterver');
27 | },
28 | set: function (jid, ver, cb) {
29 | cb = cb || function () {};
30 | var data = {
31 | jid: jid,
32 | ver: ver
33 | };
34 | var request = this.transaction('readwrite').put(data);
35 | request.onsuccess = function () {
36 | cb(false, data);
37 | };
38 | request.onerror = cb;
39 | },
40 | get: function (jid, cb) {
41 | cb = cb || function () {};
42 | if (!jid) {
43 | return cb('not-found');
44 | }
45 | var request = this.transaction('readonly').get(jid);
46 | request.onsuccess = function (e) {
47 | var res = request.result;
48 | if (res === undefined) {
49 | return cb('not-found');
50 | }
51 | cb(false, request.result);
52 | };
53 | request.onerror = cb;
54 | },
55 | remove: function (jid, cb) {
56 | cb = cb || function () {};
57 | var request = this.transaction('readwrite')['delete'](jid);
58 | request.onsuccess = function (e) {
59 | cb(false, request.result);
60 | };
61 | request.onerror = cb;
62 | }
63 | };
64 |
65 |
66 | module.exports = RosterVerStorage;
67 |
--------------------------------------------------------------------------------
/src/js/views/call.js:
--------------------------------------------------------------------------------
1 | /*global $, app*/
2 | "use strict";
3 |
4 | var _ = require('underscore');
5 | var HumanView = require('human-view');
6 | var templates = require('../templates');
7 |
8 |
9 | module.exports = HumanView.extend({
10 | template: templates.includes.call,
11 | classBindings: {
12 | state: ''
13 | },
14 | events: {
15 | 'click .answer': 'handleAnswerClick',
16 | 'click .ignore': 'handleIgnoreClick',
17 | 'click .cancel': 'handleCancelClick',
18 | 'click .end': 'handleEndClick',
19 | 'click .mute': 'handleMuteClick'
20 | },
21 | render: function () {
22 | this.renderAndBind();
23 | // register bindings for sub model
24 | this.registerBindings(this.model.contact, {
25 | textBindings: {
26 | displayName: '.callerName'
27 | },
28 | srcBindings: {
29 | avatar: '.callerAvatar'
30 | }
31 | });
32 | this.$buttons = this.$('button');
33 | this.listenToAndRun(this.model, 'change:state', this.handleCallStateChange);
34 |
35 | return this;
36 | },
37 | handleAnswerClick: function (e) {
38 | e.preventDefault();
39 | var self = this;
40 | self.model.state = 'active';
41 | app.navigate('/chat/' + encodeURIComponent(self.model.contact.jid));
42 | self.model.contact.onCall = true;
43 | self.model.jingleSession.accept();
44 | return false;
45 | },
46 | handleIgnoreClick: function (e) {
47 | e.preventDefault();
48 | this.model.jingleSession.end({
49 | condition: 'decline'
50 | });
51 | return false;
52 | },
53 | handleCancelClick: function (e) {
54 | e.preventDefault();
55 | this.model.jingleSession.end({
56 | condition: 'cancel'
57 | });
58 | return false;
59 | },
60 | handleEndClick: function (e) {
61 | e.preventDefault();
62 | this.model.jingleSession.end({
63 | condition: 'success'
64 | });
65 | return false;
66 | },
67 | handleMuteClick: function (e) {
68 | return false;
69 | },
70 | // we want to make sure we show the appropriate buttons
71 | // when in various stages of the call
72 | handleCallStateChange: function (model, callState) {
73 | var state = callState || this.model.state;
74 | // hide all
75 | this.$buttons.hide();
76 |
77 | var map = {
78 | incoming: '.ignore, .answer',
79 | outgoing: '.cancel',
80 | accepted: '.end, .mute',
81 | terminated: '',
82 | ringing: '.cancel',
83 | mute: '.end, .unmute',
84 | unmute: '.end, .mute',
85 | //hold: '',
86 | //resumed: ''
87 | };
88 |
89 | console.log('map[state]', map[state]);
90 |
91 | this.$(map[state]).show();
92 | }
93 | });
94 |
--------------------------------------------------------------------------------
/src/js/views/contactListItem.js:
--------------------------------------------------------------------------------
1 | /*global $, app, me*/
2 | "use strict";
3 |
4 | var _ = require('underscore');
5 | var HumanView = require('human-view');
6 | var templates = require('../templates');
7 |
8 |
9 | module.exports = HumanView.extend({
10 | template: templates.includes.contactListItem,
11 | classBindings: {
12 | show: '',
13 | subscription: '',
14 | chatState: '',
15 | activeContact: '',
16 | hasUnread: '',
17 | idle: '',
18 | persistent: ''
19 | },
20 | textBindings: {
21 | displayName: '.name',
22 | displayUnreadCount: '.unread'
23 | },
24 | srcBindings: {
25 | avatar: '.avatar'
26 | },
27 | events: {
28 | 'click': 'handleClick',
29 | 'click .remove': 'handleRemoveContact'
30 | },
31 | render: function () {
32 | this.renderAndBind({contact: this.model});
33 | return this;
34 | },
35 | handleClick: function () {
36 | if (me.contacts.get(this.model.jid)) {
37 | app.navigate('chat/' + encodeURIComponent(this.model.jid));
38 | }
39 | },
40 | handleRemoveContact: function() {
41 | var question = "Remove "
42 | + (this.model.name ?
43 | (this.model.name + " (" + this.model.jid + ")")
44 | : this.model.jid)
45 | + " from contact list?";
46 | if(!confirm(question)) return;
47 | me.removeContact(this.model.jid);
48 | if (app.history.fragment === 'chat/' + encodeURIComponent(this.model.jid)) {
49 | app.navigate('/');
50 | }
51 | }
52 | });
53 |
--------------------------------------------------------------------------------
/src/js/views/contactRequest.js:
--------------------------------------------------------------------------------
1 | /*global $, app*/
2 | "use strict";
3 |
4 | var _ = require('underscore');
5 | var HumanView = require('human-view');
6 | var templates = require('../templates');
7 |
8 |
9 | module.exports = HumanView.extend({
10 | template: templates.includes.contactRequest,
11 | initialize: function (opts) {
12 | this.render();
13 | },
14 | events: {
15 | 'click .approve': 'handleApprove',
16 | 'click .deny': 'handleDeny'
17 | },
18 | textBindings: {
19 | jid: '.jid'
20 | },
21 | render: function () {
22 | this.renderAndBind({message: this.model});
23 | return this;
24 | },
25 | handleApprove: function (e) {
26 | e.preventDefault();
27 | app.api.sendPresence({
28 | to: this.model.jid,
29 | type: 'subscribed'
30 | });
31 | app.api.sendPresence({
32 | to: this.model.jid,
33 | type: 'subscribe'
34 | });
35 | app.me.contactRequests.remove(this.model);
36 | return false;
37 | },
38 | handleDeny: function (e) {
39 | e.preventDefault();
40 | app.api.sendPresence({
41 | to: this.model.jid,
42 | type: 'unsubscribed'
43 | });
44 | app.me.contactRequests.remove(this.model);
45 | return false;
46 | }
47 | });
48 |
--------------------------------------------------------------------------------
/src/js/views/main.js:
--------------------------------------------------------------------------------
1 | /*global $, app, me, client*/
2 | "use strict";
3 |
4 | var HumanView = require('human-view');
5 | var StanzaIo = require('stanza.io');
6 | var templates = require('../templates');
7 | var ContactListItem = require('../views/contactListItem');
8 | var MUCListItem = require('../views/mucListItem');
9 | var CallView = require('../views/call');
10 |
11 | var ContactRequestItem = require('../views/contactRequest');
12 |
13 |
14 | module.exports = HumanView.extend({
15 | template: templates.body,
16 | initialize: function () {
17 | this.listenTo(app.state, 'change:title', this.handleTitle);
18 | app.desktop.updateBadge('');
19 | app.state.on('change:deviceID', function () {
20 | console.log('DEVICE ID>>>', app.state.deviceID);
21 | });
22 |
23 | app.state.bind('change:connected', this.connectionChange, this);
24 | },
25 | events: {
26 | 'click a[href]': 'handleLinkClick',
27 | 'click .embed': 'handleEmbedClick',
28 | 'click .reconnect': 'handleReconnect',
29 | 'click .logout': 'handleLogout',
30 | 'keydown #addcontact': 'keyDownAddContact',
31 | 'keydown #joinmuc': 'keyDownJoinMUC',
32 | 'blur #me .status': 'handleStatusChange',
33 | 'keydown .status': 'keyDownStatus'
34 | },
35 | classBindings: {
36 | connected: '#topbar',
37 | cacheStatus: '#updateBar',
38 | hasActiveCall: '#wrapper',
39 | currentPageIsSettings: '.settings'
40 | },
41 | render: function () {
42 | $('head').append(templates.head());
43 | $('body').removeClass('aux');
44 | this.renderAndBind();
45 | this.renderCollection(me.contacts, ContactListItem, this.$('#roster nav'));
46 | this.renderCollection(me.mucs, MUCListItem, this.$('#bookmarks nav'));
47 | this.renderCollection(me.contactRequests, ContactRequestItem, this.$('#contactrequests'));
48 |
49 | this.$joinmuc = this.$('#joinmuc');
50 | this.$addcontact = this.$('#addcontact');
51 | this.$meStatus = this.$('#footer .status');
52 |
53 | this.registerBindings(me, {
54 | textBindings: {
55 | displayName: '#me .name',
56 | status: '#me .status',
57 | organization: '#organization #orga_name',
58 | },
59 | srcBindings: {
60 | avatar: '#me .avatar'
61 | }
62 | });
63 | return this;
64 | },
65 | handleReconnect: function (e) {
66 | client.connect();
67 | },
68 | handleLinkClick: function (e) {
69 | var t = $(e.target);
70 | var aEl = t.is('a') ? t[0] : t.closest('a')[0];
71 | var local = window.location.host === aEl.host;
72 | var path = aEl.pathname.slice(1);
73 |
74 | if (local) {
75 | e.preventDefault();
76 | app.navigate(path);
77 | return false;
78 | }
79 | },
80 | handleEmbedClick: function (e) {
81 | if (e.shiftKey) {
82 | e.preventDefault();
83 | $(e.currentTarget).toggleClass('collapsed');
84 | }
85 | },
86 | handleTitle: function (e) {
87 | document.title = app.state.title;
88 | app.desktop.updateBadge(app.state.badge);
89 | },
90 | handleStatusChange: function (e) {
91 | var text = e.target.textContent;
92 | me.status = text;
93 | client.sendPresence({
94 | status: text,
95 | caps: client.disco.caps
96 | });
97 | },
98 | keyDownStatus: function (e) {
99 | if (e.which === 13 && !e.shiftKey) {
100 | e.target.blur();
101 | return false;
102 | }
103 | },
104 | handleLogout: function (e) {
105 | app.navigate('/logout');
106 | },
107 | handleAddContact: function (e) {
108 | e.preventDefault();
109 |
110 | var contact = this.$('#addcontact').val();
111 | if (contact.indexOf('@') == -1 && SERVER_CONFIG.domain)
112 | contact += '@' + SERVER_CONFIG.domain;
113 | if (contact) {
114 | app.api.sendPresence({to: contact, type: 'subscribe'});
115 | }
116 | this.$('#addcontact').val('');
117 |
118 | return false;
119 | },
120 | keyDownAddContact: function (e) {
121 | if (e.which === 13 && !e.shiftKey) {
122 | this.handleAddContact(e);
123 | return false;
124 | }
125 | },
126 | handleJoinMUC: function (e) {
127 | e.preventDefault();
128 |
129 | var mucjid = this.$('#joinmuc').val();
130 | this.$('#joinmuc').val('');
131 | if (mucjid.indexOf('@') == -1 && SERVER_CONFIG.muc)
132 | mucjid += '@' + SERVER_CONFIG.muc;
133 | me.mucs.add({
134 | id: mucjid,
135 | name: mucjid,
136 | jid: new StanzaIo.JID(mucjid),
137 | nick: me.nick,
138 | autoJoin: true
139 | });
140 | me.mucs.save();
141 | me.mucs.get(mucjid).join(true);
142 | },
143 | keyDownJoinMUC: function (e) {
144 | if (e.which === 13 && !e.shiftKey) {
145 | this.handleJoinMUC(e);
146 | return false;
147 | }
148 | },
149 | connectionChange: function () {
150 | if (app.state.connected) {
151 | this.$joinmuc.attr("disabled", false);
152 | this.$addcontact.attr("disabled", false);
153 | this.$meStatus.attr("contenteditable", true);
154 | } else {
155 | this.$joinmuc.attr("disabled", "disabled");
156 | this.$addcontact.attr("disabled", "disabled");
157 | this.$meStatus.attr("contenteditable", false);
158 | }
159 | }
160 | });
161 |
--------------------------------------------------------------------------------
/src/js/views/message.js:
--------------------------------------------------------------------------------
1 | /*global $*/
2 | "use strict";
3 |
4 | var _ = require('underscore');
5 | var HumanView = require('human-view');
6 | var templates = require('../templates');
7 |
8 |
9 | module.exports = HumanView.extend({
10 | template: templates.includes.message,
11 | initialize: function (opts) {
12 | this.render();
13 | },
14 | classBindings: {
15 | mine: '.message',
16 | receiptReceived: '.message',
17 | pending: '.message',
18 | delayed: '.message',
19 | edited: '.message',
20 | meAction: '.message'
21 | },
22 | textBindings: {
23 | body: '.body',
24 | formattedTime: '.timestamp'
25 | },
26 | render: function () {
27 | this.renderAndBind({message: this.model});
28 | return this;
29 | }
30 | });
31 |
--------------------------------------------------------------------------------
/src/js/views/mucListItem.js:
--------------------------------------------------------------------------------
1 | /*global $, app, me*/
2 | "use strict";
3 |
4 | var _ = require('underscore');
5 | var HumanView = require('human-view');
6 | var templates = require('../templates');
7 |
8 |
9 | module.exports = HumanView.extend({
10 | template: templates.includes.mucListItem,
11 | classBindings: {
12 | activeContact: '',
13 | hasUnread: '',
14 | joined: '',
15 | persistent: ''
16 | },
17 | textBindings: {
18 | displayName: '.name',
19 | displayUnreadCount: '.unread'
20 | },
21 | events: {
22 | 'click': 'handleClick',
23 | 'click .join': 'handleJoinRoom',
24 | 'click .remove': 'handleLeaveRoom'
25 | },
26 | render: function () {
27 | this.renderAndBind({contact: this.model});
28 | return this;
29 | },
30 | handleClick: function (e) {
31 | app.navigate('groupchat/' + encodeURIComponent(this.model.jid));
32 | },
33 | handleJoinRoom: function (e) {
34 | this.model.join();
35 | },
36 | handleLeaveRoom: function (e) {
37 | var muc = this.model;
38 | muc.leave();
39 | }
40 | });
41 |
--------------------------------------------------------------------------------
/src/js/views/mucMessage.js:
--------------------------------------------------------------------------------
1 | /*global $*/
2 | "use strict";
3 |
4 | var _ = require('underscore');
5 | var HumanView = require('human-view');
6 | var templates = require('../templates');
7 |
8 |
9 | module.exports = HumanView.extend({
10 | template: templates.includes.mucMessage,
11 | initialize: function (opts) {
12 | this.render();
13 | },
14 | classBindings: {
15 | mine: '.message',
16 | pending: '.message',
17 | delayed: '.message',
18 | edited: '.message',
19 | meAction: '.message'
20 | },
21 | textBindings: {
22 | body: '.body',
23 | nick: '.nick',
24 | formattedTime: '.timestamp'
25 | },
26 | render: function () {
27 | this.renderAndBind({message: this.model});
28 | return this;
29 | }
30 | });
31 |
--------------------------------------------------------------------------------
/src/js/views/mucRosterItem.js:
--------------------------------------------------------------------------------
1 | /*global $, app, me*/
2 | "use strict";
3 |
4 | var _ = require('underscore');
5 | var HumanView = require('human-view');
6 | var templates = require('../templates');
7 |
8 |
9 | module.exports = HumanView.extend({
10 | template: templates.includes.mucRosterItem,
11 | events: {
12 | 'click': 'handleClick'
13 | },
14 | classBindings: {
15 | show: '',
16 | chatState: '',
17 | idle: ''
18 | },
19 | textBindings: {
20 | mucDisplayName: '.name'
21 | },
22 | render: function () {
23 | this.renderAndBind({contact: this.model});
24 | return this;
25 | },
26 | handleClick: function (e) {
27 | this.parent.trigger('rosterItemClicked', this.model.mucDisplayName);
28 | }
29 | });
30 |
--------------------------------------------------------------------------------
/src/manifest/manifest.cache:
--------------------------------------------------------------------------------
1 | CACHE MANIFEST
2 | # #{version}
3 |
4 | CACHE:
5 | /js/app.js
6 | /css/app.css
7 |
8 | /images/logo.png
9 | /images/icon_128x128.png
10 |
11 | /js/login.js
12 | /js/logout.js
13 |
14 | NETWORK:
15 | *
16 |
17 | FALLBACK:
18 |
--------------------------------------------------------------------------------
/src/resources/images/kaiwa.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ForNeVeR/Kaiwa/a86d4080994d49e5e6195188e7fda230ba66d437/src/resources/images/kaiwa.png
--------------------------------------------------------------------------------
/src/resources/images/logo-big.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ForNeVeR/Kaiwa/a86d4080994d49e5e6195188e7fda230ba66d437/src/resources/images/logo-big.png
--------------------------------------------------------------------------------
/src/resources/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ForNeVeR/Kaiwa/a86d4080994d49e5e6195188e7fda230ba66d437/src/resources/images/logo.png
--------------------------------------------------------------------------------
/src/resources/js/login.js:
--------------------------------------------------------------------------------
1 | if (localStorage.authFailed) {
2 | document.getElementById('auth-failed').style.display = 'block';
3 | localStorage.removeItem('authFailed');
4 | }
5 |
6 | document.getElementById('login-form').addEventListener('submit', function (e) {
7 | function value(id) {
8 | return document.getElementById(id).value;
9 | }
10 |
11 | var jid = value('jid');
12 | if (SERVER_CONFIG.domain && jid.indexOf('@') == -1)
13 | jid += "@" + SERVER_CONFIG.domain;
14 | var password = value('password');
15 | var connURL = SERVER_CONFIG.wss ? SERVER_CONFIG.wss : value('connURL');
16 | var publicComputer = document.getElementById('public-computer').checked;
17 |
18 | var transport;
19 | var wsURL = '';
20 | var boshURL = '';
21 | if (connURL.indexOf('http') === 0) {
22 | boshURL = connURL;
23 | transport = 'bosh';
24 | } else if (connURL.indexOf('ws') === 0) {
25 | wsURL = connURL;
26 | transport = 'websocket';
27 | }
28 |
29 | var softwareVersion = SERVER_CONFIG.softwareVersion;
30 | if (softwareVersion) {
31 | softwareVersion.os = navigator.userAgent
32 | }
33 |
34 | localStorage.config = JSON.stringify({
35 | jid: jid,
36 | server: jid.slice(jid.indexOf('@') + 1),
37 | wsURL: wsURL,
38 | boshURL: boshURL,
39 | transports: [transport],
40 | credentials: {
41 | password: password
42 | },
43 | saveCredentials: !publicComputer,
44 | softwareVersion: softwareVersion
45 | });
46 |
47 | window.location = './';
48 |
49 | e.preventDefault();
50 | });
51 |
--------------------------------------------------------------------------------
/src/resources/js/logout.js:
--------------------------------------------------------------------------------
1 | localStorage.clear();
2 | window.location = 'login.html';
3 |
--------------------------------------------------------------------------------
/src/resources/manifest.webapp:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Kaiwa",
3 | "description": "Modern XMPP client",
4 | "icons": {
5 | "128": "/images/icon_128x128.png"
6 | },
7 | "permissions": {
8 | "storage": {
9 | "description": "Required for caching contact information and message history."
10 | },
11 | "desktop-notification": {
12 | "description": "Required to show alerts for new messages."
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/resources/sounds/ding.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ForNeVeR/Kaiwa/a86d4080994d49e5e6195188e7fda230ba66d437/src/resources/sounds/ding.wav
--------------------------------------------------------------------------------
/src/resources/sounds/threetone-alert.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ForNeVeR/Kaiwa/a86d4080994d49e5e6195188e7fda230ba66d437/src/resources/sounds/threetone-alert.wav
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var express = require('express');
3 |
4 | var config = JSON.parse(fs.readFileSync('./dev_config.json'));
5 |
6 | var app = express();
7 | var serveStatic = require('serve-static');
8 |
9 | app.use(serveStatic('./public'));
10 |
11 | app.listen(config.http.port, function () {
12 | console.log('Kaiwa running at: ' + config.http.baseUrl);
13 | });
14 |
--------------------------------------------------------------------------------
/src/stylus/_mixins.styl:
--------------------------------------------------------------------------------
1 | @import _variables
2 |
3 | roundall($round)
4 | -moz-border-radius: $round
5 | -webkit-border-radius: $round
6 | -khtml-border-radius: $round
7 | -o-border-radius: $round
8 | -border-radius: $round
9 | border-radius: $round
10 |
11 | noselect()
12 | -webkit-touch-callout: none
13 | -webkit-user-select: none
14 | -khtml-user-select: none
15 | -moz-user-select: none
16 | -ms-user-select: none
17 | user-select: none
18 |
19 | allowselect()
20 | -webkit-touch-callout: initial
21 | -webkit-user-select: initial
22 | -khtml-user-select: initial
23 | -moz-user-select: initial
24 | -ms-user-select: initial
25 | user-select: initial
26 |
27 | transform()
28 | -webkit-transform: arguments
29 | -moz-transform: arguments
30 | -ms-transform: arguments
31 | transform: arguments
32 |
33 | borderbox()
34 | -moz-box-sizing: border-box
35 | -webkit-box-sizing: border-box
36 | box-sizing: border-box
37 |
38 | // avatars
39 |
40 | avatar()
41 | width: 36px
42 | height: 36px
43 | roundall(0.2rem)
44 | border-radius: 50%
45 |
46 | // this if for the content flash and hardware acceleration bugs in webkit
47 | webkit-transition-fix()
48 | -webkit-backface-visibility: hidden
49 | //-webkit-transform: translateZ(0)
50 |
--------------------------------------------------------------------------------
/src/stylus/_normalize.styl:
--------------------------------------------------------------------------------
1 | /*! normalize.css v2.1.3 | MIT License | git.io/normalize */
2 |
3 | article
4 | aside
5 | details
6 | figcaption
7 | figure
8 | footer
9 | header
10 | hgroup
11 | main
12 | nav
13 | section
14 | summary
15 | display: block
16 |
17 | audio, canvas, video
18 | display: inline-block
19 |
20 | audio:not([controls])
21 | display: none
22 | height: 0
23 |
24 | [hidden],
25 | template
26 | display: none
27 |
28 | html
29 | font-family: sans-serif
30 | -ms-text-size-adjust: 100%
31 | -webkit-text-size-adjust: 100%
32 |
33 | body
34 | margin: 0
35 |
36 | a
37 | background: transparent
38 |
39 | a:focus
40 | outline: thin dotted
41 |
42 | a:active,
43 | a:hover
44 | outline: 0
45 |
46 | abbr[title]
47 | border-bottom: 1px dotted
48 |
49 | dfn
50 | font-style: italic
51 |
52 | hr
53 | -moz-box-sizing: content-box
54 | box-sizing: content-box
55 | height: 0
56 |
57 | mark
58 | background: #ff0
59 | color: #000
60 |
61 | code,
62 | kbd,
63 | pre,
64 | samp
65 | font-family: monospace, serif
66 | font-size: 1em
67 |
68 | pre
69 | white-space: pre-wrap
70 |
71 | q
72 | quotes: "\201C" "\201D" "\2018" "\2019"
73 |
74 | small
75 | font-size: 80%
76 |
77 | sub,
78 | sup
79 | font-size: 75%
80 | line-height: 0
81 | position: relative
82 | vertical-align: baseline
83 |
84 | sup
85 | top: -0.5em
86 |
87 | sub
88 | bottom: -0.25em
89 |
90 | img
91 | border: 0
92 |
93 | svg:not(:root)
94 | overflow: hidden
95 |
96 | figure
97 | margin: 0
98 |
99 | fieldset
100 | border: 1px solid #c0c0c0
101 | margin: 0 2px
102 | padding: 0.35em 0.625em 0.75em
103 |
104 | legend
105 | border: 0
106 | padding: 0
107 |
108 | button,
109 | input,
110 | select,
111 | textarea
112 | font-family: inherit
113 | font-size: 100%
114 | margin: 0
115 |
116 | button,
117 | input
118 | line-height: normal
119 |
120 | button,
121 | select
122 | text-transform: none
123 |
124 | button,
125 | html input[type="button"],
126 | input[type="reset"],
127 | input[type="submit"]
128 | -webkit-appearance: button
129 | cursor: pointer
130 |
131 | button[disabled],
132 | html input[disabled]
133 | cursor: default
134 |
135 | input[type="checkbox"],
136 | input[type="radio"]
137 | box-sizing: border-box
138 | padding: 0
139 |
140 | input[type="search"]
141 | -webkit-appearance: textfield
142 | -moz-box-sizing: content-box
143 | -webkit-box-sizing: content-box
144 | box-sizing: content-box
145 |
146 | input[type="search"]::-webkit-search-cancel-button,
147 | input[type="search"]::-webkit-search-decoration
148 | -webkit-appearance: none
149 |
150 | button::-moz-focus-inner,
151 | input::-moz-focus-inner
152 | border: 0
153 | padding: 0
154 |
155 | textarea
156 | overflow: auto
157 | vertical-align: top
158 |
159 | table
160 | border-collapse: collapse
161 | border-spacing: 0
--------------------------------------------------------------------------------
/src/stylus/_variables.styl:
--------------------------------------------------------------------------------
1 | // Blues
2 |
3 | $blue-saturated-darker = #0B1316
4 | $blue-saturated-dark = #0f2034
5 | $blue-saturated = #192a47
6 | $blue-saturated-light = #34465a
7 | $blue-saturated-lighter = #6c7f95
8 |
9 | $blue = #12acef
10 | $blue-light = lighten($blue, 50%)
11 | $blue-lighter = lighten($blue, 90%)
12 |
13 | // Monochromatic
14 |
15 | $gray-dark = #2d2d2d
16 | $gray = #565656
17 | $gray-light = #878787
18 | $gray-lighter = #eeeeee
19 |
20 | // Pinks
21 |
22 | $pink = #ec008c
23 | $pink-light = #f8bee0
24 | $pink-lighter = #fce8f1
25 |
26 | // Greens
27 |
28 | $green = #41A564
29 | $green-light = lighten($green, 70%)
30 | $green-lighter = lighten($green, 94%)
31 | $green-lighterer = #B2D5C9
32 |
33 | // Reds
34 |
35 | $red = #de0a32
36 | $red-light = lighten($red, 80%)
37 | $red-lighter = lighten($red, 97%)
38 |
39 | // Orange
40 |
41 | $orange = #f18902
42 | $orange-light = lighten($orange, 50%)
43 | $orange-lighter = lighten($orange, 85%)
44 |
45 | // Brown
46 |
47 | $brown = #4D394B
48 | $brown-light = #AB9BA9
49 | $brown-lighter = lighten($brown, 20%)
50 | $brown-dark = #3E313C
51 | $brown-darker = #372C36
52 | $brown-darkerer = #625361
53 |
54 | // Gray
55 |
56 | $gray-dark = #555459
57 | $gray = #9E9EA6
58 | $gray-light = #BABBBF
59 | $gray-lighter = #E0E0E0
60 |
61 | // Black
62 |
63 | $black = #3D3C40
64 |
65 |
66 | // Style
67 |
68 | $topbarBg = #3D486B
69 | $topbarFg = #fff
70 |
71 | $meUsernameColor = #fff
72 |
73 | $sidebarTopAndBottomBg = #2B3556
74 | $sidebarOrgaName = #fff
75 | $sidebarBorder = #121A33
76 | $sidebarBg = #E5E5E5
77 | $sidebarText = #565656
78 | $sidebarSectionTitleColor = #8E8E8E
79 |
80 | $sidebarNames = #919191
81 |
82 | $sidebarInputBg = $sidebarBg
83 | $sidebarInputBorder = #BBBBBB
84 | $sidebarInputText = $sidebarNames
85 |
86 | $sidebarSelectionBg = #AEAEAE
87 | $sidebarSelectionText = #000
88 |
89 | $sidebarUnreadBg = $sidebarSelectionBg
90 | $sidebarUnreadText = #000
91 |
92 | $sidebarHover = $sidebarSelectionBg
93 | $sidebarHoverText = #fff
94 |
95 | $sidebarRequestBg = $sidebarHover
96 | $sidebarRequestBorder = $sidebarHover
97 |
98 | $sidebarStatusText = $sidebarNames
99 | $sidebarStatusBorder = $sidebarInputBorder
100 |
101 | $settingsBg = transparent
102 | $settingsText = #B5B5B5
103 | $settingsHoverBg = #fff
104 | $settingsHoverText = $sidebarSelectionBg
105 |
106 | // Font families
107 |
108 | $font-family-lato = 'Lato', sans-serif
109 | $font-family-gotham = 'Gotham SSm A', 'Gotham SSm B', Helvetica, Arial, sans-serif
110 |
111 | // Font sizes
112 |
113 | $font-size-large = 17px
114 | $font-size-message = 15px
115 | $font-size-base = 14px
116 | $font-size-small = 12px
117 | $font-size-smaller = 10px
118 |
119 | $font-size-h1 = 28px
120 | $font-size-h2 = 22px
121 | $font-size-h3 = 16px
122 | $font-size-h4 = 12px
123 |
124 | $line-height-base = 18px
125 | $line-height-headings = 26px
126 |
127 | $font-weight-classic = 500
128 | $font-weight-bold = 700
129 | $font-weight-bolder = 900
130 |
131 | $group-roster-width = 15rem
132 | $conversation-header-height = 42px
--------------------------------------------------------------------------------
/src/stylus/client.styl:
--------------------------------------------------------------------------------
1 | @import '_normalize'
2 | @import '_variables'
3 | @import '_mixins'
4 |
5 | @import 'components/layout'
6 | @import 'components/forms'
7 | @import 'components/buttons'
8 |
9 | @import 'pages/login'
10 | @import 'pages/updateBar'
11 | @import 'pages/roster'
12 | @import 'pages/chat'
13 | @import 'pages/settings'
14 | @import 'pages/aucs'
15 | @import 'pages/callbar'
16 | @import 'pages/header'
17 | @import 'pages/me'
18 |
--------------------------------------------------------------------------------
/src/stylus/components/buttons.styl:
--------------------------------------------------------------------------------
1 | // Buttons
2 |
3 | button
4 | border: none
5 |
6 | .button
7 | line-height: 35px
8 |
9 | .button, button
10 | display: inline-block
11 | padding: 0 15px
12 | height: 35px
13 | background: $gray-lighter
14 | font-size: $font-size-small
15 | font-weight: $font-weight-bold
16 | text-align: center
17 | text-decoration: none
18 | color: $blue-saturated-light
19 | border: 1px solid $blue-saturated-lighter
20 | border-radius: 3px
21 | text-overflow: ellipsis
22 | vertical-align: middle
23 | user-select()
24 |
25 | &:focus
26 | outline: none
27 |
28 | &:hover:not(:disabled)
29 | color: $gray
30 | background: darken($gray-lighter, 10%)
31 | transition: all .3s ease-in
32 |
33 | &:disabled
34 | cursor: not-allowed
35 | color: lighten($gray-light, 40%)
36 |
37 | &.small
38 | height: 25px
39 | padding: 0 8px
40 | font-size: $font-size-small
41 | line-height: 25px
42 |
43 | &.large
44 | height: 50px
45 | line-height: 50px
46 | padding: 0 30px
47 | font-size: $font-size-large
48 |
49 | &.primary, &.secondary, &.primary:hover, &.secondary:hover
50 | color: white
51 |
52 | &.primary
53 | background: $green
54 |
55 | &:hover:not(:disabled)
56 | background: darken($green, 10%)
57 |
58 | &:disabled
59 | background: $green-light
60 |
61 | &.secondary
62 | background: $red
63 |
64 | &:disabled
65 | background: $red-light
66 |
67 | &:hover:not(:disabled)
68 | background: darken($red, 10%)
69 |
70 | .button-group
71 |
72 | .button, button
73 | border-radius: 0
74 | margin-left: -1px
75 |
76 | &:first-child:only-child
77 | border-radius: 3px
78 |
79 | &:first-child
80 | border-radius: 3px 0 0 3px
81 |
82 | &:last-child
83 | border-radius: 0 3px 3px 0
84 |
85 | &.outlined
86 | display: inline-block
87 | border-radius: 3px
88 |
89 | button, .button
90 | border: 1px solid $gray-light
91 |
92 | &.primary
93 |
94 | button, .button
95 | background: $pink-lighter
96 | border: 1px solid $pink-light
97 | color: $pink
98 |
99 | &:hover
100 | background: lighten($pink-light, 40%)
101 |
102 | &.secondary
103 |
104 | button, .button
105 | background: $blue-lighter
106 | border: 1px solid $blue-light
107 | color: $blue
108 |
109 | &:hover
110 | background: lighten($blue-light, 40%)
111 |
--------------------------------------------------------------------------------
/src/stylus/components/forms.styl:
--------------------------------------------------------------------------------
1 | // Forms
2 |
3 | input[type=text], input[type=email], input[type=password], input[type=search], input[type=date], input[type=url], input[type=file], textarea, .main .status
4 | width: 100%
5 | borderbox()
6 |
7 | &.inline
8 | display: inline-block
9 | width: auto
10 |
11 | input[type=text], input[type=email], input[type=password], input[type=search],
12 | input[type=date], input[type=url], input[type=file], textarea, select, input[type=checkbox], input[type=radio]
13 |
14 | &:disabled
15 | cursor: not-allowed
16 |
17 | .invalid
18 |
19 | label
20 | color: $red
21 |
22 | input[type=text], input[type=email], input[type=password], input[type=search], input[type=date], input[type=url], input[type=file], textarea
23 | background: $red-lighter
24 | border: 1px solid $red-light
25 | color: $red-light
26 |
27 | &:focus
28 | border: 1px solid $red-light
29 | box-shadow: 0 0 5px $red-light
30 |
31 | .valid
32 |
33 | label
34 | color: $green
35 |
36 | input[type=text], input[type=email], input[type=password], input[type=search], input[type=date], input[type=url], input[type=file], textarea
37 | background: $green-lighter
38 | border: 1px solid $green-light
39 | color: $green-light
40 |
41 | &:focus
42 | border: 1px solid $green-light
43 | box-shadow: 0 0 5px $green-light
44 |
45 | input[type=text], input[type=email], input[type=password], input[type=search], input[type=date], input[type=url], input[type=file], textarea, select, .main .status
46 | display: block
47 | border-radius: 3px
48 | border: 1px solid $gray-lighter
49 |
50 | &:focus
51 | outline: none
52 |
53 | &:disabled
54 | background: lighten($gray-lighter, 60%)
55 |
56 | input[type=text], input[type=email], input[type=password], input[type=search], input[type=date], input[type=url], .main .status
57 | height: 35px
58 | padding: 7px 10px
59 |
60 | textarea
61 | padding: 10px
62 | resize: none
63 | font-size: $font-size-small
64 |
65 | input[type=file]
66 | padding: 15px
67 | background: lighten($gray-lighter, 70%)
68 | font-size: $font-size-small
69 | color: $gray-light
70 |
71 | label
72 | display: block
73 | margin-bottom: 5px
74 | font-weight: $font-weight-bold
75 | font-size: $font-size-small
76 |
77 | .has-icon
78 | position: relative
79 |
80 | .ss-icon
81 | position: absolute
82 | top: 31px
83 | right: 10px
84 | font-size: $font-size-small
85 | transition: all .3s ease-in
86 |
87 | &.ss-delete
88 | color: $red-light
89 |
90 | &.ss-check
91 | color: $green-light
92 |
93 | &.ss-search
94 | color: $gray-lighter
95 |
--------------------------------------------------------------------------------
/src/stylus/components/layout.styl:
--------------------------------------------------------------------------------
1 | @import '../_variables'
2 | @import '../_mixins'
3 |
4 | html
5 | font-size: 100%
6 |
7 | body
8 | background: white
9 | font-family: $font-family-lato
10 | font-size: $font-size-base
11 | font-weight: 400
12 | -webkit-font-smoothing: antialiased
13 |
14 | h4
15 | margin-top: 0
16 | font-size: $font-size-h4
17 |
18 | a
19 | color: $blue
20 | font-weight: $font-weight-bold
21 | text-decoration: none
22 |
23 | &:hover, &:active
24 | color: $blue-saturated-light
25 |
26 | #pages
27 | position: absolute
28 | top: 0px
29 | right: 0px
30 | left: 230px
31 | height: 100%
32 | borderbox()
33 |
--------------------------------------------------------------------------------
/src/stylus/pages/aucs.styl:
--------------------------------------------------------------------------------
1 | .aux
2 |
3 | header
4 | margin-top: 8%
5 | text-align: center
6 |
7 | #logo
8 | margin: auto
9 |
10 | .box
11 | position: relative
12 | margin: auto
13 | margin-top: 5%
14 | padding: 20px 0
15 | background: white
16 | width: 75%
17 | roundall(3px)
18 |
19 | &.connect
20 | padding: 20px
21 | text-align: center
22 |
23 | h2
24 | padding: 0
25 |
26 | .button
27 | float: none
28 | margin-top: 20px
29 |
30 | .head, .content
31 | padding: 0 20px
32 |
33 | .head
34 | margin-bottom: 20px
35 | border-bottom: 1px solid lighten($gray-lighter, 50%)
36 |
37 | input[type="text"], input[type="password"]
38 | width: 100%
39 | margin-bottom: 15px
40 |
41 | a.button
42 | float: right
43 |
44 | h2
45 | margin: 0
46 | padding-bottom: 20px
47 |
48 | @media screen and (min-width: 768px)
49 |
50 | .box
51 | width: 50%
52 |
--------------------------------------------------------------------------------
/src/stylus/pages/callbar.styl:
--------------------------------------------------------------------------------
1 | @import _mixins
2 | @import _variables
3 |
4 | $callHeight = 80px
5 |
6 | #wrapper
7 | transition: all 1s
8 |
9 | //&.hasActiveCall
10 | // transform: translateY($callHeight)
11 |
12 | #calls
13 | position: fixed
14 | top: 0
15 | left: 0px
16 | transition: all 1s
17 | width: 100%
18 | height: $callHeight
19 | z-index: 1000
20 | background: #DDD
21 |
22 | &:empty, &.ending
23 | transform: translateY(-($callHeight))
24 |
25 | .call
26 | height: $callHeight
27 | padding: 0
28 | margin: 0
29 |
30 | &.incoming
31 | background: $blue
32 | .callTime
33 | display: none
34 | .callerName:before
35 | content: "Incoming Call: "
36 |
37 | &.waiting
38 | background: $blue
39 |
40 | .spinner div
41 | background-color: #fff
42 |
43 | &.calling
44 | background: $blue
45 | .callTime
46 | display: none
47 | .callerName:before
48 | content: "Calling: "
49 |
50 | &.accepted
51 | background: $sidebarBg
52 | .callerName:before
53 | content: "On Call: "
54 |
55 | &.ending
56 | .callerName:before
57 | content: "Call ending with: "
58 | background: lighten($gray-light, 93%)
59 |
60 | .callActions
61 | position: absolute
62 | left: 80px
63 | top: 38px
64 | display: block
65 | width: 100%
66 |
67 | nav
68 | float: left
69 |
70 | button
71 | min-width: auto
72 | background-color: rgba(255,255,255,.3)
73 | border:
74 | top: 1px solid rgba(255,255,255,.6)
75 | bottom: 1px solid rgba(0,0,0,.1)
76 | left: 1px solid rgba(255,255,255,.2)
77 | right: 1px solid rgba(255,255,255,.2)
78 | width: 100px
79 | margin-right: 10px
80 | font-size: $font-size-large
81 | color: rgba(0,0,0,.75)
82 | float: left
83 | &:hover
84 | background-color: rgba(255,255,255,.4)
85 | &:active
86 | padding-top: 11px
87 | padding-bottom: 9px
88 | border:
89 | bottom: 1px solid rgba(255,255,255,1)
90 | top: 1px solid rgba(0,0,0,.2)
91 |
92 | .callerAvatar
93 | float: left
94 | width: 60px
95 | height: 60px
96 | margin: 10px
97 | roundall(30px)
98 |
99 | .callerName, .callTime
100 | font-weight: $font-weight-bold
101 | color: #fff
102 | line-height: 1
103 | font-size: $font-size-h3
104 |
105 | .caller
106 | margin: 0
107 | padding: 5px 0 0 0
108 | padding-bottom: 0px
109 |
110 | .callerName
111 | display: inline
112 | .callerNumber
113 | display: inline
114 | margin-left: 10px
115 |
116 | .callTime
117 | position: absolute
118 | top: 12px
119 | right: 40px
120 | margin: 0
121 |
--------------------------------------------------------------------------------
/src/stylus/pages/header.styl:
--------------------------------------------------------------------------------
1 | @import '../_variables'
2 | @import '../_mixins'
3 |
4 | #topbar.connected
5 | z-index: 100
6 | #connectionStatus
7 | display: none
8 |
9 | #topbar
10 | position: fixed
11 | right: 0px
12 | top: 0px
13 | height: 42px
14 | width: 100%
15 | background-color: $topbarBg
16 | z-index: 102
17 | noselect()
18 | color: $topbarFg
19 |
20 | #connectionStatus
21 | width: 340px
22 | margin: 2px auto
23 | background-color: transparent
24 |
25 | p
26 | position: relative
27 | top: 2px
28 | display: inline
29 | color: #fff
30 | font-size: 15px
31 | padding: 0
32 | margin: 0 18px 0 0
33 |
34 | button
35 | height: 30px
36 | margin: 3px 0
37 | cursor: pointer
38 |
--------------------------------------------------------------------------------
/src/stylus/pages/login.styl:
--------------------------------------------------------------------------------
1 | #auth-failed
2 | display: none
3 | color: #f00
4 | text-align: center
5 | width: 75%
6 |
7 | #loginbox
8 | margin-bottom: 120px
9 |
10 | #login-form button
11 | margin-top: 2em;
12 | width: 50%
13 |
14 | .fieldContainer.checkbox
15 | input
16 | margin-right: 0.5em
17 |
18 | label
19 | display: inline-block
20 |
21 | .fieldContainerWSS
22 | display: none
23 |
--------------------------------------------------------------------------------
/src/stylus/pages/me.styl:
--------------------------------------------------------------------------------
1 | @import '../_variables'
2 | @import '../_mixins'
3 |
4 | #topbar
5 |
6 | #me
7 | display: none
8 | position: fixed
9 | right: 0
10 | padding: 1px 0px 5px 5px
11 | width: 175px
12 |
13 | .avatar
14 | float:right
15 | width: 35px
16 | height: 35px
17 | background: rgba(0,0,0,0)
18 | background-color: transparent
19 | vertical-align: middle
20 | margin: 3px 0.5rem 0px 0px;
21 | border-radius: 50%;
22 |
23 | .name,
24 | .status
25 | display: block
26 | width: 120px
27 | overflow: hidden
28 | text-overflow: ellipsis
29 | text-align: right
30 |
31 | .name
32 | color: $meUsernameColor
33 | font-size: $font-size-base
34 | font-weight: $font-weight-bolder
35 | margin-top: 3px;
36 |
37 | .status
38 | color: $gray
39 | font-weight: normal
40 | font-size: $font-size-small
41 | border-width: 0px
42 | margin: 0px
43 | padding: 0px
44 | height: 18px
45 | white-space: nowrap
46 | transition: all .25s
47 | allowselect()
48 |
49 | &:focus
50 | border-radius: 0.25rem
51 | border: 1px solid $gray
52 | outline: none
53 | padding: 2px
54 |
55 | &:empty:before
56 | content: 'Update your status'
57 |
58 | &:before,
59 | &:empty:focus:before
60 | content: ''
61 |
62 | &.connected
63 | #me
64 | display: block
65 |
--------------------------------------------------------------------------------
/src/stylus/pages/roster.styl:
--------------------------------------------------------------------------------
1 | @import '../_variables'
2 | @import '../_mixins'
3 |
4 | #menu
5 | position: fixed
6 | top: 0px
7 | bottom: 0px
8 | left: 0px
9 | width: 230px
10 | background-color: $sidebarBg
11 | color: $sidebarText
12 | z-index: 300
13 | webkit-transition-fix()
14 | noselect()
15 |
16 | #organization
17 | width: 230px
18 | height: 42px
19 | background: $sidebarTopAndBottomBg
20 |
21 | #orga_name
22 | color: $topbarFg
23 | font-size: $font-size-large
24 | font-weight: $font-weight-bolder
25 | line-height: 42px;
26 | vertical-align: middle
27 | margin-left: 20px
28 |
29 | .settings
30 | position: relative
31 | height: 28px;
32 | left: 5px;
33 | background-color: $settingsBg
34 | color: $settingsText
35 | transition: none
36 | border: 0
37 |
38 | svg
39 | position: absolute
40 | top: 55%
41 | left: 3px
42 | margin: 0px
43 | margin-top: -13px
44 | fill: $settingsText
45 |
46 | &:hover
47 | svg
48 | fill: $settingsHoverText
49 |
50 | &.active
51 | svg
52 | fill: $settingsHoverText
53 |
54 | .viewport
55 | position: absolute
56 | top: 42px
57 | bottom: 22px
58 | left: 0
59 | right: 0
60 | overflow-y: auto
61 | overflow-x: hidden
62 | padding-top: 20px
63 | padding-bottom: 10px
64 |
65 | .main
66 | margin: 10px 0 0 0
67 | text-align: center
68 | li
69 | display: inline-block
70 |
71 | &:last-of-type
72 | a
73 | margin-left: 5px
74 | padding-left: 30px
75 |
76 | svg
77 | position: absolute
78 | top: 50%
79 | left: 5px
80 | margin-top: -13px
81 | fill: white
82 |
83 | a
84 | position: relative
85 |
86 | h1
87 | font-size: $font-size-message
88 | margin: 0
89 | padding: 4px 20px
90 | text-transform: uppercase
91 | margin-bottom: 7px;
92 | color: $sidebarSectionTitleColor
93 |
94 | #roster,
95 | #bookmarks
96 | margin-bottom: 25px
97 |
98 | #contactrequests
99 | margin: 0px
100 | padding-left: 0px
101 |
102 | li
103 | width: 180px
104 | height: 68px
105 | margin-left: 20px
106 | margin-bottom: 10px
107 | padding-bottom: 4px
108 | background: $sidebarRequestBg
109 | border-radius: 0.25rem
110 | border: 2px solid $sidebarRequestBorder
111 |
112 | .response
113 | text-align: center
114 |
115 | .jid
116 | display: inline-block
117 | width: 100%
118 | text-align: center
119 |
120 | .approve, .deny
121 | display: inline-block
122 | margin-top: 10px
123 |
124 | #addcontact,
125 | #joinmuc
126 | margin: 8px 20px 5px 20px
127 | padding: 0px 10px
128 | width: 178px
129 | height: 25px
130 | font-size: $font-size-small
131 | background-color: $sidebarInputBg
132 | border-radius: 0.25rem
133 | border: 1px solid $sidebarInputBorder
134 | color: $sidebarInputText
135 |
136 | li
137 | list-style-type: none
138 | padding: 3px 10px
139 | padding-right: 15px
140 | position: relative
141 | font-size: $font-size-base
142 | cursor: pointer
143 | width: 100%
144 | borderbox()
145 | color: $sidebarNames
146 |
147 | &:hover
148 | background: $sidebarHover
149 | color: $sidebarHoverText
150 |
151 | &.joined
152 | &:hover:not(.hasUnread) .remove
153 | visibility: visible
154 |
155 | &:not(.joined)
156 | &:hover:not(.hasUnread) .join
157 | visibility: visible
158 |
159 | &.hasUnread, &.hasUnread .prefix
160 | font-weight: $font-weight-bolder
161 | color: $sidebarUnreadText
162 |
163 | &:not(.activeContact).offline,
164 | &:not(.activeContact).chat
165 | .presence
166 | color: $brown-light
167 |
168 | &:not(.activeContact).dnd
169 | .presence
170 | color: $red
171 | opacity: 1
172 |
173 | &:not(.activeContact).away,
174 | &:not(.activeContact).xa
175 | .presence
176 | color: $orange
177 | opacity: 1
178 |
179 | &.activeContact, &.offline.activeContact
180 | background: $sidebarSelectionBg
181 | &:not(.dnd):not(.away):not(.online) .presence
182 | color: $brown-darkerer
183 | opacity: 1
184 | .remove, .join
185 | color: $sidebarSelectionText
186 | .name
187 | color: $sidebarSelectionText
188 |
189 | &:not(.activeContact).online
190 | .presence
191 | color: $green
192 | opacity: 1
193 |
194 | &.online, &.dnd, &.away
195 | .name
196 | font-style : normal
197 |
198 | &.activeContact
199 | &.online .presence,
200 | &.dnd .presence,
201 | &.away .presence
202 | color: #fff
203 | opacity: 1
204 |
205 | &.composing
206 | &:after
207 | animation: pulsate 1.5s infinite ease-in
208 | -webkit-animation: pulsate 1.5s infinite ease-in
209 | -moz-animation: pulsate 1.5s infinite ease-in
210 |
211 | &.paused
212 | &:after
213 | background: lighten($gray-light, 30%)
214 | border-color: lighten($gray-light, 30%)
215 |
216 | &.idle
217 | &:after
218 | background: transparent
219 |
220 | .name
221 | color: $sidebarNames
222 | font-style: italic
223 |
224 | .presence
225 | font-size: 10px
226 | display: inline-block
227 | margin-top: -10px
228 | vertical-align: middle
229 | position: absolute;
230 | top: 28px;
231 | left: 37px
232 |
233 | .user
234 | display: inline-block
235 | width: 170px
236 | overflow: hidden
237 | text-overflow: ellipsis
238 | height: 22px
239 | font-style: italic
240 |
241 | .status
242 | font-size: $font-size-small
243 | font-weight: 400
244 | line-height: 12px
245 | margin: 0
246 |
247 | .idleTime
248 | display: inline-block
249 | margin-left: 5px
250 | font-size: $font-size-small
251 | color: darken($brown-light, 50%)
252 |
253 | .avatar
254 | width: 20px
255 | height: 20px
256 | roundall(0.2rem)
257 | border-radius: 50%
258 | margin: 0
259 | padding: 0
260 | margin-right: 10px
261 | margin-top: 2px
262 |
263 | .name
264 | width: 100%
265 | overflow: hidden
266 | text-overflow: ellipsis
267 | max-width: 140px
268 | line-height: 23px
269 | height: 23px
270 | position: absolute
271 |
272 | .unread
273 | display: none
274 | color: $sidebarUnreadText
275 | height: 16px
276 | width: 18px
277 | roundall(5px)
278 | position: absolute
279 | padding-top:2px
280 | right: 15px
281 | top: 6px
282 | font-size: $font-size-smaller
283 | font-weight: $font-weight-bold
284 | text-align: center
285 | background: $sidebarUnreadBg
286 |
287 | .unread:not(:empty)
288 | display: block
289 |
290 | .remove, .join
291 | position: absolute
292 | right: 10px
293 | top: 8px
294 | font-size: $font-size-base
295 | display: inline-block
296 | font-family: FontAwesome
297 | visibility: hidden
298 |
299 | button
300 | margin: -15px 0px 0px 5px
301 |
302 | .leaveRoom
303 | display: none
304 |
305 | .joinRoom
306 | display: inline-block
307 |
308 | &.joined
309 | .joinRoom
310 | display: none
311 |
312 | .leaveRoom
313 | display: inline-block
314 |
315 | #roster
316 | .wrap
317 | padding-left: 14px
318 | height: 23px;
319 |
320 | #bookmarks
321 | .wrap
322 | padding-left: 14px
323 | height: 23px
324 |
325 | @keyframes pulsate
326 | 0%
327 | opacity: 1.0
328 | 50%
329 | opacity: 0.5
330 | 100%
331 | opacity: 1.0
332 |
333 | #kaiwaNotice
334 | position: absolute
335 | bottom: 8px
336 | left: 50%
337 | width: 120px
338 | margin-left: -60px
339 | opacity: .4
340 |
341 | img
342 | width: 36px
343 | margin-left: 3px;
344 |
--------------------------------------------------------------------------------
/src/stylus/pages/settings.styl:
--------------------------------------------------------------------------------
1 | @import '../_variables'
2 | @import '../_mixins'
3 |
4 | // Settings
5 | .main
6 |
7 | h1
8 | color: #fff
9 | padding: 6px 12px
10 | margin: 0
11 | z-index: 101
12 | position: fixed
13 |
14 | > div
15 | padding: 20px
16 | border-bottom: 1px solid $gray-lighter
17 | h4
18 | color: $blue-saturated
19 |
20 | &:last-of-type
21 | border: none
22 |
23 | .status
24 | overflow: hidden
25 | min-height: 35px
26 | max-height: 50px
27 | height: auto
28 |
29 | .enableAlerts, .installFirefox, .soundNotifs
30 | margin-right: 5px
31 |
32 | .soundNotifs
33 | &.primary
34 | &:before
35 | content: 'Disable '
36 | &.secondary
37 | &:before
38 | content: 'Enable '
39 |
40 | .uploadRegion
41 | padding: 15px
42 | roundall(3px)
43 | margin: 50px 0
44 | border: 1px solid $gray-lighter
45 | background: lighten($gray-lighter, 80%)
46 |
47 | p
48 | margin: 0
49 |
50 | img
51 | margin: 10px 0
52 |
53 | .disconnect, .logout
54 | margin-right: 5px
55 |
--------------------------------------------------------------------------------
/src/stylus/pages/updateBar.styl:
--------------------------------------------------------------------------------
1 | @import '../_variables'
2 | @import '../_mixins'
3 |
4 | #updateBar
5 | position: fixed
6 | top: 0px
7 | left: 0px
8 | width: 100%
9 | height: 30px
10 | background: rgba(0, 0, 0, 0.8)
11 | z-index: 900
12 | transition: all .25s linear 0
13 | border-bottom: 1px solid #222
14 | text-align: center
15 | display: none
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": true,
3 | "compilerOptions": {
4 | "target": "es6",
5 | "sourceMap": true,
6 | "experimentalDecorators": true,
7 | "removeComments": true
8 | },
9 | "exclude": [
10 | "node_modules"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/tsd.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "v4",
3 | "repo": "borisyankov/DefinitelyTyped",
4 | "ref": "master",
5 | "path": "typings",
6 | "bundle": "typings/tsd.d.ts",
7 | "installed": {
8 | "jquery/jquery.d.ts": {
9 | "commit": "c3ab375b371b7b4204adee3b1209baecd81cec02"
10 | },
11 | "lodash/lodash.d.ts": {
12 | "commit": "17dd34132c2a1a4d6a8bf1816effcff3ec922034"
13 | },
14 | "node/node.d.ts": {
15 | "commit": "8ea42cd8bb11863ed6f242d67c502288ebc45a7b"
16 | },
17 | "async/async.d.ts": {
18 | "commit": "8ea42cd8bb11863ed6f242d67c502288ebc45a7b"
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/typings/async/async.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for Async 1.4.2
2 | // Project: https://github.com/caolan/async
3 | // Definitions by: Boris Yankov , Arseniy Maximov , Joe Herman
4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped
5 |
6 | interface Dictionary { [key: string]: T; }
7 |
8 | interface ErrorCallback { (err?: Error): void; }
9 | interface AsyncResultCallback { (err: Error, result: T): void; }
10 | interface AsyncResultArrayCallback { (err: Error, results: T[]): void; }
11 | interface AsyncResultObjectCallback { (err: Error, results: Dictionary): void; }
12 |
13 | interface AsyncFunction { (callback: (err?: Error, result?: T) => void): void; }
14 | interface AsyncIterator { (item: T, callback: ErrorCallback): void; }
15 | interface AsyncForEachOfIterator { (item: T, key: number, callback: ErrorCallback): void; }
16 | interface AsyncResultIterator { (item: T, callback: AsyncResultCallback): void; }
17 | interface AsyncMemoIterator { (memo: R, item: T, callback: AsyncResultCallback): void; }
18 | interface AsyncBooleanIterator { (item: T, callback: (truthValue: boolean) => void): void; }
19 |
20 | interface AsyncWorker { (task: T, callback: ErrorCallback): void; }
21 | interface AsyncVoidFunction { (callback: ErrorCallback): void; }
22 |
23 | interface AsyncQueue {
24 | length(): number;
25 | started: boolean;
26 | running(): number;
27 | idle(): boolean;
28 | concurrency: number;
29 | push(task: T, callback?: ErrorCallback): void;
30 | push(task: T[], callback?: ErrorCallback): void;
31 | unshift(task: T, callback?: ErrorCallback): void;
32 | unshift(task: T[], callback?: ErrorCallback): void;
33 | saturated: () => any;
34 | empty: () => any;
35 | drain: () => any;
36 | paused: boolean;
37 | pause(): void
38 | resume(): void;
39 | kill(): void;
40 | }
41 |
42 | interface AsyncPriorityQueue {
43 | length(): number;
44 | concurrency: number;
45 | started: boolean;
46 | paused: boolean;
47 | push(task: T, priority: number, callback?: AsyncResultArrayCallback): void;
48 | push(task: T[], priority: number, callback?: AsyncResultArrayCallback): void;
49 | saturated: () => any;
50 | empty: () => any;
51 | drain: () => any;
52 | running(): number;
53 | idle(): boolean;
54 | pause(): void;
55 | resume(): void;
56 | kill(): void;
57 | }
58 |
59 | interface AsyncCargo {
60 | length(): number;
61 | payload: number;
62 | push(task: any, callback? : Function): void;
63 | push(task: any[], callback? : Function): void;
64 | saturated(): void;
65 | empty(): void;
66 | drain(): void;
67 | idle(): boolean;
68 | pause(): void;
69 | resume(): void;
70 | kill(): void;
71 | }
72 |
73 | interface Async {
74 |
75 | // Collections
76 | each(arr: T[], iterator: AsyncIterator, callback?: ErrorCallback): void;
77 | eachSeries(arr: T[], iterator: AsyncIterator, callback?: ErrorCallback): void;
78 | eachLimit(arr: T[], limit: number, iterator: AsyncIterator, callback?: ErrorCallback): void;
79 | forEachOf(obj: any, iterator: (item: any, key: string|number, callback?: ErrorCallback) => void, callback: ErrorCallback): void;
80 | forEachOf(obj: T[], iterator: AsyncForEachOfIterator, callback?: ErrorCallback): void;
81 | forEachOfSeries(obj: any, iterator: (item: any, key: string|number, callback?: ErrorCallback) => void, callback: ErrorCallback): void;
82 | forEachOfSeries(obj: T[], iterator: AsyncForEachOfIterator, callback?: ErrorCallback): void;
83 | forEachOfLimit(obj: any, limit: number, iterator: (item: any, key: string|number, callback?: ErrorCallback) => void, callback: ErrorCallback): void;
84 | forEachOfLimit(obj: T[], limit: number, iterator: AsyncForEachOfIterator, callback?: ErrorCallback): void;
85 | map(arr: T[], iterator: AsyncResultIterator, callback?: AsyncResultArrayCallback): any;
86 | mapSeries(arr: T[], iterator: AsyncResultIterator, callback?: AsyncResultArrayCallback): any;
87 | mapLimit(arr: T[], limit: number, iterator: AsyncResultIterator, callback?: AsyncResultArrayCallback): any;
88 | filter(arr: T[], iterator: AsyncBooleanIterator, callback?: (results: T[]) => any): any;
89 | select(arr: T[], iterator: AsyncBooleanIterator, callback?: (results: T[]) => any): any;
90 | filterSeries(arr: T[], iterator: AsyncBooleanIterator, callback?: (results: T[]) => any): any;
91 | selectSeries(arr: T[], iterator: AsyncBooleanIterator, callback?: (results: T[]) => any): any;
92 | filterLimit(arr: T[], limit: number, iterator: AsyncBooleanIterator, callback?: (results: T[]) => any): any;
93 | selectLimit(arr: T[], limit: number, iterator: AsyncBooleanIterator, callback?: (results: T[]) => any): any;
94 | reject(arr: T[], iterator: AsyncBooleanIterator, callback?: (results: T[]) => any): any;
95 | rejectSeries(arr: T[], iterator: AsyncBooleanIterator, callback?: (results: T[]) => any): any;
96 | rejectLimit(arr: T[], limit: number, iterator: AsyncBooleanIterator, callback?: (results: T[]) => any): any;
97 | reduce(arr: T[], memo: R, iterator: AsyncMemoIterator, callback?: AsyncResultCallback): any;
98 | inject(arr: T[], memo: R, iterator: AsyncMemoIterator, callback?: AsyncResultCallback): any;
99 | foldl(arr: T[], memo: R, iterator: AsyncMemoIterator, callback?: AsyncResultCallback): any;
100 | reduceRight(arr: T[], memo: R, iterator: AsyncMemoIterator, callback: AsyncResultCallback): any;
101 | foldr(arr: T[], memo: R, iterator: AsyncMemoIterator, callback: AsyncResultCallback): any;
102 | detect(arr: T[], iterator: AsyncBooleanIterator, callback?: (result: T) => void): any;
103 | detectSeries(arr: T[], iterator: AsyncBooleanIterator, callback?: (result: T) => void): any;
104 | detectLimit(arr: T[], limit: number, iterator: AsyncBooleanIterator, callback?: (result: T) => void): any;
105 | sortBy(arr: T[], iterator: AsyncResultIterator, callback?: AsyncResultArrayCallback): any;
106 | some(arr: T[], iterator: AsyncBooleanIterator, callback?: (result: boolean) => void): any;
107 | someLimit(arr: T[], limit: number, iterator: AsyncBooleanIterator, callback?: (result: boolean) => void): any;
108 | any(arr: T[], iterator: AsyncBooleanIterator, callback?: (result: boolean) => void): any;
109 | every(arr: T[], iterator: AsyncBooleanIterator, callback?: (result: boolean) => any): any;
110 | everyLimit(arr: T[], limit: number, iterator: AsyncBooleanIterator, callback?: (result: boolean) => any): any;
111 | all(arr: T[], iterator: AsyncBooleanIterator, callback?: (result: boolean) => any): any;
112 | concat(arr: T[], iterator: AsyncResultIterator, callback?: AsyncResultArrayCallback): any;
113 | concatSeries(arr: T[], iterator: AsyncResultIterator, callback?: AsyncResultArrayCallback): any;
114 |
115 | // Control Flow
116 | series(tasks: AsyncFunction[], callback?: AsyncResultArrayCallback): void;
117 | series(tasks: Dictionary>, callback?: AsyncResultObjectCallback): void;
118 | parallel(tasks: Array>, callback?: AsyncResultArrayCallback): void;
119 | parallel(tasks: Dictionary>, callback?: AsyncResultObjectCallback): void;
120 | parallelLimit(tasks: Array>, limit: number, callback?: AsyncResultArrayCallback): void;
121 | parallelLimit(tasks: Dictionary>, limit: number, callback?: AsyncResultObjectCallback): void;
122 | whilst(test: () => boolean, fn: AsyncVoidFunction, callback: (err: any) => void): void;
123 | doWhilst(fn: AsyncVoidFunction, test: () => boolean, callback: (err: any) => void): void;
124 | until(test: () => boolean, fn: AsyncVoidFunction, callback: (err: any) => void): void;
125 | doUntil(fn: AsyncVoidFunction, test: () => boolean, callback: (err: any) => void): void;
126 | during(test: (testCallback : (error: Error, truth: boolean) => void) => void, fn: AsyncVoidFunction, callback: (err: any) => void): void;
127 | doDuring(fn: AsyncVoidFunction, test: (testCallback: (error: Error, truth: boolean) => void) => void, callback: (err: any) => void): void;
128 | forever(next: (errCallback : (err: Error) => void) => void, errBack: (err: Error) => void) : void;
129 | waterfall(tasks: Function[], callback?: (err: Error, results?: any) => void): void;
130 | compose(...fns: Function[]): void;
131 | seq(...fns: Function[]): void;
132 | applyEach(fns: Function[], argsAndCallback: any[]): void; // applyEach(fns, args..., callback). TS does not support ... for a middle argument. Callback is optional.
133 | applyEachSeries(fns: Function[], argsAndCallback: any[]): void; // applyEachSeries(fns, args..., callback). TS does not support ... for a middle argument. Callback is optional.
134 | queue(worker: AsyncWorker, concurrency?: number): AsyncQueue;
135 | priorityQueue(worker: AsyncWorker, concurrency: number): AsyncPriorityQueue;
136 | cargo(worker : (tasks: any[], callback : ErrorCallback) => void, payload? : number) : AsyncCargo;
137 | auto(tasks: any, callback?: (error: Error, results: any) => void): void;
138 | retry(opts: number, task: (callback : AsyncResultCallback, results: any) => void, callback: (error: Error, results: any) => void): void;
139 | retry(opts: { times: number, interval: number }, task: (callback: AsyncResultCallback, results : any) => void, callback: (error: Error, results: any) => void): void;
140 | iterator(tasks: Function[]): Function;
141 | apply(fn: Function, ...arguments: any[]): AsyncFunction;
142 | nextTick(callback: Function): void;
143 | setImmediate(callback: Function): void;
144 |
145 | times (n: number, iterator: AsyncResultIterator, callback: AsyncResultArrayCallback): void;
146 | timesSeries(n: number, iterator: AsyncResultIterator, callback: AsyncResultArrayCallback): void;
147 | timesLimit(n: number, limit: number, iterator: AsyncResultIterator, callback: AsyncResultArrayCallback): void;
148 |
149 | // Utils
150 | memoize(fn: Function, hasher?: Function): Function;
151 | unmemoize(fn: Function): Function;
152 | ensureAsync(fn: (... argsAndCallback: any[]) => void): Function;
153 | constant(...values: any[]): Function;
154 | asyncify(fn: Function): Function;
155 | wrapSync(fn: Function): Function;
156 | log(fn: Function, ...arguments: any[]): void;
157 | dir(fn: Function, ...arguments: any[]): void;
158 | noConflict(): Async;
159 | }
160 |
161 | declare var async: Async;
162 |
163 | declare module "async" {
164 | export = async;
165 | }
166 |
--------------------------------------------------------------------------------
/typings/tsd.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 |
3 | module.exports = {
4 | debug: true,
5 | devtool: 'cheap-source-map',
6 | resolve: {
7 | extensions: ['', '.ts', '.js', '.json', '.jade', '.html', '.less', '.css', '.json']
8 | },
9 |
10 | context: path.join(__dirname, 'src', 'js'),
11 |
12 | entry: {
13 | "0-babel-polyfill": 'babel-polyfill',
14 | "1-vendor":
15 | [
16 | require.resolve('jquery'),
17 | './libraries/resampler.js',
18 | require.resolve('indexeddbshim'),
19 | require.resolve('sugar-date'),
20 | './libraries/jquery.oembed.js'
21 | ],
22 | "app": "./app.ts"
23 | },
24 |
25 | output: {
26 | path: path.join(__dirname, 'public', 'js'),
27 | filename: '[name].js',
28 | chunkFilename: '[name].js'
29 | },
30 |
31 | stats: {
32 | colors: true,
33 | reasons: true
34 | },
35 |
36 | module: {
37 | loaders: [
38 | {
39 | test: require.resolve('jquery'),
40 | loader: "expose?$!expose?jQuery"
41 | },
42 | {
43 | test: /resampler\.js$/,
44 | loader: 'expose?Resample!imports?this=>window!exports?Resample'
45 | },
46 | {
47 | test: /jquery\.oembed\.js$/,
48 | loader: 'imports?jQuery=jquery'
49 | },
50 | {
51 | test: /\.ts(x?)$/,
52 | exclude: /(node_modules|bower_components|libraries)/,
53 | loader: 'babel!ts-loader'
54 | },
55 | {
56 | test: /\.js(x?)$/,
57 | exclude: /(node_modules|bower_components|libraries)/,
58 | loader: 'babel'
59 | },
60 | {
61 | test: /\.jade$/,
62 | loader: 'jade-loader'
63 | },
64 | {
65 | test: /\.json$/,
66 | loader: 'json-loader'
67 | }
68 | ]
69 | },
70 | node: {
71 | Buffer: true,
72 | console: true,
73 | global: true,
74 | fs: "empty"
75 | },
76 | externals: {
77 | jquery: 'jQuery'
78 | }
79 |
80 | };
--------------------------------------------------------------------------------