├── .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 [![Build Status](https://travis-ci.org/ForNeVeR/kaiwa.svg?branch=develop)](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 | ![Screenshot](http://getkaiwa.com/assets/img/header.png) 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 | }; --------------------------------------------------------------------------------