diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..49029e1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +# EditorConfig: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# all files +[*] +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.yml] +indent_size = 2 + +[*.yml.sample] +indent_size = 2 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..6c46b78 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +media +migrootions diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..4dc9815 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,20 @@ +{ + "env": { + "node": true + }, + + "rules": { + "no-underscore-dangle": 0, + "quotes": [2, "single"], + "strict": [2, "global"], + + "consistent-return": 0, + + // Disable certain checks until we have time to fix these issues + "no-shadow": 0, + "camelcase": 0, + "no-path-concat": 0, + "no-process-exit": 0, + "new-cap": 0 + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c245793 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +settings.yml +settings.js +node_modules +bower_components +npm-debug.log +.DS_Store +.idea +*._ +*.sw[opqr] +*.swap +.*.swp +*humbs.db +*.key +*.pem +*.crt +*.csr +coverage diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2bfe894 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +sudo: false + +language: node_js + +node_js: + - "0.10" + - "0.12" + - "4.0" + - "4.1" + - "4.2" + +env: + - CXX="g++-4.8" + +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 + - gcc-4.8 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6a18760 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,206 @@ +## 0.4.5 (2015-12-22) + +Fixes + +* Issues with NODE_PATH +* Fr language update + +## 0.4.4 (2015-10-26) + +Enhancements + +* Support for Node v4 + +## 0.4.3 (2015-10-09) + +Fixes + +* Issues with passport dependencies +* XMPP message ID + +Enhancements + +* New client styling +* Russian and Polish languages + +## 0.4.2 (2015-07-14) + +Fixes + +* Update dependencies + +Enhancements + +* Show version in the app +* Various accessibility additions + +## 0.4.1 (2015-07-08) + +Fixes + +* [Conversations](https://github.com/siacs/Conversations) double messaging +* Transcripts were not printable + +Enhancements + +* Giphy can be toggled in settings +* New translations: nl, de, pt, fr, jp, etc +* Various Docker improvements (config volume, etc) +* Better message time groupings + +## 0.4.0 (2015-06-04) + +Enhancements + +* Private and password-protected rooms +* Private 1-to-1 chat between XMPP users +* Giphy image search +* @all mentions are highlighted for everyone +* Basic server-side i18n support + +## 0.3.13 (2015-05-29) + +Fixes + +* Multiple layout bugs due to overflowing content in Firefox +* Layout issue introduced by Chrome 43 + +## 0.3.12 (2015-05-19) + +Fixes + +* Multiple layout bugs due to overflowing content in Firefox +* Layout issue introduced by Chrome 43 + +## 0.3.11 (2015-05-19) + +Fixes + +* Env variable loading +* Various notification adjustments + +Enhancements + +* Usernames may now contain underscores and dashes +* Unicode character support for links in messages +* Unread message counts in the favicon +* Newlines with `shift+enter` + +## 0.3.10 (2015-04-14) + +Fixes + +* Fixed Hyperlink parsing +* Fixed issue whereby a deleted user's messages would not display + +## 0.3.9 (2015-04-06) + +Deprecated + +* `xmpp.host` configuration setting - use `xmpp.domain` instead + +Enhancements + +* Thumbnails are improved +* Support multiple XMPP domains being used between clients +* XMPP can be configured to authenticate using full JID - instead of only node +* Added process title + +Fixes + +* Fixed "express deprecated req.host: Use req.hostname instead" +* Fixed File Uploads "Post" checkbox +* Fixed issues with desktop notifications +* Fixed XMPP MUC nickname not being reflected to user + +## 0.3.8 (2015-03-02) + +Fixes + +* Fixed error when passing an invalid room ID to transcript page +* Fixed problem where client would join a room twice + +## 0.3.7 (2015-03-01) + +Fixes + +* Fixed MongoDB version check + +## 0.3.6 (2015-02-28) + +Enhancements + +* Transcripts now support text search (#39) +* Plugins support environment variables (#254) + +Fixes + +* Long room names are clipped in the UI (#221) +* User details are updated in message list (#172) +* XMPP status changes no longer re-trigger room join event (#322) +* Fixed error when rooms in local storage are no longer available + +## 0.3.5 (2015-02-26) + +Fixes + +* Asset pipeline outputs relative paths (#208) +* Temporary upload files are cleaned up +* Fixed UI rendering performance issues +* Updated login registration form layout +* Fixed room archive bug +* XMPP fixes (chat history and client compatibility) + +## 0.3.4 (2015-02-19) + +Enhancements + +* OpenShift compatibility (#199) +* Added ability to serve robots.txt file + +Fixes + +* Fixed parsing of environment variables +* Disable autocomplete on password fields +* Upload modal only shows rooms you have joined + +## 0.3.3 (2015-02-14) + +Enhancements + +* XMPP users can create rooms (if enabled by configuration) +* Extra methods can be defined on XMPP message processors +* Support periods in usernames +* [Amazon S3](https://github.com/sdelements/lets-chat-s3) support has been extracted into a separate plugin + +Fixes + +* Auth token men option when XMPP was disabled (#235) +* Improved transcript date range picker +* Chat history didn't load when rejoining room (#242) +* Error messages not shown when creating a room (#229) + +## 0.3.2 (2015-02-12) + +Enhancements + +* Extracted [Kerberos](https://github.com/sdelements/lets-chat-kerberos) and [LDAP](https://github.com/sdelements/lets-chat-ldap) authentication into separate plugins +* Added fig.yml + +Fixes + +* Fixed error on messages andpoint when room parameter not specified (#211) +* Fixed undefined error (#210) +* XMPP root now advertises itself as a server, not conference (#214) + +## 0.3.1 (2015-02-10) + +Enhancements + +* Now using an [assets pipeline](https://github.com/adunkman/connect-assets) + +Fixes + +* Fixes related to file upload functionality + +## 0.3.0 (2015-02-06) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0603113 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,110 @@ +# Support / Issues + +Please [open an issue](https://github.com/sdelements/lets-chat/issues/new) for support and questions. + + +# Contributing to Let's Chat + +Looking to contribute something to Let's Chat? **Here's how you can help.** + +Please take a moment to review this document in order to make the contribution +process easy and effective for everyone involved. + +Following these guidelines helps to communicate that you respect the time of +the developers managing and developing this open source project. In return, +they should reciprocate that respect in addressing your issue or assessing +patches and features. + + +## Using the issue tracker + +Having trouble installing Let's Chat? Search for similar issues or post your new problem on the [mailing list][mailing-list]. + +The [issue tracker][tracker] is the preferred channel for [bug reports](#bug-reports), +[features requests](#feature-requests) and [submitting pull requests](#pull-requests), +but please respect the following restrictions: + + +* Please provide as much details about the problem you are encountering (platform, version on node/npm/mongo, etc) + +* Please **do not** derail or troll issues. Keep the discussion on topic and + respect the opinions of others. + + +## Issues and labels + +Our bug tracker utilizes several labels to help organize and identify issues. +They do not signal any commitment from us to deliver. Here's what they +represent and how we use them: + +#### Labels + +- `bug` - Bugs that are reported to us or found by us. +- `feature` - Issues asking for a new feature to be added, or an existing one + to be extended or modified. + + +- `duplicate` - Issues that duplicate an already existing issue. +- `invalid` - Issues that are no longer valid. +- `wontfix` - Issues that are no longer valid. + + +- `xmpp` - Issues that are related to XMPP functionality. + +For a complete look at our labels, see the [project labels page][labels]. + +#### Milestones + +- `backlog` - Issues that are we are not committing to yet, but may be nice to + have in the future. + + +## Bug reports + +A bug is a _demonstrable problem_ that is caused by the code in the repository. +Good bug reports are extremely helpful, so thanks! + +Guidelines for bug reports: + +0. **Search both GitHub issue and the mailing list**. + +2. **Check if the issue has been fixed** — try to reproduce it using the + latest `master` or development branch in the repository. + + +A good bug report shouldn't leave others needing to chase you up for more +information. Please try to be as detailed as possible in your report. What is +your environment? What steps will reproduce the issue? What browser(s) and OS +experience the problem? What +would you expect to be the outcome? All these details will help people to fix +any potential bugs. + + +## Feature requests + +Feature requests are welcome. But take a moment to find out whether your idea +fits with the scope and aims of the project. It's up to *you* to make a strong +case to convince the project's developers of the merits of this feature. Please +provide as much detail and context as possible. + + +## Pull requests + +Good pull requests—patches, improvements, new features—are a fantastic +help. They should remain focused in scope and avoid containing unrelated +commits. + +**Please ask first** before embarking on any significant pull request (e.g. +implementing features, refactoring code, porting to a different language), +otherwise you risk spending a lot of time working on something that the +project's developers might not want to merge into the project. + + +## License + +By contributing your code, you agree to license your contribution under the +[MIT License](LICENSE). + + +[labels]: https://github.com/sdelements/lets-chat/labels +[tracker]: https://github.com/sdelements/lets-chat/issues diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1f71686 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM node:0.10-slim + + +COPY ./package.json /src/package.json +RUN cd /src && npm install +COPY ./ /src +RUN npm install -g mocha +RUN npm install -g istanbul +RUN npm install -g gulp + +WORKDIR /src +#ENV DEBUG=* + +EXPOSE 8080 5222 + +CMD ["npm", "start"] diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..272eccf --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,23 @@ +// +// Gruntfile +// + +'use strict'; + +module.exports = function(grunt) { + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + bower: { + install: { + options: { + install: true, + cleanTargetDir: true, + verbose: true, + targetDir: 'media/js/vendor', + layout: 'byComponent' + } + } + } + }); + grunt.loadNpmTasks('grunt-bower-task'); +}; diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7fc104a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2012-2015 Houssam Haidar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..063b78f --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: npm start diff --git a/README.md b/README.md new file mode 100644 index 0000000..a6d6081 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +![Let's Chat](http://i.imgur.com/0a3l5VF.png) + +![Screenshot](http://i.imgur.com/C4uMD67.png) +Test +A self-hosted chat app for small teams or big Gal by [Security Compass][seccom]. + +[![Build Status](https://travis-ci.org/sdelements/lets-chat.svg?branch=master)](https://travis-ci.org/sdelements/lets-chat) +[![Dependency Status](https://david-dm.org/sdelements/lets-chat.svg)](https://david-dm.org/sdelements/lets-chat) +[![devDependency Status](https://david-dm.org/sdelements/lets-chat/dev-status.svg)](https://david-dm.org/sdelements/lets-chat#info=devDependencies) + +## Features and Stuff + +* BYOS (bring your own server) +* Persistent messages +* Multiple rooms +* Private and password-protected rooms +* New message alerts / notifications +* Mentions (hey @you/@all) +* Image embeds / Giphy search +* Code pasting +* File uploads (Local / [Amazon S3][s3] / [Azure][azure]) +* Transcripts / Chat History (with search) +* XMPP Multi-user chat (MUC) +* 1-to-1 chat between XMPP users +* Local / [Kerberos][kerberos] / [LDAP][ldap] authentication +* [Hubot Adapter][hubot] +* REST-like API +* Basic i18n support +* MIT Licensed + + +## Deployment + +For installation instructions, please use the following links: + +* [Local installation][install-local] +* [Docker][install-docker] +* [Heroku][install-heroku] +* [Vagrant][install-vagrant] + +## Support & Problems + +We have a [troubleshooting document][troubleshooting], otherwise please use our +[mailing list][mailing-list] for support issues and questions. + + +## Bugs and feature requests + +Have a bug or a feature request? Please first read the [issue +guidelines][contributing] and search for existing and closed issues. If your +problem or idea is not addressed yet, [please open a new issue][new-issue]. + + +## Documentation + +Let's Chat documentation is hosted in the [wiki]. If there is an inaccuracy in +the documentation, [please open a new issue][new-issue]. + + +## Contributing + +Please read through our [contributing guidelines][contributing]. Included are +directions for opening issues, coding standards, and notes on development. + +Editor preferences are available in the [editor config][editorconfig] for easy +use in common text editors. Read more and download plugins at +. + + +## License + +Released under [the MIT license][license]. + + +[wiki]: https://github.com/sdelements/lets-chat/wiki +[troubleshooting]: https://github.com/sdelements/lets-chat/blob/master/TROUBLESHOOTING.md +[mailing-list]: https://groups.google.com/forum/#!forum/lets-chat-app +[tracker]: https://github.com/sdelements/lets-chat/issues +[contributing]: https://github.com/sdelements/lets-chat/blob/master/CONTRIBUTING.md +[new-issue]: https://github.com/sdelements/lets-chat/issues/new +[editorconfig]: https://github.com/sdelements/lets-chat/blob/master/.editorconfig +[license]: https://github.com/sdelements/lets-chat/blob/master/LICENSE +[ldap]: https://github.com/sdelements/lets-chat-ldap +[kerberos]: https://github.com/sdelements/lets-chat-kerberos +[s3]: https://github.com/sdelements/lets-chat-s3 +[seccom]: http://securitycompass.com/ +[hubot]: https://github.com/sdelements/hubot-lets-chat +[azure]: https://github.com/maximilian-krauss/lets-chat-azure +[install-local]: https://github.com/sdelements/lets-chat/wiki/Installation +[install-docker]: https://registry.hub.docker.com/u/sdelements/lets-chat/ +[install-heroku]: https://github.com/sdelements/lets-chat/wiki/Heroku +[install-vagrant]: https://github.com/sdelements/lets-chat/wiki/Vagrant diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..0f37673 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,11 @@ +# Common Issues + +We'll be improving this document soon. + +#### What version of Node.JS is required? + +Let's Chat requires Node.JS version ```0.10.x``` or ```0.12.x```. You will have a bad time if you try to use a different version. + +#### Do I need MongoDB running? + +Yes. Please refer to [MongoDB's documentation](http://docs.mongodb.org/manual/). diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..561c01d --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,44 @@ +# Lets Chat Vagrant file + +# Set LCB_BRANCH to pick what git checkout to use when spinning up the +# application. For example: +# +# $ LCB_BRANCH="feature-branch vagrant up. +# +LCB_BRANCH = ENV['LCB_BRANCH'] || 'master' + + +# Script that we run to bootstrap the system to run Let's Chat +LCB_SCRIPT = < LCB_SCRIPT, :privileged => false + end + + config.vm.provider "virtualbox" do |v| + v.gui = true + v.name = "Lets Chat" + v.memory = 1024 + end +end diff --git a/_sources/favicon.psd b/_sources/favicon.psd new file mode 100644 index 0000000..a8ebfb6 Binary files /dev/null and b/_sources/favicon.psd differ diff --git a/_sources/icon.psd b/_sources/icon.psd new file mode 100644 index 0000000..b423bd7 Binary files /dev/null and b/_sources/icon.psd differ diff --git a/_sources/logo.psd b/_sources/logo.psd new file mode 100644 index 0000000..ab023e9 Binary files /dev/null and b/_sources/logo.psd differ diff --git a/_sources/pattern.psd b/_sources/pattern.psd new file mode 100644 index 0000000..5494052 Binary files /dev/null and b/_sources/pattern.psd differ diff --git a/app.js b/app.js new file mode 100644 index 0000000..0893b11 --- /dev/null +++ b/app.js @@ -0,0 +1,287 @@ +// +// Let's Chat +// + +'use strict'; + +process.title = 'letschat'; + +require('colors'); + +var _ = require('lodash'), + path = require('path'), + fs = require('fs'), + express = require('express.oi'), + i18n = require('i18n'), + bodyParser = require('body-parser'), + cookieParser = require('cookie-parser'), + compression = require('compression'), + helmet = require('helmet'), + http = require('http'), + nunjucks = require('nunjucks'), + mongoose = require('mongoose'), + migroose = require('./migroose'), + connectMongo = require('connect-mongo'), + all = require('require-tree'), + psjon = require('./package.json'), + settings = require('./app/config'), + auth = require('./app/auth/index'), + core = require('./app/core/index'); + +var MongoStore = connectMongo(express.session), + httpEnabled = settings.http && settings.http.enable, + httpsEnabled = settings.https && settings.https.enable, + models = all(path.resolve('./app/models')), + middlewares = all(path.resolve('./app/middlewares')), + controllers = all(path.resolve('./app/controllers')), + app; + +// +// express.oi Setup +// +if (httpsEnabled) { + app = express().https({ + key: fs.readFileSync(settings.https.key), + cert: fs.readFileSync(settings.https.cert) + }).io(); +} else { + app = express().http().io(); +} + +if (settings.env === 'production') { + app.set('env', settings.env); + app.set('json spaces', undefined); + app.enable('view cache'); +} + +// Session +var sessionStore = new MongoStore({ + url: settings.database.uri, + autoReconnect: true +}); + +// Session +var session = { + key: 'connect.sid', + secret: settings.secrets.cookie, + store: sessionStore, + cookie: { secure: httpsEnabled }, + resave: false, + saveUninitialized: true +}; + +// Set compression before any routes +app.use(compression({ threshold: 512 })); + +app.use(cookieParser()); +app.io.session(session); + +auth.setup(app, session, core); + +// Security protections +app.use(helmet.frameguard()); +app.use(helmet.hidePoweredBy()); +app.use(helmet.ieNoOpen()); +app.use(helmet.noSniff()); +app.use(helmet.xssFilter()); +app.use(helmet.hsts({ + maxAge: 31536000, + includeSubdomains: true, + force: httpsEnabled, + preload: true +})); +app.use(helmet.contentSecurityPolicy({ + defaultSrc: ['\'none\''], + connectSrc: ['*'], + scriptSrc: ['\'self\'', '\'unsafe-eval\''], + styleSrc: ['\'self\'', 'fonts.googleapis.com', '\'unsafe-inline\''], + fontSrc: ['\'self\'', 'fonts.gstatic.com'], + mediaSrc: ['\'self\''], + objectSrc: ['\'self\''], + imgSrc: ['*'] +})); + +var bundles = {}; +app.use(require('connect-assets')({ + paths: [ + 'media/js', + 'media/less' + ], + helperContext: bundles, + build: settings.env === 'production', + fingerprinting: settings.env === 'production', + servePath: 'media/dist' +})); + +// Public +app.use('/media', express.static(__dirname + '/media', { + maxAge: '364d' +})); + +// Templates +var nun = nunjucks.configure('templates', { + autoescape: true, + express: app, + tags: { + blockStart: '<%', + blockEnd: '%>', + variableStart: '<$', + variableEnd: '$>', + commentStart: '<#', + commentEnd: '#>' + } +}); + +function wrapBundler(func) { + // This method ensures all assets paths start with "./" + // Making them relative, and not absolute + return function() { + return func.apply(func, arguments) + .replace(/href="\//g, 'href="./') + .replace(/src="\//g, 'src="./'); + }; +} + +nun.addFilter('js', wrapBundler(bundles.js)); +nun.addFilter('css', wrapBundler(bundles.css)); +nun.addGlobal('text_search', false); + +// i18n +i18n.configure({ + directory: __dirname + '/locales', + defaultLocale: settings.i18n && settings.i18n.locale || 'en' +}); +app.use(i18n.init); + +// HTTP Middlewares +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ + extended: true +})); + +// IE header +app.use(function(req, res, next) { + res.setHeader('X-UA-Compatible', 'IE=Edge,chrome=1'); + next(); +}); + +// +// Controllers +// +_.each(controllers, function(controller) { + controller.apply({ + app: app, + core: core, + settings: settings, + middlewares: middlewares, + models: models, + controllers: controllers + }); +}); + +// +// Mongo +// + +mongoose.connection.on('error', function (err) { + throw new Error(err); +}); + +mongoose.connection.on('disconnected', function() { + throw new Error('Could not connect to database'); +}); + +// +// Go Time +// + +function startApp() { + var port = httpsEnabled && settings.https.port || + httpEnabled && settings.http.port; + + var host = httpsEnabled && settings.https.host || + httpEnabled && settings.http.host || '0.0.0.0'; + + + + if (httpsEnabled && httpEnabled) { + // Create an HTTP -> HTTPS redirect server + var redirectServer = express(); + redirectServer.get('*', function(req, res) { + var urlPort = port === 80 ? '' : ':' + port; + res.redirect('https://' + req.hostname + urlPort + req.path); + }); + http.createServer(redirectServer) + .listen(settings.http.port || 5000, host); + } + + app.listen(port, host); + + // + // XMPP + // + if (settings.xmpp.enable) { + var xmpp = require('./app/xmpp/index'); + xmpp(core); + } + + var art = fs.readFileSync('./app/misc/art.txt', 'utf8'); + console.log('\n' + art + '\n\n' + 'Release ' + psjon.version.yellow + '\n'); +} + +function checkForMongoTextSearch() { + if (!mongoose.mongo || !mongoose.mongo.Admin) { + // MongoDB API has changed, assume text search is enabled + nun.addGlobal('text_search', true); + return; + } + console.log(JSON.stringify(mongoose.connection.db)); + var admin = new mongoose.mongo.Admin(mongoose.connection.db); + admin.buildInfo(function (err, info) { + if (err || !info) { + return; + } + + var version = info.version.split('.'); + if (version.length < 2) { + return; + } + + if(version[0] < 2) { + return; + } + + if(version[0] === '2' && version[1] < 6) { + return; + } + + nun.addGlobal('text_search', true); + }); +} + +mongoose.connect(settings.database.uri, function(err) { + if (err) { + throw err; + } + + checkForMongoTextSearch(); + + migroose.needsMigration(function(err, migrationRequired) { + if (err) { + console.error(err); + } + + else if (migrationRequired) { + console.log('Database migration required'.red); + console.log('Ensure you backup your database first.'); + console.log(''); + console.log( + 'Run the following command: ' + 'npm run migrate'.yellow + ); + + return process.exit(); + } + + startApp(); + }); +}); diff --git a/app.json b/app.json new file mode 100644 index 0000000..c7d4dda --- /dev/null +++ b/app.json @@ -0,0 +1,24 @@ +{ + "name": "Let's Chat", + "description": "Self-hosted chat app for small teams", + "repository": "https://github.com/sdelements/lets-chat", + "logo": "http://i.imgur.com/eXTLHNN.png", + "keywords": [ + "lets", + "chat", + "xmpp" + ], + "addons": [ + "mongolab" + ], + "env": { + "NODE_ENV": { + "description": "Configures Express for production", + "value": "production" + }, + "LCB_SECRETS_COOKIE": { + "description": "Session cookies are signed with this secret to prevent tampering", + "generator": "secret" + } + } +} diff --git a/app/auth/index.js b/app/auth/index.js new file mode 100644 index 0000000..8ffcfd3 --- /dev/null +++ b/app/auth/index.js @@ -0,0 +1,228 @@ +'use strict'; + +var _ = require('lodash'), + async = require('async'), + cookieParser = require('cookie-parser'), + mongoose = require('mongoose'), + passport = require('passport'), + passportSocketIo = require('passport.socketio'), + BearerStrategy = require('passport-http-bearer'), + BasicStrategy = require('passport-http').BasicStrategy, + settings = require('./../config'), + plugins = require('./../plugins'); + +var providerSettings = {}, + MAX_AUTH_DELAY_TIME = 24 * 60 * 60 * 1000, + loginAttempts = {}, + enabledProviders = []; + +function getProviders(core) { + return settings.auth.providers.map(function(key) { + var Provider; + + if (key === 'local') { + Provider = require('./local'); + } else { + Provider = plugins.getPlugin(key, 'auth'); + } + + return { + key: key, + provider: new Provider(settings.auth[key], core) + }; + }); +} + +function setup(app, session, core) { + + enabledProviders = getProviders(core); + + enabledProviders.forEach(function(p) { + p.provider.setup(); + providerSettings[p.key] = p.provider.options; + }); + + function tokenAuth(username, password, done) { + if (!done) { + done = password; + } + + var User = mongoose.model('User'); + User.findByToken(username, function(err, user) { + if (err) { return done(err); } + if (!user) { return done(null, false); } + return done(null, user); + }); + } + + passport.use(new BearerStrategy(tokenAuth)); + passport.use(new BasicStrategy(tokenAuth)); + + passport.serializeUser(function(user, done) { + done(null, user._id); + }); + + passport.deserializeUser(function(id, done) { + var User = mongoose.model('User'); + User.findOne({ _id: id }, function(err, user) { + done(err, user); + }); + }); + + app.use(passport.initialize()); + app.use(passport.session()); + + session = _.extend(session, { + cookieParser: cookieParser, + passport: passport + }); + + var psiAuth = passportSocketIo.authorize(session); + + app.io.use(function (socket, next) { + var User = mongoose.model('User'); + if (socket.request._query && socket.request._query.token) { + User.findByToken(socket.request._query.token, function(err, user) { + if (err || !user) { + return next('Fail'); + } + + socket.request.user = user; + socket.request.user.loggedIn = true; + socket.request.user.usingToken = true; + next(); + }); + } else { + psiAuth(socket, next); + } + + }); +} + +function checkIfAccountLocked(username, cb) { + var attempt = loginAttempts[username]; + var isLocked = attempt && + attempt.lockedUntil && + attempt.lockedUntil > Date.now(); + + cb(isLocked); +} + +function wrapAuthCallback(username, cb) { + return function(err, user, info) { + if (!err && !user) { + + if(!loginAttempts[username]) { + loginAttempts[username] = { + attempts: 0, + lockedUntil: null + }; + } + + var attempt = loginAttempts[username]; + + attempt.attempts++; + + if (attempt.attempts >= settings.auth.throttling.threshold) { + var lock = Math.min(5000 * Math.pow(2, (attempt.attempts - settings.auth.throttling.threshold), MAX_AUTH_DELAY_TIME)); + attempt.lockedUntil = Date.now() + lock; + return cb(err, user, { + locked: true, + message: 'Account is locked.' + }); + } + + return cb(err, user, info); + + } else { + + if(loginAttempts[username]) { + delete loginAttempts[username]; + } + cb(err, user, info); + } + }; +} + +function authenticate() { + var req, username, cb; + + if (arguments.length === 4) { + username = arguments[1]; + + } else if (arguments.length === 3) { + username = arguments[0]; + + } else { + username = arguments[0].body.username; + } + + username = username.toLowerCase(); + + if (arguments.length === 4) { + req = _.extend({}, arguments[0], { + body: { + username: username, + password: arguments[2] + } + }); + cb = arguments[3]; + + } else if (arguments.length === 3) { + req = { + body: { + username: username, + password: arguments[1] + } + }; + cb = arguments[2]; + + } else { + req = _.extend({}, arguments[0]); + req.body.username = username; + cb = arguments[1]; + } + + checkIfAccountLocked(username, function(locked) { + if (locked) { + return cb(null, null, { + locked: true, + message: 'Account is locked.' + }); + } + + if (settings.auth.throttling && + settings.auth.throttling.enable) { + cb = wrapAuthCallback(username, cb); + } + + var series = enabledProviders.map(function(p) { + var provider = p.provider; + return function() { + var args = Array.prototype.slice.call(arguments); + var callback = args.slice(args.length - 1)[0]; + + if (args.length > 1 && args[0]) { + return callback(null, args[0]); + } + + provider.authenticate(req, function(err, user) { + if (err) { + return callback(err); + } + return callback(null, user); + }); + }; + }); + + async.waterfall(series, function(err, user) { + cb(err, user); + }); + }); +} + +module.exports = { + setup: setup, + authenticate: authenticate, + providers: providerSettings +}; diff --git a/app/auth/local.js b/app/auth/local.js new file mode 100644 index 0000000..15854cf --- /dev/null +++ b/app/auth/local.js @@ -0,0 +1,41 @@ +'use strict'; + +var mongoose = require('mongoose'), + passport = require('passport'), + LocalStrategy = require('passport-local').Strategy; + +function Local(options) { + this.options = options; + this.key = 'local'; +} + +Local.key = 'local'; + +Local.prototype.setup = function() { + passport.use(new LocalStrategy({ + usernameField: 'username', + passwordField: 'password' + }, function(identifier, password, done) { + var User = mongoose.model('User'); + User.authenticate(identifier, password, function(err, user) { + if (err) { + return done(null, false, { + message: 'Some fields did not validate.' + }); + } + if (user) { + return done(null, user); + } else { + return done(null, null, { + message: 'Incorrect login credentials.' + }); + } + }); + })); +}; + +Local.prototype.authenticate = function(req, cb) { + passport.authenticate('local', cb)(req); +}; + +module.exports = Local; diff --git a/app/config.js b/app/config.js new file mode 100644 index 0000000..1f2c7b0 --- /dev/null +++ b/app/config.js @@ -0,0 +1,192 @@ + + +var _ = require('lodash'), + fs = require('fs'), + yaml = require('js-yaml'), + plugins = require('./plugins'); +var path = require('path'); +var debug = require('debug')('config readers'); + +function parseEnvValue(value, isArray) { + value = value.trim(); + if (isArray) {s + return _.map(value.split(','), function(value) { + return parseEnvValue(value); + }); + } + // YAML compatible boolean values + else if (/^(y|yes|true|on)$/i.test(value)) { + return true; + } + else if (/^(n|no|false|off)$/i.test(value)) { + return false; + } + else if (/^[+-]?\d+.?\d*$/.test(value) && + !isNaN(parseInt(value, 10))) { + return parseInt(value, 10); + } + return value; +} + +var pipeline = [ + + function getDefaultSettings(context) { + var execPath = process.env.CWD || process.cwd(); + var defaultsPath = path.resolve(execPath,'./defaults.yml'); + var file = fs.readFileSync(defaultsPath, 'utf8'); + context.defaults = yaml.safeLoad(file); + + console.log(JSON.stringify(context.defaults)); + }, + + function getFileSettings(context) { + var file; + if (fs.existsSync('config/settings.yml')) { + file = fs.readFileSync('config/settings.yml', 'utf8'); + context.file = yaml.safeLoad(file) || {}; + } else if (fs.existsSync('settings.yml')) { + file = fs.readFileSync('settings.yml', 'utf8'); + context.file = yaml.safeLoad(file) || {}; + } else { + context.file = {}; + } + }, + + function getFilePlugin(context) { + var provider = process.env.LCB_FILES_PROVIDER || + context.file.files && context.file.files.provider || + context.defaults.files && context.defaults.files.provider; + + context.plugins.files = [ provider ]; + }, + + function getAuthPlugins(context) { + var providers = []; + var env = process.env.LCB_AUTH_PROVIDERS; + if (env) { + providers = parseEnvValue(env, true); + } else { + providers = context.file.auth && context.file.auth.providers || + context.defaults.auth && context.defaults.auth.providers; + } + + context.plugins.auth = providers; + }, + + function getFilePluginDefaults(context) { + _.each(context.plugins.files, function(key) { + if (key === 'local') { + return; + } + + var plugin = plugins.getPlugin(key, 'files'); + + if (!plugin || !plugin.defaults) { + return; + } + + context.defaults.files[key] = plugin.defaults; + }); + }, + + function getAuthPluginDefaults(context) { + _.each(context.plugins.auth, function(key) { + if (key === 'local') { + return; + } + + var plugin = plugins.getPlugin(key, 'auth'); + + if (!plugin || !plugin.defaults) { + return; + } + + context.defaults.auth[key] = plugin.defaults; + }); + }, + + function mergeDefaultAndFileSettings(context) { + context.result = _.merge(context.defaults, context.file); + }, + + function mergeEnvSettings(context) { + function recurse(baseKey, object) { + _.forEach(object, function(value, key) { + var envKey = baseKey + '_' + + key.replace(/([A-Z]+)/g, '_$1').toUpperCase(); + if (_.isPlainObject(value)) { + recurse(envKey, value); + } else { + var val = process.env[envKey]; + if (val) { + object[key] = parseEnvValue(val, + _.isArray(object[key])); + } + } + }); + } + + recurse('LCB', context.result); + }, + + function addXmppConfHost(context) { + // Deprecating xmpp.host in favour of xmpp.domain + if (context.result.xmpp.host) { + console.log('DEPRECATED: xmpp.host setting has been deprecated, please use xmpp.domain instead'); + context.result.xmpp.domain = context.result.xmpp.host; + } + + if (context.result.xmpp.domain) { + context.result.xmpp.confdomain = 'conference.' + + context.result.xmpp.domain; + } + }, + + function overrideEnvSetting(context) { + if (process.env.NODE_ENV) { + context.result.env = process.env.NODE_ENV; + } + }, + + function overridePortSetting(context) { + if (process.env.PORT) { + context.result.http.port = process.env.PORT; + } + }, + + function herokuDbUrl(context) { + // Override database URI - if using a Heroku add-on + if (process.env.MONGOHQ_URL) { + context.result.database.uri = process.env.MONGOHQ_URL; + } + if (process.env.MONGOLAB_URI) { + context.result.database.uri = process.env.MONGOLAB_URI; + } + if (process.env.MONGO_DOCKER) { + context.result.database.uri = process.env.MONGO_DOCKER; + } + }, + + function openShift(context) { + if (process.env.OPENSHIFT_APP_NAME) { + context.result.http.host = process.env.OPENSHIFT_NODEJS_IP; + context.result.http.port = process.env.OPENSHIFT_NODEJS_PORT; + context.result.database.uri = process.env.OPENSHIFT_MONGODB_DB_URL + + process.env.OPENSHIFT_APP_NAME; + + } + } +]; + +var context = { + plugins: {}, + export: {} +}; + +_.each(pipeline, function(step) { + step(context); +}); + +console.log(JSON.stringify(context.result)); + +module.exports = context.result; diff --git a/app/controllers/account.js b/app/controllers/account.js new file mode 100644 index 0000000..68287fc --- /dev/null +++ b/app/controllers/account.js @@ -0,0 +1,326 @@ +// +// Account Controller +// + +'use strict'; + +var _ = require('lodash'), + fs = require('fs'), + psjon = require('./../../package.json'), + auth = require('./../auth/index'), + path = require('path'), + settings = require('./../config'); + +module.exports = function() { + + var app = this.app, + core = this.core, + middlewares = this.middlewares; + + core.on('account:update', function(data) { + app.io.emit('users:update', data.user); + }); + + // + // Routes + // + app.get('/', middlewares.requireLogin.redirect, function(req, res) { + res.render('chat.html', { + account: req.user, + settings: settings, + version: psjon.version + }); + }); + + app.get('/login', function(req, res) { + var imagePath = path.resolve('media/img/photos'); + var images = fs.readdirSync(imagePath); + var image = _.chain(images).filter(function(file) { + return /\.(gif|jpg|jpeg|png)$/i.test(file); + }).sample().value(); + res.render('login.html', { + photo: image, + auth: auth.providers + }); + }); + + app.get('/logout', function(req, res ) { + req.session.destroy(); + res.redirect('/login'); + }); + + app.post('/account/login', function(req) { + req.io.route('account:login'); + }); + + app.post('/account/register', function(req) { + req.io.route('account:register'); + }); + + app.get('/account', middlewares.requireLogin, function(req) { + req.io.route('account:whoami'); + }); + + app.post('/account/profile', middlewares.requireLogin, function(req) { + req.io.route('account:profile'); + }); + + app.post('/account/settings', middlewares.requireLogin, function(req) { + req.io.route('account:settings'); + }); + + app.post('/account/token/generate', middlewares.requireLogin, function(req) { + req.io.route('account:generate_token'); + }); + + app.post('/account/token/revoke', middlewares.requireLogin, function(req) { + req.io.route('account:revoke_token'); + }); + + // + // Sockets + // + app.io.route('account', { + whoami: function(req, res) { + res.json(req.user); + }, + profile: function(req, res) { + var form = req.body || req.data, + data = { + displayName: form.displayName || form['display-name'], + firstName: form.firstName || form['first-name'], + lastName: form.lastName || form['last-name'] + }; + + core.account.update(req.user._id, data, function (err, user) { + if (err) { + return res.json({ + status: 'error', + message: 'Unable to update your profile.', + errors: err + }); + } + + if (!user) { + return res.sendStatus(404); + } + + res.json(user); + }); + }, + settings: function(req, res) { + if (req.user.usingToken) { + return res.status(403).json({ + status: 'error', + message: 'Cannot change account settings ' + + 'when using token authentication.' + }); + } + + var form = req.body || req.data, + data = { + username: form.username, + email: form.email, + currentPassword: form.password || + form['current-password'] || form.currentPassword, + newPassword: form['new-password'] || form.newPassword, + confirmPassowrd: form['confirm-password'] || + form.confirmPassword + }; + + auth.authenticate(req, req.user.uid || req.user.username, + data.currentPassword, function(err, user) { + if (err) { + return res.status(400).json({ + status: 'error', + message: 'There were problems authenticating you.', + errors: err + }); + } + + if (!user) { + return res.status(401).json({ + status: 'error', + message: 'Incorrect login credentials.' + }); + } + + core.account.update(req.user._id, data, function (err, user, reason) { + if (err || !user) { + return res.status(400).json({ + status: 'error', + message: 'Unable to update your account.', + reason: reason, + errors: err + }); + } + res.json(user); + }); + }); + }, + generate_token: function(req, res) { + if (req.user.usingToken) { + return res.status(403).json({ + status: 'error', + message: 'Cannot generate a new token ' + + 'when using token authentication.' + }); + } + + core.account.generateToken(req.user._id, function (err, token) { + if (err) { + return res.json({ + status: 'error', + message: 'Unable to generate a token.', + errors: err + }); + } + + res.json({ + status: 'success', + message: 'Token generated.', + token: token + }); + }); + }, + revoke_token: function(req, res) { + if (req.user.usingToken) { + return res.status(403).json({ + status: 'error', + message: 'Cannot revoke token ' + + 'when using token authentication.' + }); + } + + core.account.revokeToken(req.user._id, function (err) { + if (err) { + return res.json({ + status: 'error', + message: 'Unable to revoke token.', + errors: err + }); + } + + res.json({ + status: 'success', + message: 'Token revoked.' + }); + }); + }, + register: function(req, res) { + + if (req.user || + !auth.providers.local || + !auth.providers.local.enableRegistration) { + + return res.status(403).json({ + status: 'error', + message: 'Permission denied' + }); + } + + var fields = req.body || req.data; + + // Sanity check the password + var passwordConfirm = fields.passwordConfirm || fields.passwordconfirm || fields['password-confirm']; + + if (fields.password !== passwordConfirm) { + return res.status(400).json({ + status: 'error', + message: 'Password not confirmed' + }); + } + + var data = { + provider: 'local', + username: fields.username, + email: fields.email, + password: fields.password, + firstName: fields.firstName || fields.firstname || fields['first-name'], + lastName: fields.lastName || fields.lastname || fields['last-name'], + displayName: fields.displayName || fields.displayname || fields['display-name'] + }; + + core.account.create('local', data, function(err) { + if (err) { + var message = 'Sorry, we could not process your request'; + // User already exists + if (err.code === 11000) { + message = 'Email has already been taken'; + } + // Invalid username + if (err.errors) { + message = _.map(err.errors, function(error) { + return error.message; + }).join(' '); + // If all else fails... + } else { + console.error(err); + } + // Notify + return res.status(400).json({ + status: 'error', + message: message + }); + } + + res.status(201).json({ + status: 'success', + message: 'You\'ve been registered, ' + + 'please try logging in now!' + }); + }); + }, + login: function(req, res) { + auth.authenticate(req, function(err, user, info) { + if (err) { + return res.status(400).json({ + status: 'error', + message: 'There were problems logging you in.', + errors: err + }); + } + + if (!user && info && info.locked) { + return res.status(403).json({ + status: 'error', + message: info.message || 'Account is locked.' + }); + } + + if (!user) { + return res.status(401).json({ + status: 'error', + message: info && info.message || + 'Incorrect login credentials.' + }); + } + + req.login(user, function(err) { + if (err) { + return res.status(400).json({ + status: 'error', + message: 'There were problems logging you in.', + errors: err + }); + } + var temp = req.session.passport; + req.session.regenerate(function(err) { + if (err) { + return res.status(400).json({ + status: 'error', + message: 'There were problems logging you in.', + errors: err + }); + } + req.session.passport = temp; + res.json({ + status: 'success', + message: 'Logging you in...' + }); + }); + }); + }); + } + }); +}; diff --git a/app/controllers/connections.js b/app/controllers/connections.js new file mode 100644 index 0000000..fb0b562 --- /dev/null +++ b/app/controllers/connections.js @@ -0,0 +1,47 @@ +// +// Connections Controller +// + +'use strict'; + +module.exports = function() { + + var app = this.app, + core = this.core, + middlewares = this.middlewares; + + // + // Routes + // + app.get('/connections', middlewares.requireLogin, function(req) { + req.io.route('connections:list'); + }); + + app.get('/connections/type/:type', middlewares.requireLogin, function(req) { + req.io.route('connections:list'); + }); + + app.get('/connections/user/:user', middlewares.requireLogin, function(req) { + req.io.route('connections:list'); + }); + + // + // Sockets + // + app.io.route('connections', { + list: function(req, res) { + var query = {}; + + if (req.param('type')) { + query.type = req.param('type'); + } + + if (req.param('user')) { + query.user = req.param('user'); + } + + var connections = core.presence.system.connections.query(query); + res.json(connections); + } + }); +}; diff --git a/app/controllers/extras.js b/app/controllers/extras.js new file mode 100644 index 0000000..228b0f3 --- /dev/null +++ b/app/controllers/extras.js @@ -0,0 +1,87 @@ +// +// Emotes / Replacements Controller +// + +'use strict'; + +module.exports = function() { + + var _ = require('lodash'), + fs = require('fs'), + path = require('path'), + yaml = require('js-yaml'), + express = require('express.oi'); + + var app = this.app, + middlewares = this.middlewares; + + // + // Routes + // + app.get('/extras/emotes', middlewares.requireLogin, function(req) { + req.io.route('extras:emotes:list'); + }); + + app.use('/extras/emotes/', + express.static(path.join(process.cwd(), 'extras/emotes/public'), { + maxage: '364d' + }) + ); + + app.get('/extras/replacements', middlewares.requireLogin, function(req) { + req.io.route('extras:replacements:list'); + }); + + // + // Sockets + // + app.io.route('extras', { + 'emotes:list': function(req, res) { + var emotes = []; + + var dir = path.join(process.cwd(), 'extras/emotes'); + fs.readdir(dir, function(err, files) { + if (err) { + return res.json(emotes); + } + + var regex = new RegExp('\\.yml$'); + + files = files.filter(function(fileName) { + return regex.test(fileName); + }); + + files.forEach(function(fileName) { + var fullpath = path.join( + process.cwd(), + 'extras/emotes', + fileName + ); + + var imgDir = 'extras/emotes/' + + fileName.replace('.yml', '') + '/'; + + var file = fs.readFileSync(fullpath, 'utf8'); + var data = yaml.safeLoad(file); + _.each(data, function(emote) { + emote.image = imgDir + emote.image; + emotes.push(emote); + }); + }); + + res.json(emotes); + }); + }, + 'replacements:list': function(req, res) { + var replacements = []; + ['default.yml', 'local.yml'].forEach(function(filename) { + var fullpath = path.join(process.cwd(), 'extras/replacements/' + filename); + if (fs.existsSync(fullpath)) { + replacements = _.merge(replacements, yaml.safeLoad(fs.readFileSync(fullpath, 'utf8'))); + } + }); + res.json(replacements); + } + }); + +}; diff --git a/app/controllers/files.js b/app/controllers/files.js new file mode 100644 index 0000000..0738972 --- /dev/null +++ b/app/controllers/files.js @@ -0,0 +1,132 @@ +// +// Files Controller +// + +'use strict'; + +var multer = require('multer'), + settings = require('./../config').files; + +module.exports = function() { + + if (!settings.enable) { + return; + } + + var app = this.app, + core = this.core, + middlewares = this.middlewares, + models = this.models; + + core.on('files:new', function(file, room, user) { + var fil = file.toJSON(); + fil.owner = user; + fil.room = room.toJSON(user); + + app.io.to(room._id) + .emit('files:new', fil); + }); + + var fileUpload = multer({ + putSingleFilesInArray: true, + limits: { + files: 1, + fileSize: settings.maxFileSize + } + }); + + // + // Routes + // + app.route('/files') + .all(middlewares.requireLogin) + .get(function(req) { + req.io.route('files:list'); + }) + .post(fileUpload, middlewares.cleanupFiles, function(req) { + req.io.route('files:create'); + }); + + app.route('/rooms/:room/files') + .all(middlewares.requireLogin, middlewares.roomRoute) + .get(function(req) { + req.io.route('files:list'); + }) + .post(fileUpload, middlewares.cleanupFiles, function(req) { + req.io.route('files:create'); + }); + + app.route('/files/:id/:name') + .all(middlewares.requireLogin) + .get(function(req, res) { + models.file.findById(req.params.id, function(err, file) { + if (err) { + // Error + return res.send(400); + } + + var url = core.files.getUrl(file); + if (settings.provider === 'local') { + res.sendFile(url, { + headers: { + 'Content-Type': file.type + } + }); + } else { + res.redirect(url); + } + + }); + }); + + // + // Sockets + // + app.io.route('files', { + create: function(req, res) { + if (!req.files || !req.files.file) { + return res.sendStatus(400); + } + + var options = { + owner: req.user._id, + room: req.param('room'), + file: req.files.file[0], + post: (req.param('post') === 'true') && true + }; + + core.files.create(options, function(err, file) { + if (err) { + console.error(err); + return res.sendStatus(400); + } + res.status(201).json(file); + }); + }, + list: function(req, res) { + var options = { + userId: req.user._id, + password: req.param('password'), + + room: req.param('room'), + reverse: req.param('reverse'), + skip: req.param('skip'), + take: req.param('take'), + expand: req.param('expand') + }; + + core.files.list(options, function(err, files) { + if (err) { + return res.sendStatus(400); + } + + files = files.map(function(file) { + return file.toJSON(req.user); + }); + + res.json(files); + }); + } + }); + +}; diff --git a/app/controllers/index.js b/app/controllers/index.js new file mode 100644 index 0000000..0469ed4 --- /dev/null +++ b/app/controllers/index.js @@ -0,0 +1,9 @@ +// +// Controllers Loader! +// + +'use strict'; + +var requireDirectory = require('require-directory'); + +module.exports = requireDirectory(module); diff --git a/app/controllers/messages.js b/app/controllers/messages.js new file mode 100644 index 0000000..da43872 --- /dev/null +++ b/app/controllers/messages.js @@ -0,0 +1,91 @@ +// +// Messages Controller +// + +'use strict'; + +module.exports = function() { + + var app = this.app, + core = this.core, + middlewares = this.middlewares; + + core.on('messages:new', function(message, room, user) { + var msg = message.toJSON(); + msg.owner = user; + msg.room = room.toJSON(user); + + app.io.to(room.id) + .emit('messages:new', msg); + }); + + // + // Routes + // + app.route('/messages') + .all(middlewares.requireLogin) + .get(function(req) { + req.io.route('messages:list'); + }) + .post(function(req) { + req.io.route('messages:create'); + }); + + app.route('/rooms/:room/messages') + .all(middlewares.requireLogin, middlewares.roomRoute) + .get(function(req) { + req.io.route('messages:list'); + }) + .post(function(req) { + req.io.route('messages:create'); + }); + + // + // Sockets + // + app.io.route('messages', { + create: function(req, res) { + var options = { + owner: req.user._id, + room: req.param('room'), + text: req.param('text') + }; + + core.messages.create(options, function(err, message) { + if (err) { + return res.sendStatus(400); + } + res.status(201).json(message); + }); + }, + list: function(req, res) { + var options = { + userId: req.user._id, + password: req.param('password'), + + room: req.param('room'), + since_id: req.param('since_id'), + from: req.param('from'), + to: req.param('to'), + query: req.param('query'), + reverse: req.param('reverse'), + skip: req.param('skip'), + take: req.param('take'), + expand: req.param('expand') + }; + + core.messages.list(options, function(err, messages) { + if (err) { + return res.sendStatus(400); + } + + messages = messages.map(function(message) { + return message.toJSON(req.user); + }); + + res.json(messages); + }); + } + }); + +}; diff --git a/app/controllers/misc.js b/app/controllers/misc.js new file mode 100644 index 0000000..e2c1a29 --- /dev/null +++ b/app/controllers/misc.js @@ -0,0 +1,25 @@ +// +// Misc Controller +// + +'use strict'; + +var path = require('path'), + settings = require('./../config'); + +module.exports = function() { + + var app = this.app; + + // + // Routes + // + app.get('/robots.txt', function(req, res) { + if (!settings.noRobots) { + return res.sendStatus(404); + } + + res.sendFile(path.resolve(__dirname, '../misc/robots.txt')); + }); + +}; diff --git a/app/controllers/presence.js b/app/controllers/presence.js new file mode 100644 index 0000000..03ef94c --- /dev/null +++ b/app/controllers/presence.js @@ -0,0 +1,38 @@ +'use strict'; + +var util = require('util'), + Connection = require('./../core/presence').Connection; + +function SocketIoConnection(user, socket) { + Connection.call(this, 'socket.io', user); + this.socket = socket; + socket.conn = this; + socket.on('disconnect', this.disconnect.bind(this)); +} + +util.inherits(SocketIoConnection, Connection); + +SocketIoConnection.prototype.disconnect = function() { + this.emit('disconnect'); + + this.socket.conn = null; + this.socket = null; +}; + +module.exports = function() { + var app = this.app, + core = this.core, + User = this.models.user; + + app.io.on('connection', function(socket) { + var userId = socket.request.user._id; + User.findById(userId, function (err, user) { + if (err) { + console.error(err); + return; + } + var conn = new SocketIoConnection(user, socket); + core.presence.connect(conn); + }); + }); +}; diff --git a/app/controllers/rooms.js b/app/controllers/rooms.js new file mode 100644 index 0000000..ba6124f --- /dev/null +++ b/app/controllers/rooms.js @@ -0,0 +1,312 @@ +// +// Rooms Controller +// + +'use strict'; + +var settings = require('./../config').rooms; + +module.exports = function() { + var app = this.app, + core = this.core, + middlewares = this.middlewares, + models = this.models, + User = models.user; + + core.on('presence:user_join', function(data) { + User.findById(data.userId, function (err, user) { + if (!err && user) { + user = user.toJSON(); + user.room = data.roomId; + if (data.roomHasPassword) { + app.io.to(data.roomId).emit('users:join', user); + } else { + app.io.emit('users:join', user); + } + } + }); + }); + + core.on('presence:user_leave', function(data) { + User.findById(data.userId, function (err, user) { + if (!err && user) { + user = user.toJSON(); + user.room = data.roomId; + if (data.roomHasPassword) { + app.io.to(data.roomId).emit('users:leave', user); + } else { + app.io.emit('users:leave', user); + } + } + }); + }); + + var getEmitters = function(room) { + if (room.private && !room.hasPassword) { + var connections = core.presence.connections.query({ + type: 'socket.io' + }).filter(function(connection) { + return room.isAuthorized(connection.user); + }); + + return connections.map(function(connection) { + return { + emitter: connection.socket, + user: connection.user + }; + }); + } + + return [{ + emitter: app.io + }]; + }; + + core.on('rooms:new', function(room) { + var emitters = getEmitters(room); + emitters.forEach(function(e) { + e.emitter.emit('rooms:new', room.toJSON(e.user)); + }); + }); + + core.on('rooms:update', function(room) { + var emitters = getEmitters(room); + emitters.forEach(function(e) { + e.emitter.emit('rooms:update', room.toJSON(e.user)); + }); + }); + + core.on('rooms:archive', function(room) { + var emitters = getEmitters(room); + emitters.forEach(function(e) { + e.emitter.emit('rooms:archive', room.toJSON(e.user)); + }); + }); + + + // + // Routes + // + app.route('/rooms') + .all(middlewares.requireLogin) + .get(function(req) { + req.io.route('rooms:list'); + }) + .post(function(req) { + req.io.route('rooms:create'); + }); + + app.route('/rooms/:room') + .all(middlewares.requireLogin, middlewares.roomRoute) + .get(function(req) { + req.io.route('rooms:get'); + }) + .put(function(req) { + req.io.route('rooms:update'); + }) + .delete(function(req) { + req.io.route('rooms:archive'); + }); + + app.route('/rooms/:room/users') + .all(middlewares.requireLogin, middlewares.roomRoute) + .get(function(req) { + req.io.route('rooms:users'); + }); + + + // + // Sockets + // + app.io.route('rooms', { + list: function(req, res) { + var options = { + userId: req.user._id, + users: req.param('users'), + + skip: req.param('skip'), + take: req.param('take') + }; + + core.rooms.list(options, function(err, rooms) { + if (err) { + console.error(err); + return res.status(400).json(err); + } + + var results = rooms.map(function(room) { + return room.toJSON(req.user); + }); + + res.json(results); + }); + }, + get: function(req, res) { + var options = { + userId: req.user._id, + identifier: req.param('room') || req.param('id') + }; + + core.rooms.get(options, function(err, room) { + if (err) { + console.error(err); + return res.status(400).json(err); + } + + if (!room) { + return res.sendStatus(404); + } + + res.json(room.toJSON(req.user)); + }); + }, + create: function(req, res) { + var options = { + owner: req.user._id, + name: req.param('name'), + slug: req.param('slug'), + description: req.param('description'), + private: req.param('private'), + password: req.param('password') + }; + + if (!settings.private) { + options.private = false; + delete options.password; + } + + core.rooms.create(options, function(err, room) { + if (err) { + console.error(err); + return res.status(400).json(err); + } + + res.status(201).json(room.toJSON(req.user)); + }); + }, + update: function(req, res) { + var roomId = req.param('room') || req.param('id'); + + var options = { + name: req.param('name'), + slug: req.param('slug'), + description: req.param('description'), + password: req.param('password'), + participants: req.param('participants'), + user: req.user + }; + + if (!settings.private) { + delete options.password; + delete options.participants; + } + + core.rooms.update(roomId, options, function(err, room) { + if (err) { + console.error(err); + return res.status(400).json(err); + } + + if (!room) { + return res.sendStatus(404); + } + + res.json(room.toJSON(req.user)); + }); + }, + archive: function(req, res) { + var roomId = req.param('room') || req.param('id'); + + core.rooms.archive(roomId, function(err, room) { + if (err) { + console.log(err); + return res.sendStatus(400); + } + + if (!room) { + return res.sendStatus(404); + } + + res.sendStatus(204); + }); + }, + join: function(req, res) { + var options = { + userId: req.user._id, + saveMembership: true + }; + + if (typeof req.data === 'string') { + options.id = req.data; + } else { + options.id = req.param('roomId'); + options.password = req.param('password'); + } + + core.rooms.canJoin(options, function(err, room, canJoin) { + if (err) { + console.error(err); + return res.sendStatus(400); + } + + if (!room) { + return res.sendStatus(404); + } + + if(!canJoin && room.password) { + return res.status(403).json({ + status: 'error', + roomName: room.name, + message: 'password required', + errors: 'password required' + }); + } + + if(!canJoin) { + return res.sendStatus(404); + } + + var user = req.user.toJSON(); + user.room = room._id; + + core.presence.join(req.socket.conn, room); + req.socket.join(room._id); + res.json(room.toJSON(req.user)); + }); + }, + leave: function(req, res) { + var roomId = req.data; + var user = req.user.toJSON(); + user.room = roomId; + + core.presence.leave(req.socket.conn, roomId); + req.socket.leave(roomId); + res.json(); + }, + users: function(req, res) { + var roomId = req.param('room'); + + core.rooms.get(roomId, function(err, room) { + if (err) { + console.error(err); + return res.sendStatus(400); + } + + if (!room) { + return res.sendStatus(404); + } + + var users = core.presence.rooms + .getOrAdd(room) + .getUsers() + .map(function(user) { + // TODO: Do we need to do this? + user.room = room.id; + return user; + }); + + res.json(users); + }); + } + }); +}; diff --git a/app/controllers/transcript.js b/app/controllers/transcript.js new file mode 100644 index 0000000..44d0155 --- /dev/null +++ b/app/controllers/transcript.js @@ -0,0 +1,35 @@ +// +// Transcript Controller +// + +'use strict'; + +module.exports = function() { + var app = this.app, + core = this.core, + middlewares = this.middlewares; + + // + // Routes + // + app.get('/transcript', middlewares.requireLogin, function(req, res) { + var roomId = req.param('room'); + core.rooms.get(roomId, function(err, room) { + if (err) { + console.error(err); + return res.sendStatus(404); + } + + if (!room) { + return res.sendStatus(404); + } + + res.render('transcript.html', { + room: { + id: roomId, + name: room.name + } + }); + }); + }); +}; diff --git a/app/controllers/usermessages.js b/app/controllers/usermessages.js new file mode 100644 index 0000000..0fe03ac --- /dev/null +++ b/app/controllers/usermessages.js @@ -0,0 +1,86 @@ +// +// UserMessages Controller +// + +'use strict'; + +var _ = require('lodash'), + settings = require('./../config'); + +module.exports = function() { + + var app = this.app, + core = this.core, + middlewares = this.middlewares; + + + if (!settings.private.enable) { + return; + } + + core.on('user-messages:new', function(message) { + _.each(message.users, function(userId) { + var connections = core.presence.system.connections.query({ + type: 'socket.io', userId: userId.toString() + }); + + _.each(connections, function(connection) { + connection.socket.emit('user-messages:new', message); + }); + }); + }); + + // + // Routes + // + + app.route('/users/:user/messages') + .all(middlewares.requireLogin) + .get(function(req) { + req.io.route('user-messages:list'); + }) + .post(function(req) { + req.io.route('user-messages:create'); + }); + + // + // Sockets + // + app.io.route('user-messages', { + create: function(req, res) { + var options = { + owner: req.user._id, + user: req.param('user'), + text: req.param('text') + }; + + core.usermessages.create(options, function(err, message) { + if (err) { + return res.sendStatus(400); + } + res.status(201).json(message); + }); + }, + list: function(req, res) { + var options = { + currentUser: req.user._id, + user: req.param('user'), + since_id: req.param('since_id'), + from: req.param('from'), + to: req.param('to'), + reverse: req.param('reverse'), + skip: req.param('skip'), + take: req.param('take'), + expand: req.param('expand') + }; + + core.usermessages.list(options, function(err, messages) { + if (err) { + return res.sendStatus(400); + } + res.json(messages); + }); + } + }); + +}; diff --git a/app/controllers/users.js b/app/controllers/users.js new file mode 100644 index 0000000..3d40fca --- /dev/null +++ b/app/controllers/users.js @@ -0,0 +1,62 @@ +// +// Users Controller +// + +'use strict'; + +module.exports = function() { + + var app = this.app, + core = this.core, + middlewares = this.middlewares, + models = this.models, + User = models.user; + + // + // Routes + // + app.get('/users', middlewares.requireLogin, function(req) { + req.io.route('users:list'); + }); + + app.get('/users/:id', middlewares.requireLogin, function(req) { + req.io.route('users:get'); + }); + + // + // Sockets + // + app.io.route('users', { + list: function(req, res) { + var options = { + skip: req.param('skip'), + take: req.param('take') + }; + + core.users.list(options, function(err, users) { + if (err) { + console.log(err); + return res.status(400).json(err); + } + + res.json(users); + }); + }, + get: function(req, res) { + var identifier = req.param('id'); + + User.findByIdentifier(identifier, function (err, user) { + if (err) { + console.error(err); + return res.status(400).json(err); + } + + if (!user) { + return res.sendStatus(404); + } + + res.json(user); + }); + } + }); +}; diff --git a/app/core/account.js b/app/core/account.js new file mode 100644 index 0000000..539eb4e --- /dev/null +++ b/app/core/account.js @@ -0,0 +1,114 @@ +'use strict'; + +var mongoose = require('mongoose'); + + +function AccountManager(options) { + this.core = options.core; +} + +AccountManager.prototype.create = function(provider, options, cb) { + var User = mongoose.model('User'); + var user = new User({ provider: provider }); + + Object.keys(options).forEach(function(key) { + user.set(key, options[key]); + }); + + user.save(cb); +}; + +AccountManager.prototype.update = function(id, options, cb) { + var User = mongoose.model('User'); + var usernameChange = false; + + User.findById(id, function (err, user) { + if (err) { + return cb(err); + } + + if (options.firstName) { + user.firstName = options.firstName; + } + if (options.lastName) { + user.lastName = options.lastName; + } + if (options.displayName) { + user.displayName = options.displayName; + } + if (options.email) { + user.email = options.email; + } + + if (options.username && options.username !== user.username) { + var xmppConns = this.core.presence.system.connections.query({ + userId: user._id, + type: 'xmpp' + }); + + if (xmppConns.length) { + return cb(null, null, 'You can not change your username ' + + 'with active XMPP sessions.'); + } + + usernameChange = true; + user.username = options.username; + } + + if (user.local) { + + if (options.password || options.newPassword) { + user.password = options.password || options.newPassword; + } + + } + + user.save(function(err, user) { + if (err) { + return cb(err); + } + + this.core.emit('account:update', { + usernameChanged: usernameChange, + user: user.toJSON() + }); + + if (cb) { + cb(null, user); + } + + }.bind(this)); + }.bind(this)); +}; + +AccountManager.prototype.generateToken = function(id, cb) { + var User = mongoose.model('User'); + + User.findById(id, function (err, user) { + if (err) { + return cb(err); + } + + user.generateToken(function(err, token) { + if (err) { + return cb(err); + } + + user.save(function(err) { + if (err) { + return cb(err); + } + + cb(null, token); + }); + }); + }); +}; + +AccountManager.prototype.revokeToken = function(id, cb) { + var User = mongoose.model('User'); + + User.update({_id: id}, {$unset: {token: 1}}, cb); +}; + +module.exports = AccountManager; diff --git a/app/core/avatar-cache.js b/app/core/avatar-cache.js new file mode 100644 index 0000000..ed6a521 --- /dev/null +++ b/app/core/avatar-cache.js @@ -0,0 +1,46 @@ +'use strict'; + +var crypto = require('crypto'), + http = require('http'); + +function AvatarCache(options) { + this.core = options.core; + this.avatars = {}; + + this.get = this.get.bind(this); + this.add = this.add.bind(this); +} + +AvatarCache.prototype.get = function(userId) { + return this.avatars[userId]; +}; + +AvatarCache.prototype.add = function(user) { + var userId = (user.id || user._id).toString(); + var url = 'http://www.gravatar.com/avatar/' + user.avatar + '?s=64'; + + http.get(url, function(response) { + if (response.statusCode !== 200) { + return; + } + + var buffers = []; + + response.on('data', function(buffer) { + buffers.push(buffer); + }); + + response.on('end', function() { + var buffer = Buffer.concat(buffers); + + this.avatars[userId] = { + base64: buffer.toString('base64'), + sha1: crypto.createHash('sha1').update(buffer).digest('hex') + }; + + this.core.emit('avatar-cache:update', user); + }.bind(this)); + }.bind(this)).on('error', function(){ }); +}; + +module.exports = AvatarCache; diff --git a/app/core/files.js b/app/core/files.js new file mode 100644 index 0000000..aade909 --- /dev/null +++ b/app/core/files.js @@ -0,0 +1,198 @@ +'use strict'; + +var _ = require('lodash'), + mongoose = require('mongoose'), + helpers = require('./helpers'), + plugins = require('./../plugins'), + settings = require('./../config').files; + +var enabled = settings.enable; + +function FileManager(options) { + this.core = options.core; + + if (!enabled) { + return; + } + + var Provider; + + if (settings.provider === 'local') { + Provider = require('./files/local'); + } else { + Provider = plugins.getPlugin(settings.provider, 'files'); + } + + this.provider = new Provider(settings[settings.provider]); +} + +FileManager.prototype.create = function(options, cb) { + if (!enabled) { + return cb('Files are disabled.'); + } + + var File = mongoose.model('File'), + Room = mongoose.model('Room'), + User = mongoose.model('User'); + + if (settings.restrictTypes && + settings.allowedTypes && + settings.allowedTypes.length && + !_.include(settings.allowedTypes, options.file.mimetype)) { + return cb('The MIME type ' + options.file.mimetype + + ' is not allowed'); + } + + Room.findById(options.room, function(err, room) { + + if (err) { + console.error(err); + return cb(err); + } + if (!room) { + return cb('Room does not exist.'); + } + if (room.archived) { + return cb('Room is archived.'); + } + if (!room.isAuthorized(options.owner)) { + return cb('Not authorized.'); + } + + new File({ + owner: options.owner, + name: options.file.originalname, + type: options.file.mimetype, + size: options.file.size, + room: options.room + }).save(function(err, savedFile) { + if (err) { + return cb(err); + } + + this.provider.save({file: options.file, doc: savedFile}, function(err) { + if (err) { + savedFile.remove(); + return cb(err); + } + + // Temporary workaround for _id until populate can do aliasing + User.findOne(options.owner, function(err, user) { + if (err) { + console.error(err); + return cb(err); + } + + cb(null, savedFile, room, user); + + this.core.emit('files:new', savedFile, room, user); + + if (options.post) { + this.core.messages.create({ + room: room, + owner: user.id, + text: 'upload://' + savedFile.url + }); + } + }.bind(this)); + }.bind(this)); + }.bind(this)); + }.bind(this)); +}; + +FileManager.prototype.list = function(options, cb) { + var Room = mongoose.model('Room'); + + if (!enabled) { + return cb(null, []); + } + + options = options || {}; + + if (!options.room) { + return cb(null, []); + } + + options = helpers.sanitizeQuery(options, { + defaults: { + reverse: true, + take: 500 + }, + maxTake: 5000 + }); + + var File = mongoose.model('File'); + + var find = File.find({ + room: options.room + }); + + if (options.from) { + find.where('uploaded').gt(options.from); + } + + if (options.to) { + find.where('uploaded').lte(options.to); + } + + if (options.expand) { + var includes = options.expand.replace(/\s/, '').split(','); + + if (_.includes(includes, 'owner')) { + find.populate('owner', 'id username displayName email avatar'); + } + } + + if (options.skip) { + find.skip(options.skip); + } + + if (options.reverse) { + find.sort({ 'uploaded': -1 }); + } else { + find.sort({ 'uploaded': 1 }); + } + + Room.findById(options.room, function(err, room) { + if (err) { + console.error(err); + return cb(err); + } + + var opts = { + userId: options.userId, + password: options.password + }; + + room.canJoin(opts, function(err, canJoin) { + if (err) { + console.error(err); + return cb(err); + } + + if (!canJoin) { + return cb(null, []); + } + + find + .limit(options.take) + .exec(function(err, files) { + if (err) { + console.error(err); + return cb(err); + } + cb(null, files); + }); + }); + }); +}; + +FileManager.prototype.getUrl = function(file) { + if (!enabled) { + return; + } + + return this.provider.getUrl(file); +}; + +module.exports = FileManager; diff --git a/app/core/files/local.js b/app/core/files/local.js new file mode 100644 index 0000000..bedc530 --- /dev/null +++ b/app/core/files/local.js @@ -0,0 +1,48 @@ +'use strict'; + +var fs = require('fs'), + path = require('path'); + +function LocalFiles(options) { + this.options = options; + + this.getUrl = this.getUrl.bind(this); + this.save = this.save.bind(this); +} + +LocalFiles.prototype.getUrl = function(file) { + return path.resolve(this.options.dir + '/' + file._id); +}; + +LocalFiles.prototype.save = function(options, callback) { + var file = options.file, + doc = options.doc, + fileFolder = doc._id, + filePath = fileFolder + '/' + encodeURIComponent(doc.name), + newPath = this.options.dir + '/' + fileFolder; + + this.copyFile(file.path, newPath, function(err) { + + if (err) { + return callback(err); + } + + // Let the clients know about the new file + var url = '/files/' + filePath; + callback(null, url, doc); + }); +}; + +LocalFiles.prototype.copyFile = function(path, newPath, callback) { + fs.readFile(path, function(err, data) { + if (err) { + return callback(err); + } + + fs.writeFile(newPath, data, function(err) { + callback(err); + }); + }); +}; + +module.exports = LocalFiles; diff --git a/app/core/helpers.js b/app/core/helpers.js new file mode 100644 index 0000000..18a29e5 --- /dev/null +++ b/app/core/helpers.js @@ -0,0 +1,22 @@ +'use strict'; + +module.exports = { + sanitizeQuery: function(query, options) { + if (options.defaults.take && !query.take) { + query.take = options.defaults.take; + } + if (options.maxTake < query.take) { + query.take = options.maxTake; + } + + if (typeof query.reverse === 'string') { + query.reverse = query.reverse.toLowerCase() === 'true'; + } + + if (typeof query.reverse === 'undefined') { + query.reverse = options.defaults.reverse; + } + + return query; + } +}; diff --git a/app/core/index.js b/app/core/index.js new file mode 100644 index 0000000..83fd0ad --- /dev/null +++ b/app/core/index.js @@ -0,0 +1,81 @@ +'use strict'; + +var EventEmitter = require('events').EventEmitter, + util = require('util'), + _ = require('lodash'), + AccountManager = require('./account'), + AvatarCache = require('./avatar-cache'), + FileManager = require('./files'), + MessageManager = require('./messages'), + PresenceManager = require('./presence'), + RoomManager = require('./rooms'), + UserManager = require('./users'), + UserMessageManager = require('./usermessages'); + +function Core() { + EventEmitter.call(this); + + this.account = new AccountManager({ + core: this + }); + + this.files = new FileManager({ + core: this + }); + + this.messages = new MessageManager({ + core: this + }); + + this.presence = new PresenceManager({ + core: this + }); + + this.rooms = new RoomManager({ + core: this + }); + + this.users = new UserManager({ + core: this + }); + + this.usermessages = new UserMessageManager({ + core: this + }); + + this.avatars = new AvatarCache({ + core: this + }); + + this.onAccountUpdated = this.onAccountUpdated.bind(this); + + this.on('account:update', this.onAccountUpdated); +} + +util.inherits(Core, EventEmitter); + +Core.prototype.onAccountUpdated = function(data) { + var userId = data.user.id.toString(); + var user = this.presence.users.get(userId); + + if (!user) { + return; + } + + var new_data = { + userId: userId, + oldUsername: user.username, + username: data.user.username + }; + + if (user) { + _.assign(user, data.user, { id: userId }); + } + + if (data.usernameChanged) { + // Emit to all rooms, that this user has changed their username + this.presence.rooms.usernameChanged(new_data); + } +}; + +module.exports = new Core(); diff --git a/app/core/messages.js b/app/core/messages.js new file mode 100644 index 0000000..24bb51f --- /dev/null +++ b/app/core/messages.js @@ -0,0 +1,151 @@ +'use strict'; + +var _ = require('lodash'), + mongoose = require('mongoose'), + helpers = require('./helpers'); + +function MessageManager(options) { + this.core = options.core; +} + +MessageManager.prototype.create = function(options, cb) { + var Message = mongoose.model('Message'), + Room = mongoose.model('Room'), + User = mongoose.model('User'); + + if (typeof cb !== 'function') { + cb = function() {}; + } + + Room.findById(options.room, function(err, room) { + if (err) { + console.error(err); + return cb(err); + } + if (!room) { + return cb('Room does not exist.'); + } + if (room.archived) { + return cb('Room is archived.'); + } + if (!room.isAuthorized(options.owner)) { + return cb('Not authorized.'); + } + + Message.create(options, function(err, message) { + if (err) { + console.error(err); + return cb(err); + } + // Touch Room's lastActive + room.lastActive = message.posted; + room.save(); + // Temporary workaround for _id until populate can do aliasing + User.findOne(message.owner, function(err, user) { + if (err) { + console.error(err); + return cb(err); + } + + cb(null, message, room, user); + this.core.emit('messages:new', message, room, user, options.data); + }.bind(this)); + }.bind(this)); + }.bind(this)); +}; + +MessageManager.prototype.list = function(options, cb) { + var Room = mongoose.model('Room'); + + options = options || {}; + + if (!options.room) { + return cb(null, []); + } + + options = helpers.sanitizeQuery(options, { + defaults: { + reverse: true, + take: 500 + }, + maxTake: 5000 + }); + + var Message = mongoose.model('Message'); + + var find = Message.find({ + room: options.room + }); + + if (options.since_id) { + find.where('_id').gt(options.since_id); + } + + if (options.from) { + find.where('posted').gt(options.from); + } + + if (options.to) { + find.where('posted').lte(options.to); + } + + if (options.query) { + find = find.find({$text: {$search: options.query}}); + } + + if (options.expand) { + var includes = options.expand.replace(/\s/, '').split(','); + + if (_.includes(includes, 'owner')) { + find.populate('owner', 'id username displayName email avatar'); + } + + if (_.includes(includes, 'room')) { + find.populate('room', 'id name'); + } + } + + if (options.skip) { + find.skip(options.skip); + } + + if (options.reverse) { + find.sort({ 'posted': -1 }); + } else { + find.sort({ 'posted': 1 }); + } + + Room.findById(options.room, function(err, room) { + if (err) { + console.error(err); + return cb(err); + } + + var opts = { + userId: options.userId, + password: options.password + }; + + room.canJoin(opts, function(err, canJoin) { + if (err) { + console.error(err); + return cb(err); + } + + if (!canJoin) { + return cb(null, []); + } + + find.limit(options.take) + .exec(function(err, messages) { + if (err) { + console.error(err); + return cb(err); + } + cb(null, messages); + }); + }); + }); +}; + +module.exports = MessageManager; diff --git a/app/core/presence.js b/app/core/presence.js new file mode 100644 index 0000000..dbf7440 --- /dev/null +++ b/app/core/presence.js @@ -0,0 +1,71 @@ +'use strict'; + +var Connection = require('./presence/connection'), + Room = require('./presence/room'), + ConnectionCollection = require('./presence/connection-collection'), + RoomCollection = require('./presence/room-collection'), + UserCollection = require('./presence/user-collection'); + +function PresenceManager(options) { + this.core = options.core; + this.system = new Room({ system: true }); + this.connections = new ConnectionCollection(); + this.rooms = new RoomCollection(); + this.users = new UserCollection({ core: this.core }); + this.rooms.on('user_join', this.onJoin.bind(this)); + this.rooms.on('user_leave', this.onLeave.bind(this)); + + this.connect = this.connect.bind(this); + this.getUserCountForRoom = this.getUserCountForRoom.bind(this); + this.getUsersForRoom = this.getUsersForRoom.bind(this); +} + +PresenceManager.prototype.getUserCountForRoom = function(roomId) { + var room = this.rooms.get(roomId); + return room ? room.userCount : 0; +}; + +PresenceManager.prototype.getUsersForRoom = function(roomId) { + var room = this.rooms.get(roomId); + return room ? room.getUsers() : []; +}; + +PresenceManager.prototype.connect = function(connection) { + this.system.addConnection(connection); + this.core.emit('connect', connection); + + connection.user = this.users.getOrAdd(connection.user); + + connection.on('disconnect', function() { + this.disconnect(connection); + }.bind(this)); +}; + +PresenceManager.prototype.disconnect = function(connection) { + this.system.removeConnection(connection); + this.core.emit('disconnect', connection); + this.rooms.removeConnection(connection); +}; + +PresenceManager.prototype.join = function(connection, room) { + var pRoom = this.rooms.getOrAdd(room); + pRoom.addConnection(connection); +}; + +PresenceManager.prototype.leave = function(connection, roomId) { + var room = this.rooms.get(roomId); + if (room) { + room.removeConnection(connection); + } +}; + +PresenceManager.prototype.onJoin = function(data) { + this.core.emit('presence:user_join', data); +}; + +PresenceManager.prototype.onLeave = function(data) { + this.core.emit('presence:user_leave', data); +}; + +PresenceManager.Connection = Connection; +module.exports = PresenceManager; diff --git a/app/core/presence/connection-collection.js b/app/core/presence/connection-collection.js new file mode 100644 index 0000000..4e7846d --- /dev/null +++ b/app/core/presence/connection-collection.js @@ -0,0 +1,123 @@ +'use strict'; + +var _ = require('lodash'); + +function ConnectionCollection() { + this.connections = {}; + + this.get = this.get.bind(this); + this.getUsers = this.getUsers.bind(this); + this.getUserIds = this.getUserIds.bind(this); + this.getUsernames = this.getUsernames.bind(this); + + this.add = this.add.bind(this); + this.remove = this.remove.bind(this); + this.removeAll = this.removeAll.bind(this); +} + +ConnectionCollection.prototype.get = function(connectionId) { + return this.connections[connectionId]; +}; + +ConnectionCollection.prototype.contains = function(connection) { + if (!connection) { + return false; + } + + return !!this.connections[connection.id]; +}; + +ConnectionCollection.prototype.getUsers = function(filter) { + var connections = this.connections; + + if (filter) { + connections = this.query(filter); + } + + var users = _.chain(connections) + .filter(function(value) { + return !!value.user; + }) + .map(function(value) { + return value.user; + }) + .uniq('id') + .value(); + + return users; +}; + +ConnectionCollection.prototype.getUserIds = function(filter) { + var users = this.getUsers(filter); + + return _.map(users, function(user) { + return user.id; + }); +}; + +ConnectionCollection.prototype.getUsernames = function(filter) { + var users = this.getUsers(filter); + + return _.map(users, function(user) { + return user.username; + }); +}; + +ConnectionCollection.prototype.query = function(options) { + if (options.userId) { + options.userId = options.userId.toString(); + } + + return _.map(this.connections, function(value) { + return value; + }).filter(function(conn) { + var result = true; + + if (options.user) { + var u = options.user; + if (conn.user && conn.user.id !== u && conn.user.username !== u) { + result = false; + } + } + + if (options.userId && conn.user && conn.user.id !== options.userId) { + result = false; + } + + if (options.type && conn.type !== options.type) { + result = false; + } + + return result; + }); +}; + +ConnectionCollection.prototype.add = function(connection) { + this.connections[connection.id] = connection; +}; + +ConnectionCollection.prototype.remove = function(connection) { + if (!connection) { + return; + } + + var connId = typeof connection === 'string' ? connection : connection.id; + if (this.connections[connId]) { + delete this.connections[connId]; + return true; + } else { + return false; + } +}; + +ConnectionCollection.prototype.removeAll = function() { + var keys = Object.keys(this.connections); + + keys.forEach(function(key) { + delete this.connections[key]; + }, this); + + return true; +}; + +module.exports = ConnectionCollection; diff --git a/app/core/presence/connection.js b/app/core/presence/connection.js new file mode 100644 index 0000000..ab196fb --- /dev/null +++ b/app/core/presence/connection.js @@ -0,0 +1,25 @@ +'use strict'; + +var EventEmitter = require('events').EventEmitter, + util = require('util'), + uuid = require('node-uuid'); + + +function Connection(type, user) { + EventEmitter.call(this); + this.type = type; + this.id = uuid.v4(); + this.user = user; +} + +util.inherits(Connection, EventEmitter); + +Connection.prototype.toJSON = function() { + return { + id: this.id, + type: this.type, + user: this.user + }; +}; + +module.exports = Connection; diff --git a/app/core/presence/room-collection.js b/app/core/presence/room-collection.js new file mode 100644 index 0000000..7c7d934 --- /dev/null +++ b/app/core/presence/room-collection.js @@ -0,0 +1,65 @@ +'use strict'; + +var EventEmitter = require('events').EventEmitter, + util = require('util'), + _ = require('lodash'), + Room = require('./room'); + +function RoomCollection() { + EventEmitter.call(this); + this.rooms = {}; + + this.get = this.get.bind(this); + this.getOrAdd = this.getOrAdd.bind(this); + + this.onJoin = this.onJoin.bind(this); + this.onLeave = this.onLeave.bind(this); +} + +util.inherits(RoomCollection, EventEmitter); + +RoomCollection.prototype.get = function(roomId) { + roomId = roomId.toString(); + return this.rooms[roomId]; +}; + +RoomCollection.prototype.slug = function(slug) { + return _.find(this.rooms, function(room) { + return room.roomSlug === slug; + }); +}; + +RoomCollection.prototype.getOrAdd = function(room) { + var roomId = room._id.toString(); + var pRoom = this.rooms[roomId]; + if (!pRoom) { + pRoom = this.rooms[roomId] = new Room({ + room: room + }); + pRoom.on('user_join', this.onJoin); + pRoom.on('user_leave', this.onLeave); + } + return pRoom; +}; + +RoomCollection.prototype.onJoin = function(data) { + this.emit('user_join', data); +}; + +RoomCollection.prototype.onLeave = function(data) { + this.emit('user_leave', data); +}; + +RoomCollection.prototype.usernameChanged = function(data) { + Object.keys(this.rooms).forEach(function(key) { + this.rooms[key].usernameChanged(data); + }, this); +}; + +RoomCollection.prototype.removeConnection = function(connection) { + Object.keys(this.rooms).forEach(function(key) { + this.rooms[key].removeConnection(connection); + }, this); +}; + +module.exports = RoomCollection; diff --git a/app/core/presence/room.js b/app/core/presence/room.js new file mode 100644 index 0000000..e95ee22 --- /dev/null +++ b/app/core/presence/room.js @@ -0,0 +1,147 @@ +'use strict'; + +var EventEmitter = require('events').EventEmitter, + util = require('util'), + ConnectionCollection = require('./connection-collection'); + +function Room(options) { + EventEmitter.call(this); + + if (options.system) { + // This is the system room + // Used for tracking what users are online + this.system = true; + this.roomId = undefined; + this.roomSlug = undefined; + this.hasPassword = false; + } else { + this.system = false; + this.roomId = options.room._id.toString(); + this.roomSlug = options.room.slug; + this.hasPassword = options.room.hasPassword; + } + + this.connections = new ConnectionCollection(); + this.userCount = 0; + + this.getUsers = this.getUsers.bind(this); + this.getUserIds = this.getUserIds.bind(this); + this.getUsernames = this.getUsernames.bind(this); + this.containsUser = this.containsUser.bind(this); + + this.emitUserJoin = this.emitUserJoin.bind(this); + this.emitUserLeave = this.emitUserLeave.bind(this); + this.addConnection = this.addConnection.bind(this); + this.removeConnection = this.removeConnection.bind(this); +} + +util.inherits(Room, EventEmitter); + +Room.prototype.getUsers = function() { + return this.connections.getUsers(); +}; + +Room.prototype.getUserIds = function() { + return this.connections.getUserIds(); +}; + +Room.prototype.getUsernames = function() { + return this.connections.getUsernames(); +}; + +Room.prototype.containsUser = function(userId) { + return this.getUserIds().indexOf(userId) !== -1; +}; + +Room.prototype.emitUserJoin = function(data) { + this.userCount++; + + var d = { + userId: data.userId, + username: data.username + }; + + if (this.system) { + d.system = true; + } else { + d.roomId = this.roomId; + d.roomSlug = this.roomSlug; + d.roomHasPassword = this.hasPassword; + } + + this.emit('user_join', d); +}; + +Room.prototype.emitUserLeave = function(data) { + this.userCount--; + + var d = { + user: data.user, + userId: data.userId, + username: data.username + }; + + if (this.system) { + d.system = true; + } else { + d.roomId = this.roomId; + d.roomSlug = this.roomSlug; + d.roomHasPassword = this.hasPassword; + } + + this.emit('user_leave', d); +}; + +Room.prototype.usernameChanged = function(data) { + if (this.containsUser(data.userId)) { + // User leaving room + this.emitUserLeave({ + userId: data.userId, + username: data.oldUsername + }); + // User rejoining room with new username + this.emitUserJoin({ + userId: data.userId, + username: data.username + }); + } +}; + +Room.prototype.addConnection = function(connection) { + if (!connection) { + console.error('Attempt to add an invalid connection was detected'); + return; + } + + if (connection.user && connection.user.id && + !this.containsUser(connection.user.id)) { + // User joining room + this.emitUserJoin({ + user: connection.user, + userId: connection.user.id, + username: connection.user.username + }); + } + this.connections.add(connection); +}; + +Room.prototype.removeConnection = function(connection) { + if (!connection) { + console.error('Attempt to remove an invalid connection was detected'); + return; + } + + if (this.connections.remove(connection)) { + if (connection.user && connection.user.id && + !this.containsUser(connection.user.id)) { + // Leaving room altogether + this.emitUserLeave({ + user: connection.user, + userId: connection.user.id, + username: connection.user.username + }); + } + } +}; + +module.exports = Room; diff --git a/app/core/presence/user-collection.js b/app/core/presence/user-collection.js new file mode 100644 index 0000000..f65f5e6 --- /dev/null +++ b/app/core/presence/user-collection.js @@ -0,0 +1,46 @@ +'use strict'; + +var EventEmitter = require('events').EventEmitter, + util = require('util'), + _ = require('lodash'); + +function UserCollection(options) { + EventEmitter.call(this); + this.core = options.core; + this.users = {}; + + this.get = this.get.bind(this); + this.getOrAdd = this.getOrAdd.bind(this); + this.remove = this.remove.bind(this); +} + +util.inherits(UserCollection, EventEmitter); + +UserCollection.prototype.get = function(userId) { + return this.users[userId]; +}; + +UserCollection.prototype.getByUsername = function(username) { + return _.find(this.users, function(user) { + return user.username === username; + }); +}; + +UserCollection.prototype.getOrAdd = function(user) { + var user2 = typeof user.toJSON === 'function' ? user.toJSON() : user; + var userId = user2.id.toString(); + if (!this.users[userId]) { + _.assign(user2, { id: userId }); + this.users[userId] = user2; + this.core.avatars.add(user); + } + return this.users[userId]; +}; + +UserCollection.prototype.remove = function(user) { + user = typeof user.toJSON === 'function' ? user.toJSON() : user; + var userId = typeof user === 'object' ? user.id.toString() : user; + delete this.users[userId]; +}; + +module.exports = UserCollection; diff --git a/app/core/rooms.js b/app/core/rooms.js new file mode 100644 index 0000000..688b927 --- /dev/null +++ b/app/core/rooms.js @@ -0,0 +1,278 @@ +'use strict'; + +var mongoose = require('mongoose'), + _ = require('lodash'), + helpers = require('./helpers'); + +var getParticipants = function(room, options, cb) { + if (!room.private || !options.participants) { + return cb(null, []); + } + + var participants = []; + + if (Array.isArray(options.participants)) { + participants = options.participants; + } + + if (typeof options.participants === 'string') { + participants = options.participants.replace(/@/g, '') + .split(',').map(function(username) { + return username.trim(); + }); + } + + participants = _.chain(participants) + .map(function(username) { + return username && username.replace(/@,\s/g, '').trim(); + }) + .filter(function(username) { return !!username; }) + .uniq() + .value(); + + var User = mongoose.model('User'); + User.find({username: { $in: participants } }, cb); +}; + +function RoomManager(options) { + this.core = options.core; +} + +RoomManager.prototype.canJoin = function(options, cb) { + var method = options.id ? 'get' : 'slug', + roomId = options.id ? options.id : options.slug; + + this[method](roomId, function(err, room) { + if (err) { + return cb(err); + } + + if (!room) { + return cb(); + } + + room.canJoin(options, function(err, canJoin) { + cb(err, room, canJoin); + }); + }); +}; + +RoomManager.prototype.create = function(options, cb) { + var Room = mongoose.model('Room'); + Room.create(options, function(err, room) { + if (err) { + return cb(err); + } + + if (cb) { + room = room; + cb(null, room); + this.core.emit('rooms:new', room); + } + }.bind(this)); +}; + +RoomManager.prototype.update = function(roomId, options, cb) { + var Room = mongoose.model('Room'); + + Room.findById(roomId, function(err, room) { + if (err) { + // Oh noes, a bad thing happened! + console.error(err); + return cb(err); + } + + if (!room) { + return cb('Room does not exist.'); + } + + if(room.private && !room.owner.equals(options.user.id)) { + return cb('Only owner can change private room.'); + } + + getParticipants(room, options, function(err, participants) { + if (err) { + // Oh noes, a bad thing happened! + console.error(err); + return cb(err); + } + + room.name = options.name; + // DO NOT UPDATE SLUG + // room.slug = options.slug; + room.description = options.description; + + if (room.private) { + room.password = options.password; + room.participants = participants; + } + + room.save(function(err, room) { + if (err) { + console.error(err); + return cb(err); + } + room = room; + cb(null, room); + this.core.emit('rooms:update', room); + }.bind(this)); + }.bind(this)); + }.bind(this)); +}; + +RoomManager.prototype.archive = function(roomId, cb) { + var Room = mongoose.model('Room'); + + Room.findById(roomId, function(err, room) { + if (err) { + // Oh noes, a bad thing happened! + console.error(err); + return cb(err); + } + + if (!room) { + return cb('Room does not exist.'); + } + + room.archived = true; + room.save(function(err, room) { + if (err) { + console.error(err); + return cb(err); + } + cb(null, room); + this.core.emit('rooms:archive', room); + + }.bind(this)); + }.bind(this)); +}; + +RoomManager.prototype.list = function(options, cb) { + options = options || {}; + + options = helpers.sanitizeQuery(options, { + defaults: { + take: 500 + }, + maxTake: 5000 + }); + + var Room = mongoose.model('Room'); + + var find = Room.find({ + archived: { $ne: true }, + $or: [ + {private: {$exists: false}}, + {private: false}, + + {owner: options.userId}, + + {participants: options.userId}, + + {password: {$exists: true, $ne: ''}} + ] + }); + + if (options.skip) { + find.skip(options.skip); + } + + if (options.take) { + find.limit(options.take); + } + + if (options.sort) { + var sort = options.sort.replace(',', ' '); + find.sort(sort); + } else { + find.sort('-lastActive'); + } + + find.populate('participants'); + + find.exec(function(err, rooms) { + if (err) { + return cb(err); + } + + _.each(rooms, function(room) { + this.sanitizeRoom(options, room); + }, this); + + if (options.users && !options.sort) { + rooms = _.sortByAll(rooms, ['userCount', 'lastActive']) + .reverse(); + } + + cb(null, rooms); + + }.bind(this)); +}; + +RoomManager.prototype.sanitizeRoom = function(options, room) { + var authorized = options.userId && room.isAuthorized(options.userId); + + if (options.users) { + if (authorized) { + room.users = this.core.presence + .getUsersForRoom(room.id.toString()); + } else { + room.users = []; + } + } +}; + +RoomManager.prototype.findOne = function(options, cb) { + var Room = mongoose.model('Room'); + Room.findOne(options.criteria) + .populate('participants').exec(function(err, room) { + + if (err) { + return cb(err); + } + + this.sanitizeRoom(options, room); + cb(err, room); + + }.bind(this)); +}; + +RoomManager.prototype.get = function(options, cb) { + var identifier; + + if (typeof options === 'string') { + identifier = options; + options = {}; + options.identifier = identifier; + } else { + identifier = options.identifier; + } + + options.criteria = { + _id: identifier, + archived: { $ne: true } + }; + + this.findOne(options, cb); +}; + +RoomManager.prototype.slug = function(options, cb) { + var identifier; + + if (typeof options === 'string') { + identifier = options; + options = {}; + options.identifier = identifier; + } else { + identifier = options.identifier; + } + + options.criteria = { + slug: identifier, + archived: { $ne: true } + }; + + this.findOne(options, cb); +}; + +module.exports = RoomManager; diff --git a/app/core/usermessages.js b/app/core/usermessages.js new file mode 100644 index 0000000..e69ea75 --- /dev/null +++ b/app/core/usermessages.js @@ -0,0 +1,127 @@ +'use strict'; + +var _ = require('lodash'), + mongoose = require('mongoose'), + helpers = require('./helpers'); + +function UserMessageManager(options) { + this.core = options.core; +} + +// options.currentUser, options.user + +UserMessageManager.prototype.onMessageCreated = function(message, user, options, cb) { + var User = mongoose.model('User'); + + User.findOne(message.owner, function(err, owner) { + if (err) { + console.error(err); + return cb(err); + } + if (cb) { + cb(null, message, user, owner); + } + + this.core.emit('user-messages:new', message, user, owner, options.data); + }.bind(this)); +}; + +UserMessageManager.prototype.create = function(options, cb) { + var UserMessage = mongoose.model('UserMessage'), + User = mongoose.model('User'); + + User.findById(options.user, function(err, user) { + if (err) { + console.error(err); + return cb(err); + } + if (!user) { + return cb('User does not exist.'); + } + + var data = { + users: [options.owner, options.user], + owner: options.owner, + text: options.text + }; + + var message = new UserMessage(data); + + // Test if this message is OTR + if (data.text.match(/^\?OTR/)) { + message._id = 'OTR'; + this.onMessageCreated(message, user, options, cb); + } else { + message.save(function(err) { + if (err) { + console.error(err); + return cb(err); + } + this.onMessageCreated(message, user, options, cb); + }.bind(this)); + } + }.bind(this)); +}; + +UserMessageManager.prototype.list = function(options, cb) { + options = options || {}; + + if (!options.room) { + return cb(null, []); + } + + options = helpers.sanitizeQuery(options, { + defaults: { + reverse: true, + take: 500 + }, + maxTake: 5000 + }); + + var UserMessage = mongoose.model('Message'); + + var find = UserMessage.find({ + users: { $all: [options.currentUser, options.user] } + }); + + if (options.since_id) { + find.where('_id').gt(options.since_id); + } + + if (options.from) { + find.where('posted').gt(options.from); + } + + if (options.to) { + find.where('posted').lte(options.to); + } + + if (options.expand) { + var includes = options.expand.split(','); + + if (_.includes(includes, 'owner')) { + find.populate('owner', 'id username displayName email avatar'); + } + } + + if (options.skip) { + find.skip(options.skip); + } + + if (options.reverse) { + find.sort({ 'posted': -1 }); + } else { + find.sort({ 'posted': 1 }); + } + + find.limit(options.take) + .exec(function(err, messages) { + if (err) { + console.error(err); + return cb(err); + } + cb(null, messages); + }); +}; + +module.exports = UserMessageManager; diff --git a/app/core/users.js b/app/core/users.js new file mode 100644 index 0000000..19aba48 --- /dev/null +++ b/app/core/users.js @@ -0,0 +1,47 @@ +'use strict'; + +var mongoose = require('mongoose'), + helpers = require('./helpers'); + +function UserManager(options) { + this.core = options.core; +} + +UserManager.prototype.list = function(options, cb) { + options = options || {}; + + options = helpers.sanitizeQuery(options, { + defaults: { + take: 500 + }, + maxTake: 5000 + }); + + var User = mongoose.model('User'); + + var find = User.find(); + + if (options.skip) { + find.skip(options.skip); + } + + if (options.take) { + find.limit(options.take); + } + + find.exec(cb); +}; + +UserManager.prototype.get = function(identifier, cb) { + var User = mongoose.model('User'); + User.findById(identifier, cb); +}; + +UserManager.prototype.username = function(username, cb) { + var User = mongoose.model('User'); + User.findOne({ + username: username + }, cb); +}; + +module.exports = UserManager; diff --git a/app/middlewares/cleanupFiles.js b/app/middlewares/cleanupFiles.js new file mode 100644 index 0000000..437b8df --- /dev/null +++ b/app/middlewares/cleanupFiles.js @@ -0,0 +1,43 @@ +'use strict'; + +var fs = require('fs'), + _ = require('lodash'), + async = require('async'), + onFinished = require('on-finished'); + +function cleanupReqFiles(req, cb) { + if (!req.files) { + return cb(); + } + + var files = _.chain(req.files) + .map(function(x) { return x; }) + .flatten() + .value(); + + async.each(files, function(file, callback) { + fs.stat(file.path, function(err, stats) { + if (!err && stats.isFile()) { + fs.unlink(file.path, function(e) { + if (e) { + console.error(e); + } + + callback(); + }); + } + }); + }); +} + +module.exports = function(req, res, next) { + res.on('error', function() { + cleanupReqFiles(req); + }); + + onFinished(res, function () { + cleanupReqFiles(req); + }); + + next(); +}; diff --git a/app/middlewares/index.js b/app/middlewares/index.js new file mode 100644 index 0000000..d881447 --- /dev/null +++ b/app/middlewares/index.js @@ -0,0 +1,9 @@ +// +// Middlewares +// + +'use strict'; + +var requireDirectory = require('require-directory'); + +module.exports = requireDirectory(module); diff --git a/app/middlewares/requireLogin.js b/app/middlewares/requireLogin.js new file mode 100644 index 0000000..5d7785e --- /dev/null +++ b/app/middlewares/requireLogin.js @@ -0,0 +1,44 @@ +// +// Require Login +// + +'use strict'; + +var passport = require('passport'); + +function getMiddleware(fail) { + return function(req, res, next) { + if (req.user) { + next(); + return; + } + + if (req.headers && req.headers.authorization) { + var parts = req.headers.authorization.split(' '); + if (parts.length === 2) { + var scheme = parts[0], + auth; + + if (/^Bearer$/i.test(scheme)) { + auth = passport.authenticate('bearer', { session: false }); + return auth(req, res, next); + } + + if (/^Basic$/i.test(scheme)) { + auth = passport.authenticate('basic', { session: false }); + return auth(req, res, next); + } + } + } + + fail(req, res); + }; +} + +module.exports = getMiddleware(function(req, res) { + res.sendStatus(401); +}); + +module.exports.redirect = getMiddleware(function(req, res) { + res.redirect('/login'); +}); diff --git a/app/middlewares/roomRoute.js b/app/middlewares/roomRoute.js new file mode 100644 index 0000000..517c773 --- /dev/null +++ b/app/middlewares/roomRoute.js @@ -0,0 +1,35 @@ +// +// Require Login +// + +'use strict'; + +var mongoose = require('mongoose'); + +module.exports = function(req, res, next) { + var room = req.params.room; + + if (!room) { + return res.sendStatus(404); + } + + var Room = mongoose.model('Room'); + + Room.findByIdOrSlug(room, function(err, room) { + if (err) { + return res.sendStatus(400); + } + + if (!room) { + return res.sendStatus(404); + } + + var roomId = room._id.toString(); + + req.params.room = roomId; + req.body.room = roomId; + req.query.room = roomId; + + next(); + }); +}; diff --git a/app/misc/art.txt b/app/misc/art.txt new file mode 100644 index 0000000..6aa166c --- /dev/null +++ b/app/misc/art.txt @@ -0,0 +1,6 @@ +██╗ ███████╗████████╗███████╗ ██████╗██╗ ██╗ █████╗ ████████╗ +██║ ██╔════╝╚══██╔══╝██╔════╝ ██╔════╝██║ ██║██╔══██╗╚══██╔══╝ +██║ █████╗ ██║ ███████╗ ██║ ███████║███████║ ██║ +██║ ██╔══╝ ██║ ╚════██║ ██║ ██╔══██║██╔══██║ ██║ +███████╗███████╗ ██║ ███████║ ╚██████╗██║ ██║██║ ██║ ██║ +╚══════╝╚══════╝ ╚═╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ \ No newline at end of file diff --git a/app/misc/robots.txt b/app/misc/robots.txt new file mode 100644 index 0000000..77470cb --- /dev/null +++ b/app/misc/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/app/models/file.js b/app/models/file.js new file mode 100644 index 0000000..b612eeb --- /dev/null +++ b/app/models/file.js @@ -0,0 +1,61 @@ +'use strict'; + +var mongoose = require('mongoose'); +var Schema = mongoose.Schema, + ObjectId = Schema.ObjectId; + +var FileSchema = new Schema({ + room: { + type: ObjectId, + ref: 'Room', + required: true + }, + owner: { + type: ObjectId, + ref: 'User', + required: true + }, + name: { + type: String, + required: true, + trim: true + }, + type: { + type: String, + required: true + }, + size: { + type: Number, + required: true + }, + uploaded: { + type: Date, + default: Date.now + } +}); + +FileSchema.virtual('url').get(function() { + return 'files/' + this._id + '/' + encodeURIComponent(this.name); +}); + +FileSchema.method('toJSON', function(user) { + var data = { + id: this._id, + owner: this.owner, + name: this.name, + type: this.type, + size: Math.floor(this.size / 1024) + 'kb', + url: this.url, + uploaded: this.uploaded + }; + + if (this.room._id) { + data.room = this.room.toJSON(user); + } else { + data.room = this.room; + } + + return data; +}); + +module.exports = mongoose.model('File', FileSchema); diff --git a/app/models/index.js b/app/models/index.js new file mode 100644 index 0000000..eb49506 --- /dev/null +++ b/app/models/index.js @@ -0,0 +1,9 @@ +// +// Models +// + +'use strict'; + +var requireDirectory = require('require-directory'); + +module.exports = requireDirectory(module); diff --git a/app/models/message.js b/app/models/message.js new file mode 100644 index 0000000..9e713c5 --- /dev/null +++ b/app/models/message.js @@ -0,0 +1,60 @@ +// +// Message +// + +'use strict'; + +var mongoose = require('mongoose'); +var ObjectId = mongoose.Schema.Types.ObjectId; + +var MessageSchema = new mongoose.Schema({ + room: { + type: ObjectId, + ref: 'Room', + required: true + }, + owner: { + type: ObjectId, + ref: 'User', + required: true + }, + text: { + type: String, + required: true + }, + posted: { + type: Date, + default: Date.now, + index: true + } +}); + +MessageSchema.index({ text: 'text', room: 1, posted: -1, _id: 1 }); + +// EXPOSE ONLY CERTAIN FIELDS +// This helps ensure that the client gets +// data that can be digested properly +MessageSchema.method('toJSON', function(user) { + var data = { + id: this._id, + text: this.text, + posted: this.posted, + + // if populate('owner') and user's been deleted - owner will be null + // otherwise it will be an id or undefined + owner: this.owner || { + displayName: '[Deleted User]', + username: '_deleted_user_' + } + }; + + if (this.room._id) { + data.room = this.room.toJSON(user); + } else { + data.room = this.room; + } + + return data; +}); + +module.exports = mongoose.model('Message', MessageSchema); diff --git a/app/models/room.js b/app/models/room.js new file mode 100644 index 0000000..fd6651d --- /dev/null +++ b/app/models/room.js @@ -0,0 +1,221 @@ +// +// Room +// + +'use strict'; + +var mongoose = require('mongoose'), + uniqueValidator = require('mongoose-unique-validator'), + bcrypt = require('bcryptjs'); + +var ObjectId = mongoose.Schema.Types.ObjectId; + +var RoomSchema = new mongoose.Schema({ + slug: { + type: String, + required: true, + trim: true, + lowercase: true, + unique: true, + match: /^[a-z0-9_]+$/i + }, + archived: { + type: Boolean, + default: false + }, + name: { + type: String, + required: true, + trim: true + }, + description: { + type: String, + trim: true + }, + owner: { + type: ObjectId, + ref: 'User', + required: true + }, + participants: [{ // We can have an array per role + type: ObjectId, + ref: 'User' + }], + messages: [{ + type: ObjectId, + ref: 'Message' + }], + created: { + type: Date, + default: Date.now + }, + lastActive: { + type: Date, + default: Date.now + }, + private: { + type: Boolean, + default: false + }, + password: { + type: String, + required: false//only for password-protected room + } +}); + +RoomSchema.virtual('handle').get(function() { + return this.slug || this.name.replace(/\W/i, ''); +}); + +RoomSchema.virtual('hasPassword').get(function() { + return !!this.password; +}); + +RoomSchema.pre('save', function(next) { + var room = this; + if (!room.password || !room.isModified('password')) { + return next(); + } + + bcrypt.hash(room.password, 10, function(err, hash) { + if (err) { + return next(err); + } + room.password = hash; + next(); + }); +}); + +RoomSchema.plugin(uniqueValidator, { + message: 'Expected {PATH} to be unique' +}); + +RoomSchema.method('isAuthorized', function(userId) { + if (!userId) { + return false; + } + + userId = userId.toString(); + + // Check if userId doesn't match MongoID format + if (!/^[a-f\d]{24}$/i.test(userId)) { + return false; + } + + if (!this.password && !this.private) { + return true; + } + + if (this.owner.equals(userId)) { + return true; + } + + return this.participants.some(function(participant) { + if (participant._id) { + return participant._id.equals(userId); + } + + if (participant.equals) { + return participant.equals(userId); + } + + if (participant.id) { + return participant.id === userId; + } + + return participant === userId; + }); +}); + +RoomSchema.method('canJoin', function(options, cb) { + var userId = options.userId, + password = options.password, + saveMembership = options.saveMembership; + + if (this.isAuthorized(userId)) { + return cb(null, true); + } + + if (!this.password) { + return cb(null, false); + } + + bcrypt.compare(password || '', this.password, function(err, isMatch) { + if(err) { + return cb(err); + } + + if (!isMatch) { + return cb(null, false); + } + + if (!saveMembership) { + return cb(null, true); + } + + this.participants.push(userId); + + this.save(function(err) { + if(err) { + return cb(err); + } + + cb(null, true); + }); + + }.bind(this)); +}); + +RoomSchema.method('toJSON', function(user) { + var userId = user ? (user._id || user.id || user) : null; + var authorized = false; + + if (userId) { + authorized = this.isAuthorized(userId); + } + + var room = this.toObject(); + + var data = { + id: room._id, + slug: room.slug, + name: room.name, + description: room.description, + lastActive: room.lastActive, + created: room.created, + owner: room.owner, + private: room.private, + hasPassword: this.hasPassword, + participants: [] + }; + + if (room.private && authorized) { + var participants = this.participants || []; + data.participants = participants.map(function(user) { + return user.username ? user.username : user; + }); + } + + if (this.users) { + data.users = this.users; + data.userCount = this.users.length; + } + + return data; + }); + +RoomSchema.statics.findByIdOrSlug = function(identifier, cb) { + var opts = { + archived: { $ne: true } + }; + + if (identifier.match(/^[0-9a-fA-F]{24}$/)) { + opts.$or = [{_id: identifier}, {slug: identifier}]; + } else { + opts.slug = identifier; + } + + this.findOne(opts, cb); +}; + +module.exports = mongoose.model('Room', RoomSchema); diff --git a/app/models/test.js b/app/models/test.js new file mode 100644 index 0000000..52f261c --- /dev/null +++ b/app/models/test.js @@ -0,0 +1,21 @@ +var debug = require('debug')('model->test'); +var bcrypt = require('bcryptjs'), + crypto = require('crypto'), + md5 = require('md5'), + hash = require('node_hash'), + mongoose = require('mongoose'), + uniqueValidator = require('mongoose-unique-validator'), + validate = require('mongoose-validate'), + settings = require('./../config'); + +var ObjectId = mongoose.Schema.Types.ObjectId; +debug('in test model'); +var TestSchema = new mongoose.Schema({ + testField: { + type: String, + required: true//, + // trim: true + } +}); + +module.exports = mongoose.model('Test', TestSchema); diff --git a/app/models/user.js b/app/models/user.js new file mode 100644 index 0000000..0f7b6cf --- /dev/null +++ b/app/models/user.js @@ -0,0 +1,287 @@ +// +// User +// + +'use strict'; +var debug = require('debug')('model->user'); +var bcrypt = require('bcryptjs'), + crypto = require('crypto'), + md5 = require('md5'), + hash = require('node_hash'), + mongoose = require('mongoose'), + uniqueValidator = require('mongoose-unique-validator'), + validate = require('mongoose-validate'), + settings = require('./../config'); + +var ObjectId = mongoose.Schema.Types.ObjectId; + +var UserSchema = new mongoose.Schema({ + provider: { + type: String, + required: true, + trim: true + }, + uid: { + type: String, + required: false, + trim: true, + validate: [function(v) { + debug('validation of uid: ' + v.length +"," + v); + return (v.length <= 40); + }, 'invalid ldap/kerberos username'] + }, + email: { + type: String, + required: true, + trim: true, + lowercase: true, + unique: true, + validate: [ validate.email, 'invalid email address' ] + }, + password: { + type: String, + required: false, // Only required if local + trim: true, + match: new RegExp(settings.auth.local.passwordRegex), + set: function(value) { + // User can only change their password if it's a local account + if (this.local) { + return value; + } + return this.password; + } + }, + token: { + type: String, + required: false, + trim: true + }, + firstName: { + type: String, + required: true, + trim: true + }, + lastName: { + type: String, + required: true, + trim: true + }, + username: { + type: String, + required: true, + trim: true, + lowercase: true, + unique: true, + match: /^[\w][\w\-\.]*[\w]$/i + }, + displayName: { + type: String, + required: true, + trim: true + }, + joined: { + type: Date, + default: Date.now + }, + status: { + type: String, + trim: true + }, + rooms: [{ + type: ObjectId, + ref: 'Room' + }], + messages: [{ + type: ObjectId, + ref: 'Message' + }] +}, { + toObject: { + virtuals: true + }, + toJSON: { + virtuals: true + } +}); + +UserSchema.virtual('local').get(function() { + return this.provider === 'local'; +}); + +UserSchema.virtual('avatar').get(function() { + return md5(this.email); +}); + +UserSchema.pre('save', function(next) { + var user = this; + debug('presaving'); + if (!user.isModified('password')) { + return next(); + } + + bcrypt.hash(user.password, 10, function(err, hash) { + if (err) { + debug('err:' + err); + return next(err); + } + user.password = hash; + next(); + }); +}); + +UserSchema.statics.findByIdentifier = function(identifier, cb) { + var opts = {}; + + if (identifier.match(/^[0-9a-fA-F]{24}$/)) { + opts.$or = [{_id: identifier}, {username: identifier}]; + } else if (identifier.indexOf('@') === -1) { + opts.username = identifier; + } else { + opts.email = identifier; + } + + this.findOne(opts, cb); +}; + +UserSchema.methods.generateToken = function(cb) { + if (!this._id) { + return cb('User needs to be saved.'); + } + + crypto.randomBytes(24, function(ex, buf) { + var password = buf.toString('hex'); + + bcrypt.hash(password, 10, function(err, hash) { + if (err) { + return cb(err); + } + + this.token = hash; + + var userToken = new Buffer( + this._id.toString() + ':' + password + ).toString('base64'); + + cb(null, userToken); + + }.bind(this)); + }.bind(this)); +}; + +UserSchema.statics.findByToken = function(token, cb) { + + if (!token) { + return cb(null, null); + } + + var tokenParts = new Buffer(token, 'base64').toString('ascii').split(':'), + userId = tokenParts[0], + hash = tokenParts[1]; + + if (!userId.match(/^[0-9a-fA-F]{24}$/)) { + cb(null, null); + } + + this.findById(userId, function(err, user) { + if (err) { + return cb(err); + } + + if (!user) { + return cb(null, null); + } + + bcrypt.compare(hash, user.token, function(err, isMatch) { + if (err) { + return cb(err); + } + + if (isMatch) { + return cb(null, user); + } + + cb(null, null); + }); + }); +}; + +UserSchema.methods.comparePassword = function(password, cb) { + + var local = settings.auth.local, + salt = local && local.salt; + + // Legacy password hashes + if (salt && (hash.sha256(password, salt) === this.password)) { + return cb(null, true); + } + + // Current password hashes + bcrypt.compare(password, this.password, function(err, isMatch) { + + if (err) { + return cb(err); + } + + if (isMatch) { + return cb(null, true); + } + + cb(null, false); + + }); + +}; + +UserSchema.statics.authenticate = function(identifier, password, cb) { + this.findByIdentifier(identifier, function(err, user) { + if (err) { + return cb(err); + } + // Does the user exist? + if (!user) { + return cb(null, null, 0); + } + // Is this a local user? + if (user.provider !== 'local') { + return cb(null, null, 0); + } + + // Is password okay? + user.comparePassword(password, function(err, isMatch) { + if (err) { + return cb(err); + } + if (isMatch) { + return cb(null, user); + } + // Bad password + return cb(null, null, 1); + }); + }); +}; + +UserSchema.plugin(uniqueValidator, { + message: 'Expected {PATH} to be unique' +}); + +// EXPOSE ONLY CERTAIN FIELDS +// It's really important that we keep +// stuff like password private! +UserSchema.method('toJSON', function() { + return { + id: this._id, + firstName: this.firstName, + lastName: this.lastName, + username: this.username, + displayName: this.displayName, + avatar: this.avatar + }; +}); +debug('adding model' + UserSchema); +var s = mongoose.model('User', UserSchema); + +var assert = require('assert'); +assert(mongoose.model('User')); + +debug('user schema registered'); +module.exports = s diff --git a/app/models/usermessage.js b/app/models/usermessage.js new file mode 100644 index 0000000..91cfb47 --- /dev/null +++ b/app/models/usermessage.js @@ -0,0 +1,54 @@ +// +// Message +// + +'use strict'; + +var mongoose = require('mongoose'), + settings = require('./../config'); + +var ObjectId = mongoose.Schema.Types.ObjectId; + +var MessageSchema = new mongoose.Schema({ + users: [{ + type: ObjectId, + ref: 'User' + }], + owner: { + type: ObjectId, + ref: 'User', + required: true + }, + text: { + type: String, + required: true + }, + posted: { + type: Date, + default: Date.now + } +}); + +if (settings.private.expire !== false) { + var defaultExpire = 6 * 60; // 6 hours + + MessageSchema.index({ posted: 1 }, { + expireAfterSeconds: (settings.private.expire || defaultExpire) * 60 + }); +} + +MessageSchema.index({ users: 1, posted: -1, _id: 1 }); + +// EXPOSE ONLY CERTAIN FIELDS +// This helps ensure that the client gets +// data that can be digested properly +MessageSchema.method('toJSON', function() { + return { + id: this._id, + owner: this.owner, + text: this.text, + posted: this.posted + }; +}); + +module.exports = mongoose.model('UserMessage', MessageSchema); diff --git a/app/plugins.js b/app/plugins.js new file mode 100644 index 0000000..b567c0b --- /dev/null +++ b/app/plugins.js @@ -0,0 +1,26 @@ +'use strict'; + +function PluginManager() { + this.types = [ + 'auth', + 'files' + ]; +} + +PluginManager.prototype.getPlugin = function(key, type) { + var name = 'lets-chat-' + key; + var plugin = require(name); + + if (!type) { + return plugin; + } + + var Provider = plugin && plugin[type]; + if (Provider) { + return Provider; + } + + throw 'Module "' + name + '" is not a ' + type + ' provider'; +}; + +module.exports = new PluginManager(); diff --git a/app/tests/mongo.unit.spec.js b/app/tests/mongo.unit.spec.js new file mode 100644 index 0000000..1a0f4c8 --- /dev/null +++ b/app/tests/mongo.unit.spec.js @@ -0,0 +1,57 @@ +var debug = require('debug')('model->test'); +var mongoose = require('mongoose'); + + +var TestSchema = new mongoose.Schema({ + testField: { + type: String, + required: true//, + // trim: true + } +}); +var Entity = mongoose.model('Test1', TestSchema); +var enitytToSave = new Entity({testField:'test1123232'}); +var dbRemote = 'mongodb://admin:hpadmin@ds037415.mongolab.com:37415/hp_mongo'; +var db_local = 'mongodb://192.168.99.100:27017/hp_mongo'; +var db_docker = 'mongodb://mongo:27017/hp_mongo'; + +describe('sanity tests', function(done){ + +it('test mongo connection' , function(done){ + + mongoose.connect(dbRemote, function(err) { + + if (err) { + console.log(err); + throw err; + } + + console.log('mongo is connected'); + enitytToSave.save(function(err) { + //done(err); + debug('after save:' + err ); + if (err) { + console.log('saved process finished with error') + return done(err); + } + debug('saved succesfully'); + console.log('test entity succesfully saved'); + mongoose.connection.close(); + return done(null); + }); + }); + }); + +}); + + +/*Object.keys(user).forEach(function(key) { + debug('key:' + key); + userToSave.set(key, user[key]); +});*/ + +/* User.findByToken(username, function(err, user) { + if (err) { return done(err); } + if (!user) { return done(null, false); } + return done(null, user); +});*/ diff --git a/app/tests/room.unit.spec.js b/app/tests/room.unit.spec.js new file mode 100644 index 0000000..580434e --- /dev/null +++ b/app/tests/room.unit.spec.js @@ -0,0 +1,5 @@ +describe('entity tests', function(){ + it('create user', function(done){ + done(); + }) +}) diff --git a/app/tests/user.integration.spec.js b/app/tests/user.integration.spec.js new file mode 100644 index 0000000..00c9219 --- /dev/null +++ b/app/tests/user.integration.spec.js @@ -0,0 +1,111 @@ +var debug = require('debug')('users integration test'); +var assert = require('assert'); + + +debug('test file loaded'); +/*if (process.env.NO_MOCHA){ + function describe(name, callback){ + callback(function done (){}); + } + var beforeEach = function describe(callback){ + callback(function done (){}); + }; +}*/ +//var afterEach = beforeEach; +describe.skip('user CRUD tests', function(){ + + var shortId = require('shortid'); + var db = 'mongodb://mongo:27017/hp_mongo'; + debug('models->' + JSON.stringify(models)); + var mongoose = require('mongoose'); + var models = require('../models');//jshint ignore:line + var Entity = models.test; //mongoose.model('User'); + assert (Entity); + var user = {}; + var guid = shortId.generate(); + beforeEach(function(done){ + /*mongoose.connect(db, function(err) { + + if (err) { + console.log(err); + throw err; + } + + console.log('mongo is connected'); + done(); + });*/ + + //var User = mongoose.model('User'); + + mongoose.connection.on('error', function (err) { + throw new Error(err); + }); + + mongoose.connection.on('disconnected', function() { + //throw new Error('Could not connect to database'); + done();//'cound not connect'); + }); + }); + + afterEach(function(done){ +// find each person with a last nam e matching 'Ghost', selecting the `name` and `occupation` fields + + console.log('after each'); + done(); + console.log('find user' + user.uid); + Entity.findOne({ 'uid': user.uid}, function (err, user) { + debug('after user was removed'); + + if (err) throw Error(); + console.dir(user); + //console.log('%s was removed', user.username); // Space Ghost is a talk show host. + debug('user was deleted: ' + user.username); + done(); + }); + + }); + + + it('create user', function(done){ + + console.log('create user test') + //user.provider = 'github'; + //user.username = 'verchol' + guid; + var id = require('node-uuid').v4(); + debug('id' + id); + //user.uid = id; + user.testField = "moshe";// + guid; + /*user.email = "verchol" + guid + '@gmail.com'; + user.passowrd = "welcome1234!^"; + user.firstName = 'Oleg1'; + user.lastName = 'Verhovsky1'; + user.displayName = 'gever'; + user.creationDate = new Date();*/ + var enitytToSave = new Entity(user); + /*Object.keys(user).forEach(function(key) { + debug('key:' + key); + userToSave.set(key, user[key]); + });*/ + + debug(JSON.stringify(user)); + /* User.findByToken(username, function(err, user) { + if (err) { return done(err); } + if (!user) { return done(null, false); } + return done(null, user); + });*/ + enitytToSave.save(function(err) { + done(err); + debug('after save:' + err ); + if (err) { + console.log('saved process finished with error') + return done(err); + } + debug('saved succesfully'); + console.log('saved succesfully'); + mongoose.connection.close(); + return done(); + }); + + }); + +}); diff --git a/app/xmpp/event-listener.js b/app/xmpp/event-listener.js new file mode 100644 index 0000000..a886853 --- /dev/null +++ b/app/xmpp/event-listener.js @@ -0,0 +1,53 @@ +'use strict'; + +var settings = require('./../config'), + _ = require('lodash'), + util = require('util'); + + +function EventListener(core) { + this.core = core; + + this.getConnectionsForRoom = this.getConnectionsForRoom.bind(this); + this.send = this.send.bind(this); +} + +EventListener.prototype.getConnectionsForRoom = function(roomId) { + var room = this.core.presence.rooms.get(roomId); + + if (!room) { + return []; + } + + return room.connections.query({ type: 'xmpp' }); +}; + +EventListener.prototype.send = function() { + var connection = arguments[0], + msgs = Array.prototype.slice.call(arguments, 1); + + msgs = _.flatten(msgs); + + msgs.forEach(function(msg) { + if (settings.xmpp.debug.handled) { + console.log(msg.root().toString().yellow); + } + connection.client.send(msg); + }); +}; + +EventListener.extend = function(options) { + var listener = function() { + EventListener.apply(this, arguments); + this.then = this.then.bind(this); + }; + + util.inherits(listener, EventListener); + + listener.prototype.on = options.on; + listener.prototype.then = options.then; + + return listener; +}; + +module.exports = EventListener; diff --git a/app/xmpp/events/message-created.js b/app/xmpp/events/message-created.js new file mode 100644 index 0000000..d4a483f --- /dev/null +++ b/app/xmpp/events/message-created.js @@ -0,0 +1,47 @@ +'use strict'; + +var Message = require('node-xmpp-server').Message, + EventListener = require('./../event-listener'); + +var mentionPattern = /\B@(\w+)(?!@)\b/g; + +module.exports = EventListener.extend({ + + on: 'messages:new', + + then: function(msg, room, user, data) { + var connections = this.getConnectionsForRoom(room._id); + + connections.forEach(function(connection) { + var text = msg.text; + + var mentions = msg.text.match(mentionPattern); + + if (mentions && mentions.indexOf('@' + connection.user.username) > -1) { + text = connection.nickname(room.slug) + ': ' + text; + } + + var id = msg._id; + if (connection.user.username === user.username) { + id = data && data.id || id; + } + + var stanza = new Message({ + id: id, + type: 'groupchat', + to: connection.getRoomJid(room.slug), + from: connection.getRoomJid(room.slug, user.username) + }); + + stanza.c('active', { + xmlns: 'http://jabber.org/protocol/chatstates' + }); + + stanza.c('body').t(text); + + this.send(connection, stanza); + + }, this); + } + +}); diff --git a/app/xmpp/events/room-archived.js b/app/xmpp/events/room-archived.js new file mode 100644 index 0000000..18a5594 --- /dev/null +++ b/app/xmpp/events/room-archived.js @@ -0,0 +1,40 @@ +'use strict'; + +var Presence = require('node-xmpp-server').Presence, + EventListener = require('./../event-listener'); + +module.exports = EventListener.extend({ + + on: 'rooms:archived', + + then: function(room) { + var connections = this.getConnectionsForRoom(room._id); + + connections.forEach(function(connection) { + // Kick connection from room! + + var presence = new Presence({ + to: connection.jid(room.slug), + from: connection.jid(room.slug), + type: 'unavailable' + }); + + var x = presence + .c('x', { + xmlns: 'http://jabber.org/protocol/muc#user' + }); + + x.c('item', { + jid: connection.jid(), + affiliation: 'none', + role: 'none' + }); + + x.c('destroy').c('reason').t('Room closed'); + + this.send(connection, presence); + + }, this); + } + +}); diff --git a/app/xmpp/events/room-updated.js b/app/xmpp/events/room-updated.js new file mode 100644 index 0000000..5b21d02 --- /dev/null +++ b/app/xmpp/events/room-updated.js @@ -0,0 +1,28 @@ +'use strict'; + +var Message = require('node-xmpp-server').Message, + EventListener = require('./../event-listener'); + +module.exports = EventListener.extend({ + + on: 'rooms:update', + + then: function(room) { + var connections = this.getConnectionsForRoom(room._id); + + connections.forEach(function(connection) { + + var message = new Message({ + to: connection.jid(room.slug), + from: connection.jid(room.slug), + type: 'groupchat' + }); + + message.c('subject').t(room.name + ' | ' + room.description); + + this.send(connection, message); + + }, this); + } + +}); diff --git a/app/xmpp/events/user-avatar-ready.js b/app/xmpp/events/user-avatar-ready.js new file mode 100644 index 0000000..f51c379 --- /dev/null +++ b/app/xmpp/events/user-avatar-ready.js @@ -0,0 +1,47 @@ +'use strict'; + +var _ = require('lodash'), + Presence = require('node-xmpp-server').Presence, + settings = require('./../../config'), + EventListener = require('./../event-listener'); + +module.exports = EventListener.extend({ + + on: 'avatar-cache:update', + + then: function(user) { + if (!settings.private.enable) { + return; + } + + var user_connections = this.core.presence.system.connections.query({ + type: 'xmpp', + userid: user.id + }); + + if (!user_connections.length) { + // Don't publish presence for this user + return; + } + + var connections = this.core.presence.system.connections.query({ + type: 'xmpp' + }); + + _.each(connections, function(connection) { + if (connection.user.id === user.id) { + return; + } + + // Reannounce presence + var presence = new Presence({ + from: connection.getUserJid(user.username) + }); + + connection.populateVcard(presence, user, this.core); + + this.send(connection, presence); + }, this); + } + +}); diff --git a/app/xmpp/events/user-connect.js b/app/xmpp/events/user-connect.js new file mode 100644 index 0000000..b2724da --- /dev/null +++ b/app/xmpp/events/user-connect.js @@ -0,0 +1,71 @@ +'use strict'; + +var _ = require('lodash'), + IQ = require('node-xmpp-server').IQ, + Presence = require('node-xmpp-server').Presence, + settings = require('./../../config'), + EventListener = require('./../event-listener'); + +module.exports = EventListener.extend({ + + on: 'connect', + + then: function(connection) { + if (!settings.private.enable) { + return; + } + + if (connection.type !== 'xmpp') { + return; + } + + var existing = this.core.presence.system.connections.query({ + userId: connection.user.id, + type: 'xmpp' + }); + + if (existing.length > 1) { + // Was already connected via XMPP + return; + } + + var connections = this.core.presence.system.connections.query({ + type: 'xmpp' + }); + + _.each(connections, function(x) { + if (x.user.id === connection.user.id) { + return; + } + + // Update rosters + var roster = new IQ({ + id: connection.user.id, + type: 'set', + to: x.jid() + }); + + roster.c('query', { + xmlns: 'jabber:iq:roster' + }).c('item', { + jid: x.getUserJid(connection.user.username), + name: connection.user.displayName, + subscription: 'both' + }).c('group').t('Let\'s Chat'); + + this.send(x, roster); + + + // Announce presence + var presence = new Presence({ + from: x.getUserJid(connection.user.username) + }); + + x.populateVcard(presence, connection.user, this.core); + + this.send(x, presence); + + }, this); + } + +}); diff --git a/app/xmpp/events/user-disconnect.js b/app/xmpp/events/user-disconnect.js new file mode 100644 index 0000000..2c6bfe8 --- /dev/null +++ b/app/xmpp/events/user-disconnect.js @@ -0,0 +1,51 @@ +'use strict'; + +var _ = require('lodash'), + Presence = require('node-xmpp-server').Presence, + settings = require('./../../config'), + EventListener = require('./../event-listener'); + +module.exports = EventListener.extend({ + + on: 'disconnect', + + then: function(connection) { + if (!settings.private.enable) { + return; + } + + if (connection.type !== 'xmpp') { + return; + } + + var existing = this.core.presence.system.connections.query({ + userId: connection.user.id, + type: 'xmpp' + }); + + if (existing.length > 0) { + // Still has other XMPP connections + return; + } + + var connections = this.core.presence.system.connections.query({ + type: 'xmpp' + }); + + _.each(connections, function(x) { + if (x.user.id === connection.user.id) { + return; + } + + var presence = new Presence({ + to: x.jid(), + from: x.getUserJid(connection.user.username), + type: 'unavailable' + }); + + this.send(x, presence); + + }, this); + } + +}); diff --git a/app/xmpp/events/user-join.js b/app/xmpp/events/user-join.js new file mode 100644 index 0000000..39ce1cf --- /dev/null +++ b/app/xmpp/events/user-join.js @@ -0,0 +1,37 @@ +'use strict'; + +var Presence = require('node-xmpp-server').Presence, + EventListener = require('./../event-listener'); + +module.exports = EventListener.extend({ + + on: 'presence:user_join', + + then: function(data) { + var connections = this.getConnectionsForRoom(data.roomId); + + connections.forEach(function(connection) { + var presence = new Presence({ + to: connection.jid(data.roomSlug), + from: connection.getRoomJid(data.roomSlug, data.username) + }); + + presence + .c('x', { + xmlns: 'http://jabber.org/protocol/muc#user' + }) + .c('item', { + jid: connection.getUserJid(data.username), + affiliation: 'none', + role: 'participant' + }); + + if (data.user) { + connection.populateVcard(presence, data.user, this.core); + } + + this.send(connection, presence); + }, this); + } + +}); diff --git a/app/xmpp/events/user-leave.js b/app/xmpp/events/user-leave.js new file mode 100644 index 0000000..061a288 --- /dev/null +++ b/app/xmpp/events/user-leave.js @@ -0,0 +1,36 @@ +'use strict'; + +var Presence = require('node-xmpp-server').Presence, + EventListener = require('./../event-listener'); + +module.exports = EventListener.extend({ + + on: 'presence:user_leave', + + then: function(data) { + var connections = this.getConnectionsForRoom(data.roomId); + + connections.forEach(function(connection) { + var presence = new Presence({ + to: connection.jid(data.roomSlug), + from: connection.getRoomJid(data.roomSlug, data.username), + type: 'unavailable' + }); + + var x = presence.c('x', { + xmlns: 'http://jabber.org/protocol/muc#user' + }); + x.c('item', { + jid: connection.getUserJid(data.username), + role: 'none', + affiliation: 'none' + }); + x.c('status', { + code: '110' + }); + + this.send(connection, presence); + }, this); + } + +}); diff --git a/app/xmpp/events/usermessage-created.js b/app/xmpp/events/usermessage-created.js new file mode 100644 index 0000000..5a94395 --- /dev/null +++ b/app/xmpp/events/usermessage-created.js @@ -0,0 +1,45 @@ +'use strict'; + +var Message = require('node-xmpp-server').Message, + settings = require('./../../config'), + EventListener = require('./../event-listener'); + +module.exports = EventListener.extend({ + + on: 'user-messages:new', + + then: function(msg, user, owner, data) { + if (!settings.private.enable) { + return; + } + + var connections = this.core.presence.system.connections.query({ + userId: user._id.toString(), + type: 'xmpp' + }); + + connections.forEach(function(connection) { + var id = msg._id; + if (connection.user.username === user.username) { + id = data && data.id || id; + } + + var stanza = new Message({ + id: id, + type: 'chat', + to: connection.getUserJid(user.username), + from: connection.getUserJid(owner.username) + }); + + stanza.c('active', { + xmlns: 'http://jabber.org/protocol/chatstates' + }); + + stanza.c('body').t(msg.text); + + this.send(connection, stanza); + + }, this); + } + +}); diff --git a/app/xmpp/index.js b/app/xmpp/index.js new file mode 100644 index 0000000..b738723 --- /dev/null +++ b/app/xmpp/index.js @@ -0,0 +1,111 @@ +'use strict'; + +var xmpp = require('node-xmpp-server'), + IQ = xmpp.IQ, + settings = require('./../config'), + auth = require('./../auth/index'), + all = require('require-tree'), + XmppConnection = require('./xmpp-connection'); + +var allArray = function(path) { + var modules = all(path); + return Object.keys(modules).map(function(key) { + return modules[key]; + }); + }, + msgProcessors = allArray('./msg-processors'), + eventListeners = allArray('./events'); + +function xmppStart(core) { + var options = { + port: settings.xmpp.port, + domain: settings.xmpp.domain + }; + + if (settings.xmpp.tls && settings.xmpp.tls.enable) { + options.tls = { + keyPath: settings.xmpp.tls.key, + certPath: settings.xmpp.tls.cert + }; + } + + var c2s = new xmpp.C2SServer(options); + + c2s.on('connect', function(client) { + + client.on('authenticate', function(opts, cb) { + var username = settings.xmpp.username === 'full' ? + opts.jid.toString() : opts.jid.local; + + auth.authenticate(username, opts.password, function(err, user) { + if (err || !user) { + return cb(false); + } + + // TODO: remove? + client.user = user; + + var conn = new XmppConnection(user, client, opts.jid); + core.presence.connect(conn); + + cb(null, opts); + }); + }); + + client.on('online', function() { + }); + + client.on('stanza', function(stanza) { + var handled = msgProcessors.some(function(Processor) { + var processor = new Processor(client, stanza, core); + return processor.run(); + }); + + if (handled) { + return; + } + + if (settings.xmpp.debug.unhandled) { + // Print unhandled request + console.log(' '); + console.log(stanza.root().toString().red); + } + + if (stanza.name !== 'iq') { + return; + } + + var msg = new IQ({ + type: 'error', + id: stanza.attrs.id, + to: stanza.attrs.from, + from: stanza.attrs.to + }); + + msg.c('not-implemented', { + code: 501, + type: 'CANCEL' + }).c('feature-not-implemented', { + xmlns: 'urn:ietf:params:xml:n:xmpp-stanzas' + }); + + + if (settings.xmpp.debug.unhandled) { + console.log(msg.root().toString().green); + } + + client.send(msg); + }); + + // On Disconnect event. When a client disconnects + client.on('disconnect', function() { + }); + }); + + eventListeners.forEach(function(EventListener) { + var listener = new EventListener(core); + core.on(listener.on, listener.then); + }); +} + +module.exports = xmppStart; diff --git a/app/xmpp/msg-processor.js b/app/xmpp/msg-processor.js new file mode 100644 index 0000000..7e5cbba --- /dev/null +++ b/app/xmpp/msg-processor.js @@ -0,0 +1,129 @@ +'use strict'; + +var Stanza = require('node-xmpp-server').Stanza, + settings = require('./../config'), + _ = require('lodash'), + util = require('util'); + +function MessageProcessor(client, request, core) { + this.client = client; + this.connection = client.conn; + this.request = request; + this.core = core; + + this.run = this.run.bind(this); + this.send = this.send.bind(this); + + this.Stanza = this.Stanza.bind(this); + this.Iq = this.Iq.bind(this); + this.Message = this.Message.bind(this); + this.Presence = this.Presence.bind(this); +} + +MessageProcessor.prototype.Stanza = function(name, attr) { + attr = _.extend({ + id: this.request.attrs.id, + to: this.request.attrs.from, + from: this.request.attrs.to + }, attr || {}); + + return new Stanza(name, attr); +}; + +MessageProcessor.prototype.Iq = function(attr) { + attr = _.extend({ + type: 'result' + }, attr || {}); + + return this.Stanza('iq', attr); +}; + +MessageProcessor.prototype.Presence = function(attr) { + return this.Stanza('presence', attr); +}; + +MessageProcessor.prototype.Message = function(attr) { + return this.Stanza('message', attr); +}; + +MessageProcessor.prototype.preRun = function() { + this.to = this.request.attrs.to || ''; + + var confDomain = this.connection.getConfDomain(); + this.toConfRoot = this.to.indexOf(confDomain) === 0; + this.toARoom = this.to.indexOf('@' + confDomain) !== -1; + + this.ns = this.ns || {}; + + this.request.children.forEach(function(child) { + if (child.attrs && child.attrs.xmlns) { + this.ns[child.attrs.xmlns] = child; + } + }, this); +}; + +MessageProcessor.prototype.run = function() { + this.preRun(); + + if (this.if && this.if()) { + this.then(function() { + + if (!arguments || !arguments.length) { + return; + } + + var err = arguments[0], + msgs = Array.prototype.slice.call(arguments, 1); + + if (err) { + console.error(err); + return; + } + + this.send(msgs); + + }.bind(this)); + return true; + } + + return false; +}; + +MessageProcessor.prototype.send = function(msgs) { + if (settings.xmpp.debug.handled) { + console.log(' '); + console.log(this.request.root().toString().blue); + } + + msgs = _.flatten(msgs); + + msgs.forEach(function(msg) { + if (settings.xmpp.debug.handled) { + console.log(msg.root().toString().green); + } + this.client.send(msg); + }, this); +}; + +MessageProcessor.extend = function(options) { + var processor = function() { + MessageProcessor.apply(this, arguments); + + _.forEach(this.methods, function(key) { + this[key] = this[key].bind(this); + }, this); + }; + + util.inherits(processor, MessageProcessor); + + processor.prototype.methods = []; + + _.forEach(options, function(value, key) { + processor.prototype.methods.push(key); + processor.prototype[key] = value; + }); + + return processor; +}; + +module.exports = MessageProcessor; diff --git a/app/xmpp/msg-processors/conf-info.js b/app/xmpp/msg-processors/conf-info.js new file mode 100644 index 0000000..7c5402d --- /dev/null +++ b/app/xmpp/msg-processors/conf-info.js @@ -0,0 +1,32 @@ +'use strict'; + +var MessageProcessor = require('./../msg-processor'); + +module.exports = MessageProcessor.extend({ + + if: function() { + return this.toConfRoot && + this.ns['http://jabber.org/protocol/disco#info']; + }, + + then: function(cb) { + var stanza = this.Iq(); + + var query = stanza.c('query', { + xmlns: 'http://jabber.org/protocol/disco#info' + }); + + query.c('identity', { + category: 'conference', + type: 'text', + name: 'Let\'s Chat Conference Service' + }); + + query.c('feature', { + var: 'http://jabber.org/protocol/muc' + }); + + cb(null, stanza); + } + +}); diff --git a/app/xmpp/msg-processors/conf-items.js b/app/xmpp/msg-processors/conf-items.js new file mode 100644 index 0000000..5b536ac --- /dev/null +++ b/app/xmpp/msg-processors/conf-items.js @@ -0,0 +1,36 @@ +'use strict'; + +var MessageProcessor = require('./../msg-processor'); + +module.exports = MessageProcessor.extend({ + + if: function() { + return this.toConfRoot && + this.ns['http://jabber.org/protocol/disco#items']; + }, + + then: function(cb) { + this.core.rooms.list(null, function(err, rooms) { + if (err) { + return cb(err); + } + + var stanza = this.Iq(); + + var query = stanza.c('query', { + xmlns: 'http://jabber.org/protocol/disco#items' + }); + + rooms.forEach(function(room) { + query.c('item', { + jid: this.connection.getRoomJid(room.slug), + name: room.name + }); + }, this); + + cb(null, stanza); + + }.bind(this)); + } + +}); diff --git a/app/xmpp/msg-processors/last-activity.js b/app/xmpp/msg-processors/last-activity.js new file mode 100644 index 0000000..0cc58fb --- /dev/null +++ b/app/xmpp/msg-processors/last-activity.js @@ -0,0 +1,25 @@ +'use strict'; + +var MessageProcessor = require('./../msg-processor'); + +module.exports = MessageProcessor.extend({ + + if: function() { + return this.request.type === 'get' && this.ns['jabber:iq:last']; + }, + + then: function(cb) { + var stanza = this.Iq({ + to: this.connection.jid() + }); + + stanza.c('error', { + type: 'auth' + }).c('forbidden', { + xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' + }); + + cb(null, stanza); + } + +}); diff --git a/app/xmpp/msg-processors/ping.js b/app/xmpp/msg-processors/ping.js new file mode 100644 index 0000000..16d06e7 --- /dev/null +++ b/app/xmpp/msg-processors/ping.js @@ -0,0 +1,15 @@ +'use strict'; + +var MessageProcessor = require('./../msg-processor'); + +module.exports = MessageProcessor.extend({ + + if: function() { + return this.ns['urn:xmpp:ping']; + }, + + then: function(cb) { + cb(null, this.Iq()); + } + +}); diff --git a/app/xmpp/msg-processors/room-info.js b/app/xmpp/msg-processors/room-info.js new file mode 100644 index 0000000..ebfef80 --- /dev/null +++ b/app/xmpp/msg-processors/room-info.js @@ -0,0 +1,85 @@ +'use strict'; + +var MessageProcessor = require('./../msg-processor'); + +module.exports = MessageProcessor.extend({ + + if: function() { + return this.toARoom && + this.ns['http://jabber.org/protocol/disco#info']; + }, + + then: function(cb) { + var roomSlug = this.request.attrs.to.split('@')[0]; + + this.core.rooms.slug(roomSlug, function(err, room) { + if (err) { + return cb(err); + } + + if (!room) { + return this.doesNotExist(cb); + } + + this.sendInfo(room, cb); + + }.bind(this)); + }, + + sendInfo: function(room, cb) { + var stanza = this.Iq(); + + var query = stanza.c('query', { + xmlns: 'http://jabber.org/protocol/disco#info' + }); + + query.c('identity', { + category: 'conference', + type: 'text', + name: room.name + }); + + query.c('feature', { + var: 'http://jabber.org/protocol/muc' + }); + + query.c('feature', { + var: 'muc_persistent' + }); + + query.c('feature', { + var: 'muc_open' + }); + + query.c('feature', { + var: 'muc_unmoderated' + }); + + query.c('feature', { + var: 'muc_nonanonymous' + }); + + query.c('feature', { + var: 'muc_unsecured' + }); + + cb(null, stanza); + }, + + doesNotExist: function(cb) { + var stanza = this.Iq(); + + var query = stanza.c('query', { + xmlns: 'http://jabber.org/protocol/disco#info' + }); + + query.c('error', { + type: 'cancel' + }).c('item-not-found', { + xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' + }); + + cb(null, stanza); + } + +}); diff --git a/app/xmpp/msg-processors/room-items.js b/app/xmpp/msg-processors/room-items.js new file mode 100644 index 0000000..c9a52b2 --- /dev/null +++ b/app/xmpp/msg-processors/room-items.js @@ -0,0 +1,22 @@ +'use strict'; + +var MessageProcessor = require('./../msg-processor'); + +module.exports = MessageProcessor.extend({ + + if: function() { + return this.toARoom && + this.ns['http://jabber.org/protocol/disco#items']; + }, + + then: function(cb) { + var stanza = this.Iq(); + + stanza.c('query', { + xmlns: 'http://jabber.org/protocol/disco#items' + }); + + cb(null, stanza); + } + +}); diff --git a/app/xmpp/msg-processors/room-join.js b/app/xmpp/msg-processors/room-join.js new file mode 100644 index 0000000..12a100a --- /dev/null +++ b/app/xmpp/msg-processors/room-join.js @@ -0,0 +1,281 @@ +'use strict'; + +var _ = require('lodash'), + moment = require('moment'), + Message = require('node-xmpp-server').Message, + MessageProcessor = require('./../msg-processor'), + settings = require('./../../config'); + +module.exports = MessageProcessor.extend({ + + if: function() { + var roomPresense = this.toARoom && + !this.request.type && + this.request.name === 'presence'; + + if (!roomPresense) { + return false; + } + + var toParts = this.request.attrs.to.split('/'), + roomUrl = toParts[0], + roomSlug = roomUrl.split('@')[0]; + + var proom = this.core.presence.rooms.slug(roomSlug); + + if (proom && proom.connections.contains(this.connection)) { + // If this connection is already in the room + // then no need to run this message processor + return false; + } + + return true; + }, + + then: function(cb) { + var toParts = this.request.attrs.to.split('/'), + roomUrl = toParts[0], + nickname = toParts[1], + roomSlug = roomUrl.split('@')[0]; + + this.connection.nickname(roomSlug, nickname); + + var options = { + userId: this.connection.user.id, + slug: roomSlug, + password: this.getPassword(), + saveMembership: true + }; + + this.core.rooms.canJoin(options, function(err, room, canJoin) { + if (err) { + return cb(err); + } + + if (room && canJoin) { + return this.handleJoin(room, cb); + } + + if (room && !canJoin) { + return this.sendErrorPassword(room, cb); + } + + if (!settings.xmpp.roomCreation) { + return this.cantCreateRoom(roomSlug, cb); + } + + return this.createRoom(roomSlug, function(err, room) { + if (err) { + return cb(err); + } + this.handleJoin(room, cb); + }.bind(this)); + + }.bind(this)); + }, + + createRoom: function(roomSlug, cb) { + var password = this.getPassword(); + var options = { + owner: this.connection.user.id, + name: roomSlug, + slug: roomSlug, + description: '', + password: password + }; + if(!settings.rooms.private) { + delete options.private; + delete options.password; + } + this.core.rooms.create(options, cb); + }, + + cantCreateRoom: function(roomSlug, cb) { + var presence = this.Presence({ + from: this.connection.getRoomJid(roomSlug, 'admin'), + type: 'error' + }); + + presence.c('x', { + xmlns: 'http://jabber.org/protocol/muc' + }); + + presence.c('error', { + by: this.connection.getRoomJid(roomSlug), + type: 'cancel' + }).c('not-allowed', { + xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' + }); + + cb(null, presence); + }, + + _getXNode: function() { + if(!this.xNode) { + this.xNode = _.find(this.request.children, function(child) { + return child.name === 'x'; + }); + } + return this.xNode; + }, + + getHistoryNode: function() { + var xNode = this._getXNode(); + if (xNode) { + return _.find(xNode.children, function(child) { + return child.name === 'history'; + }); + } + }, + + getPassword: function() { + var xNode = this._getXNode(); + if (xNode) { + var passwordNode = _.find(xNode.children, function(child) { + return child.name === 'password'; + }); + if(passwordNode && passwordNode.children) { + return passwordNode.children[0]; + } + } + + return ''; + }, + + sendErrorPassword: function(room, cb) { + //from http://xmpp.org/extensions/xep-0045.html#enter-pw + var presence = this.Presence({ + type: 'error' + }); + + presence + .c('x', { + xmlns: 'http://jabber.org/protocol/muc' + }); + presence + .c('error', { + type: 'auth', + by: this.connection.getRoomJid(room.slug) + }) + .c('not-authorized', { + xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' + }); + + return cb(null, presence); + }, + + handleJoin: function(room, cb) { + var username = this.connection.user.username; + + var proom = this.core.presence.rooms.get(room._id); + var usernames = proom ? proom.getUsernames() : []; + + // User's own presence must be last - and be their nickname + var i = usernames.indexOf(username); + if (i > -1) { + usernames.splice(i, 1); + } + usernames.push(this.connection.user.username); + + var presences = usernames.map(function(username) { + + var presence = this.Presence({ + from: this.connection.getRoomJid(room.slug, username) + }); + + presence + .c('x', { + xmlns: 'http://jabber.org/protocol/muc#user' + }) + .c('item', { + jid: this.connection.getUserJid(username), + affiliation: 'none', + role: 'participant' + }); + + // TODO: Add avatar for each room user + // helper.populateVcard(presence, user, this.core); + + return presence; + + }, this); + + var subject = this.Message({ + type: 'groupchat' + }); + + subject.c('subject').t(room.name + ' | ' + room.description); + + var historyNode = this.getHistoryNode(); + + if (!historyNode || + historyNode.attrs.maxchars === 0 || + historyNode.attrs.maxchars === '0') { + // Send no history + this.core.presence.join(this.connection, room); + return cb(null, presences, subject); + } + + var query = { + userId: this.connection.user.id, + room: room._id, + expand: 'owner' + }; + + if (historyNode.attrs.since) { + query.from = moment(historyNode.attrs.since).utc().toDate(); + } + + if (historyNode.attrs.seconds) { + query.from = moment() + .subtract(historyNode.attrs.seconds, 'seconds') + .utc() + .toDate(); + } + + if (historyNode.attrs.maxstanzas) { + query.take = historyNode.attrs.maxstanzas; + } + + this.core.messages.list(query, function(err, messages) { + if (err) { + return cb(err); + } + + messages.reverse(); + + var msgs = messages.map(function(msg) { + + var stanza = new Message({ + id: msg._id, + type: 'groupchat', + to: this.connection.getRoomJid(room.slug), + from: this.connection.getRoomJid(room.slug, msg.owner.username) + }); + + stanza.c('body').t(msg.text); + + stanza.c('delay', { + xmlns: 'urn:xmpp:delay', + from: this.connection.getRoomJid(room.slug), + stamp: msg.posted.toISOString() + }); + + stanza.c('addresses', { + xmlns: 'http://jabber.org/protocol/address' + }).c('address', { + type: 'ofrom', + jid: this.connection.getUserJid(msg.owner.username) + }); + + return stanza; + + }, this); + + this.core.presence.join(this.connection, room); + cb(null, presences, msgs, subject); + + }.bind(this)); + } + +}); diff --git a/app/xmpp/msg-processors/room-leave.js b/app/xmpp/msg-processors/room-leave.js new file mode 100644 index 0000000..14c4b64 --- /dev/null +++ b/app/xmpp/msg-processors/room-leave.js @@ -0,0 +1,49 @@ +'use strict'; + +var MessageProcessor = require('./../msg-processor'); + +module.exports = MessageProcessor.extend({ + + if: function() { + return this.request.name === 'presence' && + this.request.type === 'unavailable' && + this.toARoom; + }, + + then: function(cb) { + var roomUrl = this.request.attrs.to.split('/')[0], + roomSlug = roomUrl.split('@')[0]; + + this.core.rooms.slug(roomSlug, function(err, room) { + if (err) { + return cb(err); + } + + if (!room) { + return cb(); + } + + this.core.presence.leave(this.client.conn, room._id); + + var presence = this.Presence({ + type: 'unavailable' + }); + + var x = presence.c('x', { + xmlns: 'http://jabber.org/protocol/muc#user' + }); + x.c('item', { + jid: this.request.attrs.from, + role: 'none', + affiliation: 'none' + }); + x.c('status', { + code: '110' + }); + + cb(null, presence); + + }.bind(this)); + } + +}); diff --git a/app/xmpp/msg-processors/room-message.js b/app/xmpp/msg-processors/room-message.js new file mode 100644 index 0000000..001794f --- /dev/null +++ b/app/xmpp/msg-processors/room-message.js @@ -0,0 +1,69 @@ +'use strict'; + +var _ = require('lodash'), + MessageProcessor = require('./../msg-processor'); + +var mentionPattern = /^([a-z0-9_]+\:)\B/; + +module.exports = MessageProcessor.extend({ + + if: function() { + return this.request.name === 'message' && + this.request.type === 'groupchat' && + this.toARoom; + }, + + then: function(cb) { + var roomSlug = this.request.attrs.to.split('@')[0]; + + var body = _.find(this.request.children, function (child) { + return child.name === 'body'; + }); + + if (!body) { + return cb(); + } + + this.core.rooms.slug(roomSlug, function(err, room) { + if (err) { + return cb(err); + } + + if (!room) { + return cb(); + } + + var text = body.text().replace(mentionPattern, function(group) { + + var usernames = this.core.presence.rooms + .get(room._id).getUsernames(); + + var username = group.substring(0, group.length - 1); + + if (usernames.indexOf(username) > -1) { + return '@' + username; + } + + return group; + + }.bind(this)); + + + var options = { + owner: this.client.user._id, + room: room._id, + text: text, + data: { + id: this.request.attrs.id + } + }; + + this.core.messages.create(options, function(err) { + // Message will be sent by listener + cb(err); + }); + + }.bind(this)); + } + +}); diff --git a/app/xmpp/msg-processors/root-info.js b/app/xmpp/msg-processors/root-info.js new file mode 100644 index 0000000..554ba3d --- /dev/null +++ b/app/xmpp/msg-processors/root-info.js @@ -0,0 +1,35 @@ +'use strict'; + +var MessageProcessor = require('./../msg-processor'), + settings = require('./../../config'); + +module.exports = MessageProcessor.extend({ + + if: function() { + return this.ns['http://jabber.org/protocol/disco#info'] && ( + this.to === this.connection.getDomain() || + this.to === settings.xmpp.domain + ); + }, + + then: function(cb) { + var stanza = this.Iq(); + + var query = stanza.c('query', { + xmlns: 'http://jabber.org/protocol/disco#info' + }); + + query.c('identity', { + category: 'server', + type: 'im', + name: 'Let\'s chat' + }); + + query.c('feature', { + var: 'vcard-temp' + }); + + cb(null, stanza); + } + +}); diff --git a/app/xmpp/msg-processors/root-items.js b/app/xmpp/msg-processors/root-items.js new file mode 100644 index 0000000..2aed38b --- /dev/null +++ b/app/xmpp/msg-processors/root-items.js @@ -0,0 +1,30 @@ +'use strict'; + +var MessageProcessor = require('./../msg-processor'), + settings = require('./../../config'); + +module.exports = MessageProcessor.extend({ + + if: function() { + return this.ns['http://jabber.org/protocol/disco#items'] && ( + this.to === this.connection.getDomain() || + this.to === settings.xmpp.domain + ); + }, + + then: function(cb) { + var stanza = this.Iq(); + + var query = stanza.c('query', { + xmlns: 'http://jabber.org/protocol/disco#items' + }); + + query.c('item', { + jid: this.connection.getConfDomain(), + name: 'Let\'s Chat Conference Service' + }); + + cb(null, stanza); + } + +}); diff --git a/app/xmpp/msg-processors/root-join.js b/app/xmpp/msg-processors/root-join.js new file mode 100644 index 0000000..8ecb5c8 --- /dev/null +++ b/app/xmpp/msg-processors/root-join.js @@ -0,0 +1,46 @@ +'use strict'; + +var _ = require('lodash'), + MessageProcessor = require('./../msg-processor'), + settings = require('./../../config'); + +module.exports = MessageProcessor.extend({ + + if: function() { + return !this.request.to && + !this.request.type && + this.request.name === 'presence'; + }, + + then: function(cb) { + if (!settings.private.enable) { + return cb(); + } + + var msgs = []; + + var users = this.core.presence.system.connections.getUsers({ + type: 'xmpp' // Only XMPP supports private messaging - for now + }); + + _.each(users, function(user) { + if (user.id === this.connection.user.id) { + return; + } + + + var presence = this.Presence({ + from: this.connection.getUserJid(user.username), + type: undefined + }); + + this.connection.populateVcard(presence, user, this.core); + + msgs.push(presence); + + }, this); + + cb(null, msgs); + } + +}); diff --git a/app/xmpp/msg-processors/roster-get.js b/app/xmpp/msg-processors/roster-get.js new file mode 100644 index 0000000..fa3262c --- /dev/null +++ b/app/xmpp/msg-processors/roster-get.js @@ -0,0 +1,68 @@ +'use strict'; + +var _ = require('lodash'), + MessageProcessor = require('./../msg-processor'), + settings = require('./../../config'); + +module.exports = MessageProcessor.extend({ + + if: function() { + return this.request.type === 'get' && this.ns['jabber:iq:roster']; + }, + + then: function(cb) { + if (!settings.private.enable) { + return this.sendRoster([], cb); + } + + if (settings.private.roster === 'all') { + return this.sendAllUsers(cb); + } + + this.sendOnlineUsers(cb); + }, + + sendOnlineUsers: function(cb) { + var users = this.core.presence.system.connections.getUsers({ + type: 'xmpp' // Only XMPP supports private messaging - for now + }); + + this.sendRoster(users, cb); + }, + + sendAllUsers: function(cb) { + this.core.users.list({}, function(err, users) { + if (err) { + return cb(err); + } + + this.sendRoster(users, cb); + }.bind(this)); + }, + + sendRoster: function(users, cb) { + var stanza = this.Iq(); + + var v = stanza.c('query', { + xmlns: 'jabber:iq:roster' + }); + + _.each(users, function(user) { + if (user._id && user._id.equals(this.connection.user.id)) { + return; + } + if (user.id && user.id === this.connection.user.id) { + return; + } + + v.c('item', { + jid: this.connection.getUserJid(user.username), + name: user.displayName, + subscription: 'both' + }).c('group').t('Let\'s Chat'); + }, this); + + cb(null, stanza); + } + +}); diff --git a/app/xmpp/msg-processors/user-message.js b/app/xmpp/msg-processors/user-message.js new file mode 100644 index 0000000..af6a370 --- /dev/null +++ b/app/xmpp/msg-processors/user-message.js @@ -0,0 +1,54 @@ +'use strict'; + +var _ = require('lodash'), + MessageProcessor = require('./../msg-processor'), + settings = require('./../../config'); + +module.exports = MessageProcessor.extend({ + + if: function() { + return this.request.name === 'message' && + this.request.type === 'chat' && + !this.toARoom && + this.request.attrs.to; + }, + + then: function(cb) { + if (!settings.private.enable) { + return cb(); + } + + var username = this.request.attrs.to.split('@')[0]; + + var body = _.find(this.request.children, function (child) { + return child.name === 'body'; + }); + + if (!body) { + return cb(); + } + + this.core.users.username(username, function(err, user) { + if (err) { + return cb(err); + } + + if (!user) { + return cb(); + } + + this.core.usermessages.create({ + owner: this.connection.user.id, + user: user._id, + text: body.text(), + data: { + id: this.request.attrs.id + } + }, function(err) { + cb(err); + }); + + }.bind(this)); + } + +}); diff --git a/app/xmpp/msg-processors/vcard-get.js b/app/xmpp/msg-processors/vcard-get.js new file mode 100644 index 0000000..73b0e71 --- /dev/null +++ b/app/xmpp/msg-processors/vcard-get.js @@ -0,0 +1,65 @@ +'use strict'; + +var mongoose = require('mongoose'), + MessageProcessor = require('./../msg-processor'); + +module.exports = MessageProcessor.extend({ + + if: function() { + return this.request.type === 'get' && this.ns['vcard-temp']; + }, + + then: function(cb) { + var jid = this.connection.jid(); + var other = this.to && this.to !== jid; + + if (!other) { + return this.sendVcard(this.connection.user, cb); + } + + var username = this.to.split('@')[0]; + var user = this.core.presence.users.getByUsername(username); + + if (user) { + return this.sendVcard(user, cb); + } + + var User = mongoose.model('User'); + User.findByIdentifier(username, function(err, user) { + if (!err && user) { + this.sendVcard(user, cb); + } + }.bind(this)); + }, + + sendVcard: function(user, cb) { + var stanza = this.Iq(); + + var vcard = stanza.c('vCard', { + xmlns: 'vcard-temp' + }); + + vcard.c('FN').t(user.firstName + ' ' + user.lastName); + + + var name = vcard.c('N'); + name.c('GIVEN').t(user.firstName); + name.c('FAMILY').t(user.lastName); + + vcard.c('NICKNAME').t(user.username); + + vcard.c('JABBERID').t(this.connection.getUserJid(user.username)); + + var userId = (user.id || user._id).toString(); + + var avatar = this.core.avatars.get(userId); + if (avatar) { + var photo = vcard.c('PHOTO'); + photo.c('TYPE').t('image/jpeg'); + photo.c('BINVAL').t(avatar.base64); + } + + cb(null, stanza); + } + +}); diff --git a/app/xmpp/msg-processors/vcard-set.js b/app/xmpp/msg-processors/vcard-set.js new file mode 100644 index 0000000..de864c3 --- /dev/null +++ b/app/xmpp/msg-processors/vcard-set.js @@ -0,0 +1,16 @@ +'use strict'; + +var MessageProcessor = require('./../msg-processor'); + +module.exports = MessageProcessor.extend({ + + if: function() { + return this.request.type === 'set' && this.ns['vcard-temp']; + }, + + then: function(cb) { + // Pretend we accepted the request + cb(null, this.Iq()); + } + +}); diff --git a/app/xmpp/xmpp-connection.js b/app/xmpp/xmpp-connection.js new file mode 100644 index 0000000..19bcd80 --- /dev/null +++ b/app/xmpp/xmpp-connection.js @@ -0,0 +1,82 @@ +'use strict'; + +var util = require('util'), + Connection = require('./../core/presence').Connection, + settings = require('./../config'); + +function XmppConnection(user, client, jid) { + Connection.call(this, 'xmpp', user); + this.client = client; + this._jid = jid; + this.nicknames = {}; + client.conn = this; + client.on('disconnect', this.disconnect.bind(this)); +} + +util.inherits(XmppConnection, Connection); + +XmppConnection.prototype.disconnect = function() { + this.emit('disconnect'); + + if (this.client) { + this.client.conn = null; + this.client = null; + } +}; + +XmppConnection.prototype.jid = function(room) { + if (room) { + return room + '@' + this.getConfDomain() + + '/' + (this.nickname(room) || this._jid.local); + } + + return this._jid.local + '@' + this.getDomain(); +}; + +XmppConnection.prototype.nickname = function(room, value) { + if (value) { + this.nicknames[room] = value; + } + return this.nicknames[room]; +}; + +XmppConnection.prototype.getDomain = function() { + return this._jid.domain || settings.xmpp.domain; +}; + +XmppConnection.prototype.getConfDomain = function() { + return 'conference.' + this.getDomain(); +}; + +XmppConnection.prototype.getUserJid = function(username) { + var domain = this.getDomain(); + + if (username.indexOf('@' + domain) !== -1) { + return username; + } + return username + '@' + domain; +}; + +XmppConnection.prototype.getRoomJid = function(roomId, username) { + if (username && username === this.user.username) { + return this.jid(roomId); + } + + var jid = roomId + '@' + this.getConfDomain(); + if (username) { + jid += '/' + username; + } + return jid; +}; + +XmppConnection.prototype.populateVcard = function(presence, user, core) { + var vcard = presence.c('x', { xmlns: 'vcard-temp:x:update' }); + var photo = vcard.c('photo'); + + var avatar = core.avatars.get(user.id); + if (avatar) { + photo.t(avatar.sha1); + } +}; + +module.exports = XmppConnection; diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..9d7d0b7 --- /dev/null +++ b/bower.json @@ -0,0 +1,15 @@ +{ + "name": "Let's Chat", + "private": true, + "dependencies": { + "jquery": "~2.1.1", + "jquery-validate": "~1.12.0", + "underscore": "~1.6.0", + "backbone": "~1.1.2", + "handlebars": "~1.3.0", + "bootstrap": "~3.1.1", + "store.js": "~1.3.16", + "backbone.keys": "~0.2.0", + "favico.js": "~0.3.7" + } +} diff --git a/defaults.yml b/defaults.yml new file mode 100644 index 0000000..d678b92 --- /dev/null +++ b/defaults.yml @@ -0,0 +1,77 @@ +# +# Let's Chat Built-in Defaults +# + +env: development # development / production + +http: + enable: true + host: + port: 5000 + +https: + enable: false + host: + port: 5001 + key: key.pem + cert: certificate.pem + +xmpp: + enable: false + roomCreation: false + debug: + handled: false + unhandled: false + port: 5222 + domain: example.com + username: node # node / full + tls: + enable: false + key: ./server.key + cert: ./server.crt + +database: + uri: mongodb://mongo:27017/hp_mongo + +secrets: + cookie: secretsauce + +files: + enable: false + provider: local + maxFileSize: 100000000 + restrictTypes: true + allowedTypes: + - 'image/jpeg' + - 'image/png' + - 'image/gif' + local: + dir: uploads + +auth: + throttling: + enable: true + threshold: 3 + providers: [local] # [local, kerberos, ldap] - You can specify the order + local: + enableRegistration: true + passwordRegex: ^.{8,64}$ + +private: + enable: false + roster: online # online / all + expire: 360 # false or number of minutes + +noRobots: true # Serve robots.txt with disallow + +giphy: + enable: true + rating: pg-13 + limit: 24 + apiKey: dc6zaTOxFJmzC + +rooms: + private: false + +i18n: + locale: en diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 0000000..7d01b04 --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,3 @@ +.dockerignore +docker-compose.yml +Dockerfile diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..6aec117 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,52 @@ +FROM node:0.12-slim +MAINTAINER SD Elements + +ENV PKG_JSON_URL=https://raw.githubusercontent.com/bahchiscodefresh/lets-chat/master/package.json \ + TAR_GZ_URL=https://github.com/bahchiscodefresh/lets-chat/archive/master.tar.gz \ + BUILD_DEPS='g++ gcc git make python' \ + LCB_PLUGINS='lets-chat-ldap lets-chat-s3' + +RUN mkdir -p /usr/src/app +WORKDIR /usr/src/app + +ADD $PKG_JSON_URL ./package.json + +RUN set -x \ +&& apt-get update \ +&& apt-get install -y $BUILD_DEPS --no-install-recommends \ +&& npm install --production \ +&& npm install -g eslint \ +&& npm install $LCB_PLUGINS \ +&& npm dedupe \ +&& npm cache clean \ +&& rm -rf /tmp/npm* \ +&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false -o APT::AutoRemove::SuggestsImportant=false $BUILD_DEPS + +RUN apt-get install -y curl --no-install-recommends + +RUN rm -rf /var/lib/apt/lists/* + +ADD $TAR_GZ_URL ./master.tar.gz + +RUN tar -xzvf master.tar.gz \ +&& cp -a lets-chat-master/. . \ +&& rm -rf lets-chat-master + +RUN groupadd -r node \ +&& useradd -r -g node node \ +&& mkdir -p /home/node/.ssh \ +&& chown -R node:node /home/node/.ssh \ +&& chown node:node uploads + +ENV LCB_DATABASE_URI=mongodb://mongo/letschat \ + LCB_HTTP_HOST=0.0.0.0 \ + LCB_HTTP_PORT=8080 \ + LCB_XMPP_ENABLE=true \ + LCB_XMPP_PORT=5222 + +EXPOSE 8080 5222 + +VOLUME ["/usr/src/app/config"] +VOLUME ["/usr/src/app/uploads"] + +CMD ["npm", "start"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..d4481fc --- /dev/null +++ b/docker/README.md @@ -0,0 +1,54 @@ +![Let's Chat](http://i.imgur.com/0a3l5VF.png) + +# What is Let's Chat? + +A self-hosted chat app for small teams. + +![Screenshot](http://i.imgur.com/C4uMD67.png) + +# How to use this image + +``` +docker run --name some-letschat --link some-mongo:mongo -d sdelements/lets-chat +``` + +If you'd like to be able to access the instance from the host without the container's IP, standard port mappings can be used: + +``` +docker run --name some-letschat --link some-mongo:mongo -p 8080:8080 -d sdelements/lets-chat +``` + +Then, access it via `http://localhost:8080` or `http://host-ip:8080` in a browser. + +## ... via `docker-compose` + +Example docker-compose.yml for `sdelements/lets-chat`: + +```yml +app: + image: sdelements/lets-chat + links: + - mongo + ports: + - 8080:8080 + - 5222:5222 + +mongo: + image: mongo:latest +``` + +Run `docker-compose up`, wait for it to initialize completely, and visit `http://localhost:8080` or `http://host-ip:8080`. + +# Configuration + +You can config your Let's Chat Docker instance using one of the following methods: + +## Config file + +Create a settings.yml file in a directory and then mount that directory as a Docker volume. + +`/usr/src/app/config` + +## Environment variables + +[See the Let's Chat wiki for a list of envirnoment variables](https://github.com/sdelements/lets-chat/wiki/Environment-variables) diff --git a/docker/docker-compose-local.yml b/docker/docker-compose-local.yml new file mode 100644 index 0000000..925c6ee --- /dev/null +++ b/docker/docker-compose-local.yml @@ -0,0 +1,22 @@ +# Let's Chat: Docker Compose +# https://docs.docker.com/compose/ +# +# Usage: docker-compose up + +app: + image: letschat:latest + links: + - mongo + ports: + - 1111:8080 + - 5000:5000 + environment : + - DEBUG=* + - MONGO_DOCKER = mongodb://mongo:27017/hp_mongo + #command: node ./app/tests/mongo.unit.spec + #- MONGO_DOCKER=mongodb://admin:hpadmin@ds037415.mongolab.com:37415/hp_mongo + # - DEBUGGER = "--debug-brk" +mongo: + image: mongo:latest + ports : + - 27017:27017 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..622cde3 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,19 @@ +# Let's Chat: Docker Compose +# https://docs.docker.com/compose/ +# +# Usage: docker-compose up + +app: + image: sdelements/lets-chat:latest + links: + - mongo + ports: + - 80:8080 + - 5222:5222 + command: ls . + + +mongo: + image: mongo:latest + ports : + - 27017:27017 diff --git a/extras/emotes/default.yml b/extras/emotes/default.yml new file mode 100644 index 0000000..73a3383 --- /dev/null +++ b/extras/emotes/default.yml @@ -0,0 +1,1762 @@ +- emote: "-1" + image: -1.png +- emote: "+1" + image: +1.png +- emote: "8ball" + image: 8ball.png +- emote: "100" + image: 100.png +- emote: "1234" + image: 1234.png +- emote: "a" + image: a.png +- emote: "ab" + image: ab.png +- emote: "abc" + image: abc.png +- emote: "abcd" + image: abcd.png +- emote: "accept" + image: accept.png +- emote: "aerial_tramway" + image: aerial_tramway.png +- emote: "airplane" + image: airplane.png +- emote: "alarm_clock" + image: alarm_clock.png +- emote: "alien" + image: alien.png +- emote: "ambulance" + image: ambulance.png +- emote: "anchor" + image: anchor.png +- emote: "angel" + image: angel.png +- emote: "anger" + image: anger.png +- emote: "angry" + image: angry.png +- emote: "anguished" + image: anguished.png +- emote: "ant" + image: ant.png +- emote: "apple" + image: apple.png +- emote: "aquarius" + image: aquarius.png +- emote: "aries" + image: aries.png +- emote: "arrow_backward" + image: arrow_backward.png +- emote: "arrow_double_down" + image: arrow_double_down.png +- emote: "arrow_double_up" + image: arrow_double_up.png +- emote: "arrow_down_small" + image: arrow_down_small.png +- emote: "arrow_down" + image: arrow_down.png +- emote: "arrow_forward" + image: arrow_forward.png +- emote: "arrow_heading_down" + image: arrow_heading_down.png +- emote: "arrow_heading_up" + image: arrow_heading_up.png +- emote: "arrow_left" + image: arrow_left.png +- emote: "arrow_lower_left" + image: arrow_lower_left.png +- emote: "arrow_lower_right" + image: arrow_lower_right.png +- emote: "arrow_right_hook" + image: arrow_right_hook.png +- emote: "arrow_right" + image: arrow_right.png +- emote: "arrow_up_down" + image: arrow_up_down.png +- emote: "arrow_up_small" + image: arrow_up_small.png +- emote: "arrow_up" + image: arrow_up.png +- emote: "arrow_upper_left" + image: arrow_upper_left.png +- emote: "arrow_upper_right" + image: arrow_upper_right.png +- emote: "arrows_clockwise" + image: arrows_clockwise.png +- emote: "arrows_counterclockwise" + image: arrows_counterclockwise.png +- emote: "art" + image: art.png +- emote: "articulated_lorry" + image: articulated_lorry.png +- emote: "astonished" + image: astonished.png +- emote: "atm" + image: atm.png +- emote: "b" + image: b.png +- emote: "baby_bottle" + image: baby_bottle.png +- emote: "baby_chick" + image: baby_chick.png +- emote: "baby_symbol" + image: baby_symbol.png +- emote: "baby" + image: baby.png +- emote: "back" + image: back.png +- emote: "baggage_claim" + image: baggage_claim.png +- emote: "balloon" + image: balloon.png +- emote: "ballot_box_with_check" + image: ballot_box_with_check.png +- emote: "bamboo" + image: bamboo.png +- emote: "banana" + image: banana.png +- emote: "bangbang" + image: bangbang.png +- emote: "bank" + image: bank.png +- emote: "bar_chart" + image: bar_chart.png +- emote: "barber" + image: barber.png +- emote: "baseball" + image: baseball.png +- emote: "basketball" + image: basketball.png +- emote: "bath" + image: bath.png +- emote: "bathtub" + image: bathtub.png +- emote: "battery" + image: battery.png +- emote: "bear" + image: bear.png +- emote: "bee" + image: bee.png +- emote: "beer" + image: beer.png +- emote: "beers" + image: beers.png +- emote: "beetle" + image: beetle.png +- emote: "beginner" + image: beginner.png +- emote: "bell" + image: bell.png +- emote: "bento" + image: bento.png +- emote: "bicyclist" + image: bicyclist.png +- emote: "bike" + image: bike.png +- emote: "bikini" + image: bikini.png +- emote: "bird" + image: bird.png +- emote: "birthday" + image: birthday.png +- emote: "black_circle" + image: black_circle.png +- emote: "black_joker" + image: black_joker.png +- emote: "black_medium_small_square" + image: black_medium_small_square.png +- emote: "black_medium_square" + image: black_medium_square.png +- emote: "black_nib" + image: black_nib.png +- emote: "black_small_square" + image: black_small_square.png +- emote: "black_square_button" + image: black_square_button.png +- emote: "black_square" + image: black_square.png +- emote: "blossom" + image: blossom.png +- emote: "blowfish" + image: blowfish.png +- emote: "blue_book" + image: blue_book.png +- emote: "blue_car" + image: blue_car.png +- emote: "blue_heart" + image: blue_heart.png +- emote: "blush" + image: blush.png +- emote: "boar" + image: boar.png +- emote: "boat" + image: boat.png +- emote: "bomb" + image: bomb.png +- emote: "book" + image: book.png +- emote: "bookmark_tabs" + image: bookmark_tabs.png +- emote: "bookmark" + image: bookmark.png +- emote: "books" + image: books.png +- emote: "boom" + image: boom.png +- emote: "boot" + image: boot.png +- emote: "bouquet" + image: bouquet.png +- emote: "bow" + image: bow.png +- emote: "bowling" + image: bowling.png +- emote: "bowtie" + image: bowtie.png +- emote: "boy" + image: boy.png +- emote: "bread" + image: bread.png +- emote: "bride_with_veil" + image: bride_with_veil.png +- emote: "bridge_at_night" + image: bridge_at_night.png +- emote: "briefcase" + image: briefcase.png +- emote: "broken_heart" + image: broken_heart.png +- emote: "bug" + image: bug.png +- emote: "bulb" + image: bulb.png +- emote: "bullettrain_front" + image: bullettrain_front.png +- emote: "bullettrain_side" + image: bullettrain_side.png +- emote: "bus" + image: bus.png +- emote: "busstop" + image: busstop.png +- emote: "bust_in_silhouette" + image: bust_in_silhouette.png +- emote: "busts_in_silhouette" + image: busts_in_silhouette.png +- emote: "cactus" + image: cactus.png +- emote: "cake" + image: cake.png +- emote: "calendar" + image: calendar.png +- emote: "calling" + image: calling.png +- emote: "camel" + image: camel.png +- emote: "camera" + image: camera.png +- emote: "cancer" + image: cancer.png +- emote: "candy" + image: candy.png +- emote: "capital_abcd" + image: capital_abcd.png +- emote: "capricorn" + image: capricorn.png +- emote: "car" + image: car.png +- emote: "card_index" + image: card_index.png +- emote: "carousel_horse" + image: carousel_horse.png +- emote: "cat" + image: cat.png +- emote: "cat2" + image: cat2.png +- emote: "cd" + image: cd.png +- emote: "chart_with_downwards_trend" + image: chart_with_downwards_trend.png +- emote: "chart_with_upwards_trend" + image: chart_with_upwards_trend.png +- emote: "chart" + image: chart.png +- emote: "checkered_flag" + image: checkered_flag.png +- emote: "cherries" + image: cherries.png +- emote: "cherry_blossom" + image: cherry_blossom.png +- emote: "chestnut" + image: chestnut.png +- emote: "chicken" + image: chicken.png +- emote: "children_crossing" + image: children_crossing.png +- emote: "chocolate_bar" + image: chocolate_bar.png +- emote: "christmas_tree" + image: christmas_tree.png +- emote: "church" + image: church.png +- emote: "cinema" + image: cinema.png +- emote: "circus_tent" + image: circus_tent.png +- emote: "city_sunrise" + image: city_sunrise.png +- emote: "city_sunset" + image: city_sunset.png +- emote: "cl" + image: cl.png +- emote: "clap" + image: clap.png +- emote: "clapper" + image: clapper.png +- emote: "clipboard" + image: clipboard.png +- emote: "clock1" + image: clock1.png +- emote: "clock2" + image: clock2.png +- emote: "clock3" + image: clock3.png +- emote: "clock4" + image: clock4.png +- emote: "clock5" + image: clock5.png +- emote: "clock6" + image: clock6.png +- emote: "clock7" + image: clock7.png +- emote: "clock8" + image: clock8.png +- emote: "clock9" + image: clock9.png +- emote: "clock10" + image: clock10.png +- emote: "clock11" + image: clock11.png +- emote: "clock12" + image: clock12.png +- emote: "clock130" + image: clock130.png +- emote: "clock230" + image: clock230.png +- emote: "clock330" + image: clock330.png +- emote: "clock430" + image: clock430.png +- emote: "clock530" + image: clock530.png +- emote: "clock630" + image: clock630.png +- emote: "clock730" + image: clock730.png +- emote: "clock830" + image: clock830.png +- emote: "clock930" + image: clock930.png +- emote: "clock1030" + image: clock1030.png +- emote: "clock1130" + image: clock1130.png +- emote: "clock1230" + image: clock1230.png +- emote: "closed_book" + image: closed_book.png +- emote: "closed_lock_with_key" + image: closed_lock_with_key.png +- emote: "closed_umbrella" + image: closed_umbrella.png +- emote: "cloud" + image: cloud.png +- emote: "clubs" + image: clubs.png +- emote: "cn" + image: cn.png +- emote: "cocktail" + image: cocktail.png +- emote: "coffee" + image: coffee.png +- emote: "cold_sweat" + image: cold_sweat.png +- emote: "collision" + image: collision.png +- emote: "computer" + image: computer.png +- emote: "confetti_ball" + image: confetti_ball.png +- emote: "confounded" + image: confounded.png +- emote: "confused" + image: confused.png +- emote: "congratulations" + image: congratulations.png +- emote: "construction_worker" + image: construction_worker.png +- emote: "construction" + image: construction.png +- emote: "convenience_store" + image: convenience_store.png +- emote: "cookie" + image: cookie.png +- emote: "cool" + image: cool.png +- emote: "cop" + image: cop.png +- emote: "copyright" + image: copyright.png +- emote: "corn" + image: corn.png +- emote: "couple_with_heart" + image: couple_with_heart.png +- emote: "couple" + image: couple.png +- emote: "couplekiss" + image: couplekiss.png +- emote: "cow" + image: cow.png +- emote: "cow2" + image: cow2.png +- emote: "credit_card" + image: credit_card.png +- emote: "crocodile" + image: crocodile.png +- emote: "crossed_flags" + image: crossed_flags.png +- emote: "crown" + image: crown.png +- emote: "cry" + image: cry.png +- emote: "crying_cat_face" + image: crying_cat_face.png +- emote: "crystal_ball" + image: crystal_ball.png +- emote: "cupid" + image: cupid.png +- emote: "curly_loop" + image: curly_loop.png +- emote: "currency_exchange" + image: currency_exchange.png +- emote: "curry" + image: curry.png +- emote: "custard" + image: custard.png +- emote: "customs" + image: customs.png +- emote: "cyclone" + image: cyclone.png +- emote: "dancer" + image: dancer.png +- emote: "dancers" + image: dancers.png +- emote: "dango" + image: dango.png +- emote: "dart" + image: dart.png +- emote: "dash" + image: dash.png +- emote: "date" + image: date.png +- emote: "de" + image: de.png +- emote: "deciduous_tree" + image: deciduous_tree.png +- emote: "department_store" + image: department_store.png +- emote: "diamond_shape_with_a_dot_inside" + image: diamond_shape_with_a_dot_inside.png +- emote: "diamonds" + image: diamonds.png +- emote: "disappointed_relieved" + image: disappointed_relieved.png +- emote: "disappointed" + image: disappointed.png +- emote: "dizzy_face" + image: dizzy_face.png +- emote: "dizzy" + image: dizzy.png +- emote: "do_not_litter" + image: do_not_litter.png +- emote: "dog" + image: dog.png +- emote: "dog2" + image: dog2.png +- emote: "dollar" + image: dollar.png +- emote: "dolls" + image: dolls.png +- emote: "dolphin" + image: dolphin.png +- emote: "donut" + image: donut.png +- emote: "door" + image: door.png +- emote: "doughnut" + image: doughnut.png +- emote: "dragon_face" + image: dragon_face.png +- emote: "dragon" + image: dragon.png +- emote: "dress" + image: dress.png +- emote: "dromedary_camel" + image: dromedary_camel.png +- emote: "droplet" + image: droplet.png +- emote: "dvd" + image: dvd.png +- emote: "e-mail" + image: e-mail.png +- emote: "ear_of_rice" + image: ear_of_rice.png +- emote: "ear" + image: ear.png +- emote: "earth_africa" + image: earth_africa.png +- emote: "earth_americas" + image: earth_americas.png +- emote: "earth_asia" + image: earth_asia.png +- emote: "egg" + image: egg.png +- emote: "eggplant" + image: eggplant.png +- emote: "eight_pointed_black_star" + image: eight_pointed_black_star.png +- emote: "eight_spoked_asterisk" + image: eight_spoked_asterisk.png +- emote: "eight" + image: eight.png +- emote: "electric_plug" + image: electric_plug.png +- emote: "elephant" + image: elephant.png +- emote: "email" + image: email.png +- emote: "end" + image: end.png +- emote: "envelope" + image: envelope.png +- emote: "es" + image: es.png +- emote: "euro" + image: euro.png +- emote: "european_castle" + image: european_castle.png +- emote: "european_post_office" + image: european_post_office.png +- emote: "evergreen_tree" + image: evergreen_tree.png +- emote: "exclamation" + image: exclamation.png +- emote: "expressionless" + image: expressionless.png +- emote: "eyeglasses" + image: eyeglasses.png +- emote: "eyes" + image: eyes.png +- emote: "facepunch" + image: facepunch.png +- emote: "factory" + image: factory.png +- emote: "fallen_leaf" + image: fallen_leaf.png +- emote: "family" + image: family.png +- emote: "fast_forward" + image: fast_forward.png +- emote: "fax" + image: fax.png +- emote: "fearful" + image: fearful.png +- emote: "feelsgood" + image: feelsgood.png +- emote: "feet" + image: feet.png +- emote: "ferris_wheel" + image: ferris_wheel.png +- emote: "file_folder" + image: file_folder.png +- emote: "finnadie" + image: finnadie.png +- emote: "fire_engine" + image: fire_engine.png +- emote: "fire" + image: fire.png +- emote: "fireworks" + image: fireworks.png +- emote: "first_quarter_moon_with_face" + image: first_quarter_moon_with_face.png +- emote: "first_quarter_moon" + image: first_quarter_moon.png +- emote: "fish_cake" + image: fish_cake.png +- emote: "fish" + image: fish.png +- emote: "fishing_pole_and_fish" + image: fishing_pole_and_fish.png +- emote: "fist" + image: fist.png +- emote: "five" + image: five.png +- emote: "flags" + image: flags.png +- emote: "flashlight" + image: flashlight.png +- emote: "floppy_disk" + image: floppy_disk.png +- emote: "flower_playing_cards" + image: flower_playing_cards.png +- emote: "flushed" + image: flushed.png +- emote: "foggy" + image: foggy.png +- emote: "football" + image: football.png +- emote: "fork_and_knife" + image: fork_and_knife.png +- emote: "fountain" + image: fountain.png +- emote: "four_leaf_clover" + image: four_leaf_clover.png +- emote: "four" + image: four.png +- emote: "fr" + image: fr.png +- emote: "free" + image: free.png +- emote: "fried_shrimp" + image: fried_shrimp.png +- emote: "fries" + image: fries.png +- emote: "frog" + image: frog.png +- emote: "frowning" + image: frowning.png +- emote: "fu" + image: fu.png +- emote: "fuelpump" + image: fuelpump.png +- emote: "full_moon_with_face" + image: full_moon_with_face.png +- emote: "full_moon" + image: full_moon.png +- emote: "game_die" + image: game_die.png +- emote: "gb" + image: gb.png +- emote: "gem" + image: gem.png +- emote: "gemini" + image: gemini.png +- emote: "ghost" + image: ghost.png +- emote: "gift_heart" + image: gift_heart.png +- emote: "gift" + image: gift.png +- emote: "girl" + image: girl.png +- emote: "globe_with_meridians" + image: globe_with_meridians.png +- emote: "goat" + image: goat.png +- emote: "goberserk" + image: goberserk.png +- emote: "godmode" + image: godmode.png +- emote: "golf" + image: golf.png +- emote: "grapes" + image: grapes.png +- emote: "green_apple" + image: green_apple.png +- emote: "green_book" + image: green_book.png +- emote: "green_heart" + image: green_heart.png +- emote: "grey_exclamation" + image: grey_exclamation.png +- emote: "grey_question" + image: grey_question.png +- emote: "grimacing" + image: grimacing.png +- emote: "grin" + image: grin.png +- emote: "grinning" + image: grinning.png +- emote: "guardsman" + image: guardsman.png +- emote: "guitar" + image: guitar.png +- emote: "gun" + image: gun.png +- emote: "haircut" + image: haircut.png +- emote: "hamburger" + image: hamburger.png +- emote: "hammer" + image: hammer.png +- emote: "hamster" + image: hamster.png +- emote: "hand" + image: hand.png +- emote: "handbag" + image: handbag.png +- emote: "hankey" + image: hankey.png +- emote: "hash" + image: hash.png +- emote: "hatched_chick" + image: hatched_chick.png +- emote: "hatching_chick" + image: hatching_chick.png +- emote: "headphones" + image: headphones.png +- emote: "hear_no_evil" + image: hear_no_evil.png +- emote: "heart_decoration" + image: heart_decoration.png +- emote: "heart_eyes_cat" + image: heart_eyes_cat.png +- emote: "heart_eyes" + image: heart_eyes.png +- emote: "heart" + image: heart.png +- emote: "heartbeat" + image: heartbeat.png +- emote: "heartpulse" + image: heartpulse.png +- emote: "hearts" + image: hearts.png +- emote: "heavy_check_mark" + image: heavy_check_mark.png +- emote: "heavy_division_sign" + image: heavy_division_sign.png +- emote: "heavy_dollar_sign" + image: heavy_dollar_sign.png +- emote: "heavy_exclamation_mark" + image: heavy_exclamation_mark.png +- emote: "heavy_minus_sign" + image: heavy_minus_sign.png +- emote: "heavy_multiplication_x" + image: heavy_multiplication_x.png +- emote: "heavy_plus_sign" + image: heavy_plus_sign.png +- emote: "helicopter" + image: helicopter.png +- emote: "herb" + image: herb.png +- emote: "hibiscus" + image: hibiscus.png +- emote: "high_brightness" + image: high_brightness.png +- emote: "high_heel" + image: high_heel.png +- emote: "hocho" + image: hocho.png +- emote: "honey_pot" + image: honey_pot.png +- emote: "honeybee" + image: honeybee.png +- emote: "horse_racing" + image: horse_racing.png +- emote: "horse" + image: horse.png +- emote: "hospital" + image: hospital.png +- emote: "hotel" + image: hotel.png +- emote: "hotsprings" + image: hotsprings.png +- emote: "hourglass_flowing_sand" + image: hourglass_flowing_sand.png +- emote: "hourglass" + image: hourglass.png +- emote: "house_with_garden" + image: house_with_garden.png +- emote: "house" + image: house.png +- emote: "hurtrealbad" + image: hurtrealbad.png +- emote: "hushed" + image: hushed.png +- emote: "ice_cream" + image: ice_cream.png +- emote: "icecream" + image: icecream.png +- emote: "id" + image: id.png +- emote: "ideograph_advantage" + image: ideograph_advantage.png +- emote: "imp" + image: imp.png +- emote: "inbox_tray" + image: inbox_tray.png +- emote: "incoming_envelope" + image: incoming_envelope.png +- emote: "information_desk_person" + image: information_desk_person.png +- emote: "information_source" + image: information_source.png +- emote: "innocent" + image: innocent.png +- emote: "interrobang" + image: interrobang.png +- emote: "iphone" + image: iphone.png +- emote: "it" + image: it.png +- emote: "izakaya_lantern" + image: izakaya_lantern.png +- emote: "jack_o_lantern" + image: jack_o_lantern.png +- emote: "japan" + image: japan.png +- emote: "japanese_castle" + image: japanese_castle.png +- emote: "japanese_goblin" + image: japanese_goblin.png +- emote: "japanese_ogre" + image: japanese_ogre.png +- emote: "jeans" + image: jeans.png +- emote: "joy_cat" + image: joy_cat.png +- emote: "joy" + image: joy.png +- emote: "jp" + image: jp.png +- emote: "key" + image: key.png +- emote: "keycap_ten" + image: keycap_ten.png +- emote: "kimono" + image: kimono.png +- emote: "kiss" + image: kiss.png +- emote: "kissing_cat" + image: kissing_cat.png +- emote: "kissing_closed_eyes" + image: kissing_closed_eyes.png +- emote: "kissing_face" + image: kissing_face.png +- emote: "kissing_heart" + image: kissing_heart.png +- emote: "kissing_smiling_eyes" + image: kissing_smiling_eyes.png +- emote: "kissing" + image: kissing.png +- emote: "koala" + image: koala.png +- emote: "koko" + image: koko.png +- emote: "kr" + image: kr.png +- emote: "large_blue_circle" + image: large_blue_circle.png +- emote: "large_blue_diamond" + image: large_blue_diamond.png +- emote: "large_orange_diamond" + image: large_orange_diamond.png +- emote: "last_quarter_moon_with_face" + image: last_quarter_moon_with_face.png +- emote: "last_quarter_moon" + image: last_quarter_moon.png +- emote: "laughing" + image: laughing.png +- emote: "leaves" + image: leaves.png +- emote: "ledger" + image: ledger.png +- emote: "left_luggage" + image: left_luggage.png +- emote: "left_right_arrow" + image: left_right_arrow.png +- emote: "leftwards_arrow_with_hook" + image: leftwards_arrow_with_hook.png +- emote: "lemon" + image: lemon.png +- emote: "leo" + image: leo.png +- emote: "leopard" + image: leopard.png +- emote: "libra" + image: libra.png +- emote: "light_rail" + image: light_rail.png +- emote: "link" + image: link.png +- emote: "lips" + image: lips.png +- emote: "lipstick" + image: lipstick.png +- emote: "lock_with_ink_pen" + image: lock_with_ink_pen.png +- emote: "lock" + image: lock.png +- emote: "lollipop" + image: lollipop.png +- emote: "loop" + image: loop.png +- emote: "loudspeaker" + image: loudspeaker.png +- emote: "love_hotel" + image: love_hotel.png +- emote: "love_letter" + image: love_letter.png +- emote: "low_brightness" + image: low_brightness.png +- emote: "m" + image: m.png +- emote: "mag_right" + image: mag_right.png +- emote: "mag" + image: mag.png +- emote: "mahjong" + image: mahjong.png +- emote: "mailbox_closed" + image: mailbox_closed.png +- emote: "mailbox_with_mail" + image: mailbox_with_mail.png +- emote: "mailbox_with_no_mail" + image: mailbox_with_no_mail.png +- emote: "mailbox" + image: mailbox.png +- emote: "man_with_gua_pi_mao" + image: man_with_gua_pi_mao.png +- emote: "man_with_turban" + image: man_with_turban.png +- emote: "man" + image: man.png +- emote: "mans_shoe" + image: mans_shoe.png +- emote: "maple_leaf" + image: maple_leaf.png +- emote: "mask" + image: mask.png +- emote: "massage" + image: massage.png +- emote: "meat_on_bone" + image: meat_on_bone.png +- emote: "mega" + image: mega.png +- emote: "melon" + image: melon.png +- emote: "memo" + image: memo.png +- emote: "mens" + image: mens.png +- emote: "metal" + image: metal.png +- emote: "metro" + image: metro.png +- emote: "microphone" + image: microphone.png +- emote: "microscope" + image: microscope.png +- emote: "milky_way" + image: milky_way.png +- emote: "minibus" + image: minibus.png +- emote: "minidisc" + image: minidisc.png +- emote: "mobile_phone_off" + image: mobile_phone_off.png +- emote: "money_with_wings" + image: money_with_wings.png +- emote: "moneybag" + image: moneybag.png +- emote: "monkey_face" + image: monkey_face.png +- emote: "monkey" + image: monkey.png +- emote: "monorail" + image: monorail.png +- emote: "moon" + image: moon.png +- emote: "mortar_board" + image: mortar_board.png +- emote: "mount_fuji" + image: mount_fuji.png +- emote: "mountain_bicyclist" + image: mountain_bicyclist.png +- emote: "mountain_cableway" + image: mountain_cableway.png +- emote: "mountain_railway" + image: mountain_railway.png +- emote: "mouse" + image: mouse.png +- emote: "mouse2" + image: mouse2.png +- emote: "movie_camera" + image: movie_camera.png +- emote: "moyai" + image: moyai.png +- emote: "muscle" + image: muscle.png +- emote: "mushroom" + image: mushroom.png +- emote: "musical_keyboard" + image: musical_keyboard.png +- emote: "musical_note" + image: musical_note.png +- emote: "musical_score" + image: musical_score.png +- emote: "mute" + image: mute.png +- emote: "nail_care" + image: nail_care.png +- emote: "name_badge" + image: name_badge.png +- emote: "neckbeard" + image: neckbeard.png +- emote: "necktie" + image: necktie.png +- emote: "negative_squared_cross_mark" + image: negative_squared_cross_mark.png +- emote: "neutral_face" + image: neutral_face.png +- emote: "new_moon_with_face" + image: new_moon_with_face.png +- emote: "new_moon" + image: new_moon.png +- emote: "new" + image: new.png +- emote: "newspaper" + image: newspaper.png +- emote: "ng" + image: ng.png +- emote: "nine" + image: nine.png +- emote: "no_bell" + image: no_bell.png +- emote: "no_bicycles" + image: no_bicycles.png +- emote: "no_entry_sign" + image: no_entry_sign.png +- emote: "no_entry" + image: no_entry.png +- emote: "no_good" + image: no_good.png +- emote: "no_mobile_phones" + image: no_mobile_phones.png +- emote: "no_mouth" + image: no_mouth.png +- emote: "no_pedestrians" + image: no_pedestrians.png +- emote: "no_smoking" + image: no_smoking.png +- emote: "non-potable_water" + image: non-potable_water.png +- emote: "nose" + image: nose.png +- emote: "notebook_with_decorative_cover" + image: notebook_with_decorative_cover.png +- emote: "notebook" + image: notebook.png +- emote: "notes" + image: notes.png +- emote: "nut_and_bolt" + image: nut_and_bolt.png +- emote: "o" + image: o.png +- emote: "o2" + image: o2.png +- emote: "ocean" + image: ocean.png +- emote: "octocat" + image: octocat.png +- emote: "octopus" + image: octopus.png +- emote: "oden" + image: oden.png +- emote: "office" + image: office.png +- emote: "ok_hand" + image: ok_hand.png +- emote: "ok_woman" + image: ok_woman.png +- emote: "ok" + image: ok.png +- emote: "older_man" + image: older_man.png +- emote: "older_woman" + image: older_woman.png +- emote: "on" + image: on.png +- emote: "oncoming_automobile" + image: oncoming_automobile.png +- emote: "oncoming_bus" + image: oncoming_bus.png +- emote: "oncoming_police_car" + image: oncoming_police_car.png +- emote: "oncoming_taxi" + image: oncoming_taxi.png +- emote: "one" + image: one.png +- emote: "open_file_folder" + image: open_file_folder.png +- emote: "open_hands" + image: open_hands.png +- emote: "open_mouth" + image: open_mouth.png +- emote: "ophiuchus" + image: ophiuchus.png +- emote: "orange_book" + image: orange_book.png +- emote: "outbox_tray" + image: outbox_tray.png +- emote: "ox" + image: ox.png +- emote: "package" + image: package.png +- emote: "page_facing_up" + image: page_facing_up.png +- emote: "page_with_curl" + image: page_with_curl.png +- emote: "pager" + image: pager.png +- emote: "palm_tree" + image: palm_tree.png +- emote: "panda_face" + image: panda_face.png +- emote: "paperclip" + image: paperclip.png +- emote: "parking" + image: parking.png +- emote: "part_alternation_mark" + image: part_alternation_mark.png +- emote: "partly_sunny" + image: partly_sunny.png +- emote: "passport_control" + image: passport_control.png +- emote: "paw_prints" + image: paw_prints.png +- emote: "peach" + image: peach.png +- emote: "pear" + image: pear.png +- emote: "pencil" + image: pencil.png +- emote: "pencil2" + image: pencil2.png +- emote: "penguin" + image: penguin.png +- emote: "pensive" + image: pensive.png +- emote: "performing_arts" + image: performing_arts.png +- emote: "persevere" + image: persevere.png +- emote: "person_frowning" + image: person_frowning.png +- emote: "person_with_blond_hair" + image: person_with_blond_hair.png +- emote: "person_with_pouting_face" + image: person_with_pouting_face.png +- emote: "phone" + image: phone.png +- emote: "pig_nose" + image: pig_nose.png +- emote: "pig" + image: pig.png +- emote: "pig2" + image: pig2.png +- emote: "pill" + image: pill.png +- emote: "pineapple" + image: pineapple.png +- emote: "pisces" + image: pisces.png +- emote: "pizza" + image: pizza.png +- emote: "plus1" + image: plus1.png +- emote: "point_down" + image: point_down.png +- emote: "point_left" + image: point_left.png +- emote: "point_right" + image: point_right.png +- emote: "point_up_2" + image: point_up_2.png +- emote: "point_up" + image: point_up.png +- emote: "police_car" + image: police_car.png +- emote: "poodle" + image: poodle.png +- emote: "poop" + image: poop.png +- emote: "post_office" + image: post_office.png +- emote: "postal_horn" + image: postal_horn.png +- emote: "postbox" + image: postbox.png +- emote: "potable_water" + image: potable_water.png +- emote: "pouch" + image: pouch.png +- emote: "poultry_leg" + image: poultry_leg.png +- emote: "pound" + image: pound.png +- emote: "pouting_cat" + image: pouting_cat.png +- emote: "pray" + image: pray.png +- emote: "princess" + image: princess.png +- emote: "punch" + image: punch.png +- emote: "purple_heart" + image: purple_heart.png +- emote: "purse" + image: purse.png +- emote: "pushpin" + image: pushpin.png +- emote: "put_litter_in_its_place" + image: put_litter_in_its_place.png +- emote: "question" + image: question.png +- emote: "rabbit" + image: rabbit.png +- emote: "rabbit2" + image: rabbit2.png +- emote: "racehorse" + image: racehorse.png +- emote: "radio_button" + image: radio_button.png +- emote: "radio" + image: radio.png +- emote: "rage" + image: rage.png +- emote: "rage1" + image: rage1.png +- emote: "rage2" + image: rage2.png +- emote: "rage3" + image: rage3.png +- emote: "rage4" + image: rage4.png +- emote: "railway_car" + image: railway_car.png +- emote: "rainbow" + image: rainbow.png +- emote: "raised_hand" + image: raised_hand.png +- emote: "raised_hands" + image: raised_hands.png +- emote: "raising_hand" + image: raising_hand.png +- emote: "ram" + image: ram.png +- emote: "ramen" + image: ramen.png +- emote: "rat" + image: rat.png +- emote: "recycle" + image: recycle.png +- emote: "red_car" + image: red_car.png +- emote: "red_circle" + image: red_circle.png +- emote: "registered" + image: registered.png +- emote: "relaxed" + image: relaxed.png +- emote: "relieved" + image: relieved.png +- emote: "repeat_one" + image: repeat_one.png +- emote: "repeat" + image: repeat.png +- emote: "restroom" + image: restroom.png +- emote: "revolving_hearts" + image: revolving_hearts.png +- emote: "rewind" + image: rewind.png +- emote: "ribbon" + image: ribbon.png +- emote: "rice_ball" + image: rice_ball.png +- emote: "rice_cracker" + image: rice_cracker.png +- emote: "rice_scene" + image: rice_scene.png +- emote: "rice" + image: rice.png +- emote: "ring" + image: ring.png +- emote: "rocket" + image: rocket.png +- emote: "roller_coaster" + image: roller_coaster.png +- emote: "rooster" + image: rooster.png +- emote: "rose" + image: rose.png +- emote: "rotating_light" + image: rotating_light.png +- emote: "round_pushpin" + image: round_pushpin.png +- emote: "rowboat" + image: rowboat.png +- emote: "ru" + image: ru.png +- emote: "rugby_football" + image: rugby_football.png +- emote: "runner" + image: runner.png +- emote: "running_shirt_with_sash" + image: running_shirt_with_sash.png +- emote: "running" + image: running.png +- emote: "sa" + image: sa.png +- emote: "sagittarius" + image: sagittarius.png +- emote: "sailboat" + image: sailboat.png +- emote: "sake" + image: sake.png +- emote: "sandal" + image: sandal.png +- emote: "santa" + image: santa.png +- emote: "satellite" + image: satellite.png +- emote: "satisfied" + image: satisfied.png +- emote: "saxophone" + image: saxophone.png +- emote: "school_satchel" + image: school_satchel.png +- emote: "school" + image: school.png +- emote: "scissors" + image: scissors.png +- emote: "scorpius" + image: scorpius.png +- emote: "scream_cat" + image: scream_cat.png +- emote: "scream" + image: scream.png +- emote: "scroll" + image: scroll.png +- emote: "seat" + image: seat.png +- emote: "secret" + image: secret.png +- emote: "see_no_evil" + image: see_no_evil.png +- emote: "seedling" + image: seedling.png +- emote: "seven" + image: seven.png +- emote: "shaved_ice" + image: shaved_ice.png +- emote: "sheep" + image: sheep.png +- emote: "shell" + image: shell.png +- emote: "ship" + image: ship.png +- emote: "shipit" + image: shipit.png +- emote: "shirt" + image: shirt.png +- emote: "shit" + image: shit.png +- emote: "shoe" + image: shoe.png +- emote: "shower" + image: shower.png +- emote: "signal_strength" + image: signal_strength.png +- emote: "six_pointed_star" + image: six_pointed_star.png +- emote: "six" + image: six.png +- emote: "ski" + image: ski.png +- emote: "skull" + image: skull.png +- emote: "sleeping" + image: sleeping.png +- emote: "sleepy" + image: sleepy.png +- emote: "slot_machine" + image: slot_machine.png +- emote: "small_blue_diamond" + image: small_blue_diamond.png +- emote: "small_orange_diamond" + image: small_orange_diamond.png +- emote: "small_red_triangle_down" + image: small_red_triangle_down.png +- emote: "small_red_triangle" + image: small_red_triangle.png +- emote: "smile_cat" + image: smile_cat.png +- emote: "smile" + image: smile.png +- emote: "smiley_cat" + image: smiley_cat.png +- emote: "smiley" + image: smiley.png +- emote: "smiling_imp" + image: smiling_imp.png +- emote: "smirk_cat" + image: smirk_cat.png +- emote: "smirk" + image: smirk.png +- emote: "smoking" + image: smoking.png +- emote: "snail" + image: snail.png +- emote: "snake" + image: snake.png +- emote: "snowboarder" + image: snowboarder.png +- emote: "snowflake" + image: snowflake.png +- emote: "snowman" + image: snowman.png +- emote: "sob" + image: sob.png +- emote: "soccer" + image: soccer.png +- emote: "soon" + image: soon.png +- emote: "sos" + image: sos.png +- emote: "sound" + image: sound.png +- emote: "space_invader" + image: space_invader.png +- emote: "spades" + image: spades.png +- emote: "spaghetti" + image: spaghetti.png +- emote: "sparkle" + image: sparkle.png +- emote: "sparkler" + image: sparkler.png +- emote: "sparkles" + image: sparkles.png +- emote: "sparkling_heart" + image: sparkling_heart.png +- emote: "speak_no_evil" + image: speak_no_evil.png +- emote: "speaker" + image: speaker.png +- emote: "speech_balloon" + image: speech_balloon.png +- emote: "speedboat" + image: speedboat.png +- emote: "squirrel" + image: squirrel.png +- emote: "star" + image: star.png +- emote: "star2" + image: star2.png +- emote: "stars" + image: stars.png +- emote: "station" + image: station.png +- emote: "statue_of_liberty" + image: statue_of_liberty.png +- emote: "steam_locomotive" + image: steam_locomotive.png +- emote: "stew" + image: stew.png +- emote: "straight_ruler" + image: straight_ruler.png +- emote: "strawberry" + image: strawberry.png +- emote: "stuck_out_tongue_closed_eyes" + image: stuck_out_tongue_closed_eyes.png +- emote: "stuck_out_tongue_winking_eye" + image: stuck_out_tongue_winking_eye.png +- emote: "stuck_out_tongue" + image: stuck_out_tongue.png +- emote: "sun_with_face" + image: sun_with_face.png +- emote: "sunflower" + image: sunflower.png +- emote: "sunglasses" + image: sunglasses.png +- emote: "sunny" + image: sunny.png +- emote: "sunrise_over_mountains" + image: sunrise_over_mountains.png +- emote: "sunrise" + image: sunrise.png +- emote: "surfer" + image: surfer.png +- emote: "sushi" + image: sushi.png +- emote: "suspect" + image: suspect.png +- emote: "suspension_railway" + image: suspension_railway.png +- emote: "sweat_drops" + image: sweat_drops.png +- emote: "sweat_smile" + image: sweat_smile.png +- emote: "sweat" + image: sweat.png +- emote: "sweet_potato" + image: sweet_potato.png +- emote: "swimmer" + image: swimmer.png +- emote: "symbols" + image: symbols.png +- emote: "syringe" + image: syringe.png +- emote: "tada" + image: tada.png +- emote: "tanabata_tree" + image: tanabata_tree.png +- emote: "tangerine" + image: tangerine.png +- emote: "taurus" + image: taurus.png +- emote: "taxi" + image: taxi.png +- emote: "tea" + image: tea.png +- emote: "telephone_receiver" + image: telephone_receiver.png +- emote: "telephone" + image: telephone.png +- emote: "telescope" + image: telescope.png +- emote: "tennis" + image: tennis.png +- emote: "tent" + image: tent.png +- emote: "thought_balloon" + image: thought_balloon.png +- emote: "three" + image: three.png +- emote: "thumbsdown" + image: thumbsdown.png +- emote: "thumbsup" + image: thumbsup.png +- emote: "ticket" + image: ticket.png +- emote: "tiger" + image: tiger.png +- emote: "tiger2" + image: tiger2.png +- emote: "tired_face" + image: tired_face.png +- emote: "tm" + image: tm.png +- emote: "toilet" + image: toilet.png +- emote: "tokyo_tower" + image: tokyo_tower.png +- emote: "tomato" + image: tomato.png +- emote: "tongue" + image: tongue.png +- emote: "top" + image: top.png +- emote: "tophat" + image: tophat.png +- emote: "tractor" + image: tractor.png +- emote: "traffic_light" + image: traffic_light.png +- emote: "train" + image: train.png +- emote: "train2" + image: train2.png +- emote: "tram" + image: tram.png +- emote: "triangular_flag_on_post" + image: triangular_flag_on_post.png +- emote: "triangular_ruler" + image: triangular_ruler.png +- emote: "trident" + image: trident.png +- emote: "triumph" + image: triumph.png +- emote: "trolleybus" + image: trolleybus.png +- emote: "trollface" + image: trollface.png +- emote: "trophy" + image: trophy.png +- emote: "tropical_drink" + image: tropical_drink.png +- emote: "tropical_fish" + image: tropical_fish.png +- emote: "truck" + image: truck.png +- emote: "trumpet" + image: trumpet.png +- emote: "tshirt" + image: tshirt.png +- emote: "tulip" + image: tulip.png +- emote: "turtle" + image: turtle.png +- emote: "tv" + image: tv.png +- emote: "twisted_rightwards_arrows" + image: twisted_rightwards_arrows.png +- emote: "two_hearts" + image: two_hearts.png +- emote: "two_men_holding_hands" + image: two_men_holding_hands.png +- emote: "two_women_holding_hands" + image: two_women_holding_hands.png +- emote: "two" + image: two.png +- emote: "u6e80" + image: u6e80.png +- emote: "u7a7a" + image: u7a7a.png +- emote: "u55b6" + image: u55b6.png +- emote: "u5272" + image: u5272.png +- emote: "u5408" + image: u5408.png +- emote: "u6307" + image: u6307.png +- emote: "u6708" + image: u6708.png +- emote: "u6709" + image: u6709.png +- emote: "u7121" + image: u7121.png +- emote: "u7533" + image: u7533.png +- emote: "u7981" + image: u7981.png +- emote: "uk" + image: uk.png +- emote: "umbrella" + image: umbrella.png +- emote: "unamused" + image: unamused.png +- emote: "underage" + image: underage.png +- emote: "unlock" + image: unlock.png +- emote: "up" + image: up.png +- emote: "us" + image: us.png +- emote: "v" + image: v.png +- emote: "vertical_traffic_light" + image: vertical_traffic_light.png +- emote: "vhs" + image: vhs.png +- emote: "vibration_mode" + image: vibration_mode.png +- emote: "video_camera" + image: video_camera.png +- emote: "video_game" + image: video_game.png +- emote: "violin" + image: violin.png +- emote: "virgo" + image: virgo.png +- emote: "volcano" + image: volcano.png +- emote: "vs" + image: vs.png +- emote: "walking" + image: walking.png +- emote: "waning_crescent_moon" + image: waning_crescent_moon.png +- emote: "waning_gibbous_moon" + image: waning_gibbous_moon.png +- emote: "warning" + image: warning.png +- emote: "watch" + image: watch.png +- emote: "water_buffalo" + image: water_buffalo.png +- emote: "watermelon" + image: watermelon.png +- emote: "wave" + image: wave.png +- emote: "wavy_dash" + image: wavy_dash.png +- emote: "waxing_crescent_moon" + image: waxing_crescent_moon.png +- emote: "waxing_gibbous_moon" + image: waxing_gibbous_moon.png +- emote: "wc" + image: wc.png +- emote: "weary" + image: weary.png +- emote: "wedding" + image: wedding.png +- emote: "whale" + image: whale.png +- emote: "whale2" + image: whale2.png +- emote: "wheelchair" + image: wheelchair.png +- emote: "white_check_mark" + image: white_check_mark.png +- emote: "white_circle" + image: white_circle.png +- emote: "white_flower" + image: white_flower.png +- emote: "white_large_square" + image: white_large_square.png +- emote: "white_medium_small_square" + image: white_medium_small_square.png +- emote: "white_medium_square" + image: white_medium_square.png +- emote: "white_small_square" + image: white_small_square.png +- emote: "white_square_button" + image: white_square_button.png +- emote: "wind_chime" + image: wind_chime.png +- emote: "wine_glass" + image: wine_glass.png +- emote: "wink" + image: wink.png +- emote: "wolf" + image: wolf.png +- emote: "woman" + image: woman.png +- emote: "womans_clothes" + image: womans_clothes.png +- emote: "womans_hat" + image: womans_hat.png +- emote: "womens" + image: womens.png +- emote: "worried" + image: worried.png +- emote: "wrench" + image: wrench.png +- emote: "x" + image: x.png +- emote: "yellow_heart" + image: yellow_heart.png +- emote: "yen" + image: yen.png +- emote: "yum" + image: yum.png +- emote: "zap" + image: zap.png +- emote: "zero" + image: zero.png +- emote: "zzz" + image: zzz.png diff --git a/extras/emotes/lets-chat.yml b/extras/emotes/lets-chat.yml new file mode 100644 index 0000000..e83d644 --- /dev/null +++ b/extras/emotes/lets-chat.yml @@ -0,0 +1,12 @@ +# +# Emotes +# + +- emote: simon + image: simon.gif + size: 50 +- emote: houssam + image: houssam.gif + size: 50 +- emote: pistol + image: pistol.png diff --git a/extras/emotes/local.yml.sample b/extras/emotes/local.yml.sample new file mode 100644 index 0000000..ca07693 --- /dev/null +++ b/extras/emotes/local.yml.sample @@ -0,0 +1,6 @@ +# +# Emotes +# + +- emote: myemote + image: myemote.png \ No newline at end of file diff --git a/extras/emotes/public/.gitkeep b/extras/emotes/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/extras/emotes/public/default/+1.png b/extras/emotes/public/default/+1.png new file mode 100755 index 0000000..81786c1 Binary files /dev/null and b/extras/emotes/public/default/+1.png differ diff --git a/extras/emotes/public/default/-1.png b/extras/emotes/public/default/-1.png new file mode 100755 index 0000000..41c6b82 Binary files /dev/null and b/extras/emotes/public/default/-1.png differ diff --git a/extras/emotes/public/default/100.png b/extras/emotes/public/default/100.png new file mode 100755 index 0000000..ca3bb9b Binary files /dev/null and b/extras/emotes/public/default/100.png differ diff --git a/extras/emotes/public/default/1234.png b/extras/emotes/public/default/1234.png new file mode 100755 index 0000000..c47c2e1 Binary files /dev/null and b/extras/emotes/public/default/1234.png differ diff --git a/extras/emotes/public/default/8ball.png b/extras/emotes/public/default/8ball.png new file mode 100755 index 0000000..c2c710d Binary files /dev/null and b/extras/emotes/public/default/8ball.png differ diff --git a/extras/emotes/public/default/a.png b/extras/emotes/public/default/a.png new file mode 100755 index 0000000..09ff6d6 Binary files /dev/null and b/extras/emotes/public/default/a.png differ diff --git a/extras/emotes/public/default/ab.png b/extras/emotes/public/default/ab.png new file mode 100755 index 0000000..2a52220 Binary files /dev/null and b/extras/emotes/public/default/ab.png differ diff --git a/extras/emotes/public/default/abc.png b/extras/emotes/public/default/abc.png new file mode 100755 index 0000000..505d40a Binary files /dev/null and b/extras/emotes/public/default/abc.png differ diff --git a/extras/emotes/public/default/abcd.png b/extras/emotes/public/default/abcd.png new file mode 100755 index 0000000..5218470 Binary files /dev/null and b/extras/emotes/public/default/abcd.png differ diff --git a/extras/emotes/public/default/accept.png b/extras/emotes/public/default/accept.png new file mode 100755 index 0000000..2d20090 Binary files /dev/null and b/extras/emotes/public/default/accept.png differ diff --git a/extras/emotes/public/default/aerial_tramway.png b/extras/emotes/public/default/aerial_tramway.png new file mode 100755 index 0000000..38f6dfe Binary files /dev/null and b/extras/emotes/public/default/aerial_tramway.png differ diff --git a/extras/emotes/public/default/airplane.png b/extras/emotes/public/default/airplane.png new file mode 100755 index 0000000..8407cb6 Binary files /dev/null and b/extras/emotes/public/default/airplane.png differ diff --git a/extras/emotes/public/default/alarm_clock.png b/extras/emotes/public/default/alarm_clock.png new file mode 100755 index 0000000..86ca8c8 Binary files /dev/null and b/extras/emotes/public/default/alarm_clock.png differ diff --git a/extras/emotes/public/default/alien.png b/extras/emotes/public/default/alien.png new file mode 100755 index 0000000..416de47 Binary files /dev/null and b/extras/emotes/public/default/alien.png differ diff --git a/extras/emotes/public/default/ambulance.png b/extras/emotes/public/default/ambulance.png new file mode 100755 index 0000000..b740f45 Binary files /dev/null and b/extras/emotes/public/default/ambulance.png differ diff --git a/extras/emotes/public/default/anchor.png b/extras/emotes/public/default/anchor.png new file mode 100755 index 0000000..0c5192e Binary files /dev/null and b/extras/emotes/public/default/anchor.png differ diff --git a/extras/emotes/public/default/angel.png b/extras/emotes/public/default/angel.png new file mode 100755 index 0000000..da52c31 Binary files /dev/null and b/extras/emotes/public/default/angel.png differ diff --git a/extras/emotes/public/default/anger.png b/extras/emotes/public/default/anger.png new file mode 100755 index 0000000..6fb4dca Binary files /dev/null and b/extras/emotes/public/default/anger.png differ diff --git a/extras/emotes/public/default/angry.png b/extras/emotes/public/default/angry.png new file mode 100755 index 0000000..f95bfa8 Binary files /dev/null and b/extras/emotes/public/default/angry.png differ diff --git a/extras/emotes/public/default/anguished.png b/extras/emotes/public/default/anguished.png new file mode 100755 index 0000000..c625947 Binary files /dev/null and b/extras/emotes/public/default/anguished.png differ diff --git a/extras/emotes/public/default/ant.png b/extras/emotes/public/default/ant.png new file mode 100755 index 0000000..b92d1cc Binary files /dev/null and b/extras/emotes/public/default/ant.png differ diff --git a/extras/emotes/public/default/apple.png b/extras/emotes/public/default/apple.png new file mode 100755 index 0000000..08aa17b Binary files /dev/null and b/extras/emotes/public/default/apple.png differ diff --git a/extras/emotes/public/default/aquarius.png b/extras/emotes/public/default/aquarius.png new file mode 100755 index 0000000..cbff66e Binary files /dev/null and b/extras/emotes/public/default/aquarius.png differ diff --git a/extras/emotes/public/default/aries.png b/extras/emotes/public/default/aries.png new file mode 100755 index 0000000..aab5e88 Binary files /dev/null and b/extras/emotes/public/default/aries.png differ diff --git a/extras/emotes/public/default/arrow_backward.png b/extras/emotes/public/default/arrow_backward.png new file mode 100755 index 0000000..0886218 Binary files /dev/null and b/extras/emotes/public/default/arrow_backward.png differ diff --git a/extras/emotes/public/default/arrow_double_down.png b/extras/emotes/public/default/arrow_double_down.png new file mode 100755 index 0000000..2ecbebc Binary files /dev/null and b/extras/emotes/public/default/arrow_double_down.png differ diff --git a/extras/emotes/public/default/arrow_double_up.png b/extras/emotes/public/default/arrow_double_up.png new file mode 100755 index 0000000..2bd6659 Binary files /dev/null and b/extras/emotes/public/default/arrow_double_up.png differ diff --git a/extras/emotes/public/default/arrow_down.png b/extras/emotes/public/default/arrow_down.png new file mode 100755 index 0000000..e6702f0 Binary files /dev/null and b/extras/emotes/public/default/arrow_down.png differ diff --git a/extras/emotes/public/default/arrow_down_small.png b/extras/emotes/public/default/arrow_down_small.png new file mode 100755 index 0000000..22d383a Binary files /dev/null and b/extras/emotes/public/default/arrow_down_small.png differ diff --git a/extras/emotes/public/default/arrow_forward.png b/extras/emotes/public/default/arrow_forward.png new file mode 100755 index 0000000..fbfe711 Binary files /dev/null and b/extras/emotes/public/default/arrow_forward.png differ diff --git a/extras/emotes/public/default/arrow_heading_down.png b/extras/emotes/public/default/arrow_heading_down.png new file mode 100755 index 0000000..56dd3b9 Binary files /dev/null and b/extras/emotes/public/default/arrow_heading_down.png differ diff --git a/extras/emotes/public/default/arrow_heading_up.png b/extras/emotes/public/default/arrow_heading_up.png new file mode 100755 index 0000000..c8f670a Binary files /dev/null and b/extras/emotes/public/default/arrow_heading_up.png differ diff --git a/extras/emotes/public/default/arrow_left.png b/extras/emotes/public/default/arrow_left.png new file mode 100755 index 0000000..d64ac61 Binary files /dev/null and b/extras/emotes/public/default/arrow_left.png differ diff --git a/extras/emotes/public/default/arrow_lower_left.png b/extras/emotes/public/default/arrow_lower_left.png new file mode 100755 index 0000000..55fb03c Binary files /dev/null and b/extras/emotes/public/default/arrow_lower_left.png differ diff --git a/extras/emotes/public/default/arrow_lower_right.png b/extras/emotes/public/default/arrow_lower_right.png new file mode 100755 index 0000000..da8fb82 Binary files /dev/null and b/extras/emotes/public/default/arrow_lower_right.png differ diff --git a/extras/emotes/public/default/arrow_right.png b/extras/emotes/public/default/arrow_right.png new file mode 100755 index 0000000..6d483b5 Binary files /dev/null and b/extras/emotes/public/default/arrow_right.png differ diff --git a/extras/emotes/public/default/arrow_right_hook.png b/extras/emotes/public/default/arrow_right_hook.png new file mode 100755 index 0000000..8b4ea6e Binary files /dev/null and b/extras/emotes/public/default/arrow_right_hook.png differ diff --git a/extras/emotes/public/default/arrow_up.png b/extras/emotes/public/default/arrow_up.png new file mode 100755 index 0000000..b5b0688 Binary files /dev/null and b/extras/emotes/public/default/arrow_up.png differ diff --git a/extras/emotes/public/default/arrow_up_down.png b/extras/emotes/public/default/arrow_up_down.png new file mode 100755 index 0000000..be423de Binary files /dev/null and b/extras/emotes/public/default/arrow_up_down.png differ diff --git a/extras/emotes/public/default/arrow_up_small.png b/extras/emotes/public/default/arrow_up_small.png new file mode 100755 index 0000000..3f40bfb Binary files /dev/null and b/extras/emotes/public/default/arrow_up_small.png differ diff --git a/extras/emotes/public/default/arrow_upper_left.png b/extras/emotes/public/default/arrow_upper_left.png new file mode 100755 index 0000000..2950ae2 Binary files /dev/null and b/extras/emotes/public/default/arrow_upper_left.png differ diff --git a/extras/emotes/public/default/arrow_upper_right.png b/extras/emotes/public/default/arrow_upper_right.png new file mode 100755 index 0000000..e23790b Binary files /dev/null and b/extras/emotes/public/default/arrow_upper_right.png differ diff --git a/extras/emotes/public/default/arrows_clockwise.png b/extras/emotes/public/default/arrows_clockwise.png new file mode 100755 index 0000000..5f84d7e Binary files /dev/null and b/extras/emotes/public/default/arrows_clockwise.png differ diff --git a/extras/emotes/public/default/arrows_counterclockwise.png b/extras/emotes/public/default/arrows_counterclockwise.png new file mode 100755 index 0000000..3e06f5b Binary files /dev/null and b/extras/emotes/public/default/arrows_counterclockwise.png differ diff --git a/extras/emotes/public/default/art.png b/extras/emotes/public/default/art.png new file mode 100755 index 0000000..d45212b Binary files /dev/null and b/extras/emotes/public/default/art.png differ diff --git a/extras/emotes/public/default/articulated_lorry.png b/extras/emotes/public/default/articulated_lorry.png new file mode 100755 index 0000000..81ec1f9 Binary files /dev/null and b/extras/emotes/public/default/articulated_lorry.png differ diff --git a/extras/emotes/public/default/astonished.png b/extras/emotes/public/default/astonished.png new file mode 100755 index 0000000..858a834 Binary files /dev/null and b/extras/emotes/public/default/astonished.png differ diff --git a/extras/emotes/public/default/atm.png b/extras/emotes/public/default/atm.png new file mode 100755 index 0000000..c2846e7 Binary files /dev/null and b/extras/emotes/public/default/atm.png differ diff --git a/extras/emotes/public/default/b.png b/extras/emotes/public/default/b.png new file mode 100755 index 0000000..8742b3d Binary files /dev/null and b/extras/emotes/public/default/b.png differ diff --git a/extras/emotes/public/default/baby.png b/extras/emotes/public/default/baby.png new file mode 100755 index 0000000..3b29da4 Binary files /dev/null and b/extras/emotes/public/default/baby.png differ diff --git a/extras/emotes/public/default/baby_bottle.png b/extras/emotes/public/default/baby_bottle.png new file mode 100755 index 0000000..1b2cfe5 Binary files /dev/null and b/extras/emotes/public/default/baby_bottle.png differ diff --git a/extras/emotes/public/default/baby_chick.png b/extras/emotes/public/default/baby_chick.png new file mode 100755 index 0000000..9be8d29 Binary files /dev/null and b/extras/emotes/public/default/baby_chick.png differ diff --git a/extras/emotes/public/default/baby_symbol.png b/extras/emotes/public/default/baby_symbol.png new file mode 100755 index 0000000..2e58725 Binary files /dev/null and b/extras/emotes/public/default/baby_symbol.png differ diff --git a/extras/emotes/public/default/back.png b/extras/emotes/public/default/back.png new file mode 100755 index 0000000..0cde628 Binary files /dev/null and b/extras/emotes/public/default/back.png differ diff --git a/extras/emotes/public/default/baggage_claim.png b/extras/emotes/public/default/baggage_claim.png new file mode 100755 index 0000000..59ae044 Binary files /dev/null and b/extras/emotes/public/default/baggage_claim.png differ diff --git a/extras/emotes/public/default/balloon.png b/extras/emotes/public/default/balloon.png new file mode 100755 index 0000000..0344897 Binary files /dev/null and b/extras/emotes/public/default/balloon.png differ diff --git a/extras/emotes/public/default/ballot_box_with_check.png b/extras/emotes/public/default/ballot_box_with_check.png new file mode 100755 index 0000000..f07a466 Binary files /dev/null and b/extras/emotes/public/default/ballot_box_with_check.png differ diff --git a/extras/emotes/public/default/bamboo.png b/extras/emotes/public/default/bamboo.png new file mode 100755 index 0000000..fc858d0 Binary files /dev/null and b/extras/emotes/public/default/bamboo.png differ diff --git a/extras/emotes/public/default/banana.png b/extras/emotes/public/default/banana.png new file mode 100755 index 0000000..a0563af Binary files /dev/null and b/extras/emotes/public/default/banana.png differ diff --git a/extras/emotes/public/default/bangbang.png b/extras/emotes/public/default/bangbang.png new file mode 100755 index 0000000..7270f0a Binary files /dev/null and b/extras/emotes/public/default/bangbang.png differ diff --git a/extras/emotes/public/default/bank.png b/extras/emotes/public/default/bank.png new file mode 100755 index 0000000..1faa877 Binary files /dev/null and b/extras/emotes/public/default/bank.png differ diff --git a/extras/emotes/public/default/bar_chart.png b/extras/emotes/public/default/bar_chart.png new file mode 100755 index 0000000..09d7301 Binary files /dev/null and b/extras/emotes/public/default/bar_chart.png differ diff --git a/extras/emotes/public/default/barber.png b/extras/emotes/public/default/barber.png new file mode 100755 index 0000000..a10cb23 Binary files /dev/null and b/extras/emotes/public/default/barber.png differ diff --git a/extras/emotes/public/default/baseball.png b/extras/emotes/public/default/baseball.png new file mode 100755 index 0000000..da004e2 Binary files /dev/null and b/extras/emotes/public/default/baseball.png differ diff --git a/extras/emotes/public/default/basketball.png b/extras/emotes/public/default/basketball.png new file mode 100755 index 0000000..ef694be Binary files /dev/null and b/extras/emotes/public/default/basketball.png differ diff --git a/extras/emotes/public/default/bath.png b/extras/emotes/public/default/bath.png new file mode 100755 index 0000000..8f75d1d Binary files /dev/null and b/extras/emotes/public/default/bath.png differ diff --git a/extras/emotes/public/default/bathtub.png b/extras/emotes/public/default/bathtub.png new file mode 100755 index 0000000..1c3f844 Binary files /dev/null and b/extras/emotes/public/default/bathtub.png differ diff --git a/extras/emotes/public/default/battery.png b/extras/emotes/public/default/battery.png new file mode 100755 index 0000000..aa7eedc Binary files /dev/null and b/extras/emotes/public/default/battery.png differ diff --git a/extras/emotes/public/default/bear.png b/extras/emotes/public/default/bear.png new file mode 100755 index 0000000..f5afe92 Binary files /dev/null and b/extras/emotes/public/default/bear.png differ diff --git a/extras/emotes/public/default/bee.png b/extras/emotes/public/default/bee.png new file mode 100755 index 0000000..f537339 Binary files /dev/null and b/extras/emotes/public/default/bee.png differ diff --git a/extras/emotes/public/default/beer.png b/extras/emotes/public/default/beer.png new file mode 100755 index 0000000..cd78bed Binary files /dev/null and b/extras/emotes/public/default/beer.png differ diff --git a/extras/emotes/public/default/beers.png b/extras/emotes/public/default/beers.png new file mode 100755 index 0000000..cc5e4ab Binary files /dev/null and b/extras/emotes/public/default/beers.png differ diff --git a/extras/emotes/public/default/beetle.png b/extras/emotes/public/default/beetle.png new file mode 100755 index 0000000..222577c Binary files /dev/null and b/extras/emotes/public/default/beetle.png differ diff --git a/extras/emotes/public/default/beginner.png b/extras/emotes/public/default/beginner.png new file mode 100755 index 0000000..1f022d1 Binary files /dev/null and b/extras/emotes/public/default/beginner.png differ diff --git a/extras/emotes/public/default/bell.png b/extras/emotes/public/default/bell.png new file mode 100755 index 0000000..69acceb Binary files /dev/null and b/extras/emotes/public/default/bell.png differ diff --git a/extras/emotes/public/default/bento.png b/extras/emotes/public/default/bento.png new file mode 100755 index 0000000..d680112 Binary files /dev/null and b/extras/emotes/public/default/bento.png differ diff --git a/extras/emotes/public/default/bicyclist.png b/extras/emotes/public/default/bicyclist.png new file mode 100755 index 0000000..cbbd7c3 Binary files /dev/null and b/extras/emotes/public/default/bicyclist.png differ diff --git a/extras/emotes/public/default/bike.png b/extras/emotes/public/default/bike.png new file mode 100755 index 0000000..6573860 Binary files /dev/null and b/extras/emotes/public/default/bike.png differ diff --git a/extras/emotes/public/default/bikini.png b/extras/emotes/public/default/bikini.png new file mode 100755 index 0000000..4ff63b4 Binary files /dev/null and b/extras/emotes/public/default/bikini.png differ diff --git a/extras/emotes/public/default/bird.png b/extras/emotes/public/default/bird.png new file mode 100755 index 0000000..e6be8c0 Binary files /dev/null and b/extras/emotes/public/default/bird.png differ diff --git a/extras/emotes/public/default/birthday.png b/extras/emotes/public/default/birthday.png new file mode 100755 index 0000000..36e8edc Binary files /dev/null and b/extras/emotes/public/default/birthday.png differ diff --git a/extras/emotes/public/default/black_circle.png b/extras/emotes/public/default/black_circle.png new file mode 100755 index 0000000..e46f9df Binary files /dev/null and b/extras/emotes/public/default/black_circle.png differ diff --git a/extras/emotes/public/default/black_joker.png b/extras/emotes/public/default/black_joker.png new file mode 100755 index 0000000..4c78f36 Binary files /dev/null and b/extras/emotes/public/default/black_joker.png differ diff --git a/extras/emotes/public/default/black_medium_small_square.png b/extras/emotes/public/default/black_medium_small_square.png new file mode 100755 index 0000000..25bfe9c Binary files /dev/null and b/extras/emotes/public/default/black_medium_small_square.png differ diff --git a/extras/emotes/public/default/black_medium_square.png b/extras/emotes/public/default/black_medium_square.png new file mode 100755 index 0000000..204cce1 Binary files /dev/null and b/extras/emotes/public/default/black_medium_square.png differ diff --git a/extras/emotes/public/default/black_nib.png b/extras/emotes/public/default/black_nib.png new file mode 100755 index 0000000..29f6994 Binary files /dev/null and b/extras/emotes/public/default/black_nib.png differ diff --git a/extras/emotes/public/default/black_small_square.png b/extras/emotes/public/default/black_small_square.png new file mode 100755 index 0000000..a247751 Binary files /dev/null and b/extras/emotes/public/default/black_small_square.png differ diff --git a/extras/emotes/public/default/black_square.png b/extras/emotes/public/default/black_square.png new file mode 100755 index 0000000..71da10d Binary files /dev/null and b/extras/emotes/public/default/black_square.png differ diff --git a/extras/emotes/public/default/black_square_button.png b/extras/emotes/public/default/black_square_button.png new file mode 100755 index 0000000..f2597e9 Binary files /dev/null and b/extras/emotes/public/default/black_square_button.png differ diff --git a/extras/emotes/public/default/blossom.png b/extras/emotes/public/default/blossom.png new file mode 100755 index 0000000..55a9735 Binary files /dev/null and b/extras/emotes/public/default/blossom.png differ diff --git a/extras/emotes/public/default/blowfish.png b/extras/emotes/public/default/blowfish.png new file mode 100755 index 0000000..d3ad465 Binary files /dev/null and b/extras/emotes/public/default/blowfish.png differ diff --git a/extras/emotes/public/default/blue_book.png b/extras/emotes/public/default/blue_book.png new file mode 100755 index 0000000..e2b9e8c Binary files /dev/null and b/extras/emotes/public/default/blue_book.png differ diff --git a/extras/emotes/public/default/blue_car.png b/extras/emotes/public/default/blue_car.png new file mode 100755 index 0000000..978291e Binary files /dev/null and b/extras/emotes/public/default/blue_car.png differ diff --git a/extras/emotes/public/default/blue_heart.png b/extras/emotes/public/default/blue_heart.png new file mode 100755 index 0000000..baa29b3 Binary files /dev/null and b/extras/emotes/public/default/blue_heart.png differ diff --git a/extras/emotes/public/default/blush.png b/extras/emotes/public/default/blush.png new file mode 100755 index 0000000..3a95eb6 Binary files /dev/null and b/extras/emotes/public/default/blush.png differ diff --git a/extras/emotes/public/default/boar.png b/extras/emotes/public/default/boar.png new file mode 100755 index 0000000..8196ad4 Binary files /dev/null and b/extras/emotes/public/default/boar.png differ diff --git a/extras/emotes/public/default/boat.png b/extras/emotes/public/default/boat.png new file mode 100755 index 0000000..ff656dc Binary files /dev/null and b/extras/emotes/public/default/boat.png differ diff --git a/extras/emotes/public/default/bomb.png b/extras/emotes/public/default/bomb.png new file mode 100755 index 0000000..3289787 Binary files /dev/null and b/extras/emotes/public/default/bomb.png differ diff --git a/extras/emotes/public/default/book.png b/extras/emotes/public/default/book.png new file mode 100755 index 0000000..8b69841 Binary files /dev/null and b/extras/emotes/public/default/book.png differ diff --git a/extras/emotes/public/default/bookmark.png b/extras/emotes/public/default/bookmark.png new file mode 100755 index 0000000..6fc4ed9 Binary files /dev/null and b/extras/emotes/public/default/bookmark.png differ diff --git a/extras/emotes/public/default/bookmark_tabs.png b/extras/emotes/public/default/bookmark_tabs.png new file mode 100755 index 0000000..83782ff Binary files /dev/null and b/extras/emotes/public/default/bookmark_tabs.png differ diff --git a/extras/emotes/public/default/books.png b/extras/emotes/public/default/books.png new file mode 100755 index 0000000..dca06a1 Binary files /dev/null and b/extras/emotes/public/default/books.png differ diff --git a/extras/emotes/public/default/boom.png b/extras/emotes/public/default/boom.png new file mode 100755 index 0000000..9d5bd04 Binary files /dev/null and b/extras/emotes/public/default/boom.png differ diff --git a/extras/emotes/public/default/boot.png b/extras/emotes/public/default/boot.png new file mode 100755 index 0000000..58d0fdb Binary files /dev/null and b/extras/emotes/public/default/boot.png differ diff --git a/extras/emotes/public/default/bouquet.png b/extras/emotes/public/default/bouquet.png new file mode 100755 index 0000000..ce63783 Binary files /dev/null and b/extras/emotes/public/default/bouquet.png differ diff --git a/extras/emotes/public/default/bow.png b/extras/emotes/public/default/bow.png new file mode 100755 index 0000000..024cb61 Binary files /dev/null and b/extras/emotes/public/default/bow.png differ diff --git a/extras/emotes/public/default/bowling.png b/extras/emotes/public/default/bowling.png new file mode 100755 index 0000000..13d8ece Binary files /dev/null and b/extras/emotes/public/default/bowling.png differ diff --git a/extras/emotes/public/default/bowtie.png b/extras/emotes/public/default/bowtie.png new file mode 100755 index 0000000..28ff0c7 Binary files /dev/null and b/extras/emotes/public/default/bowtie.png differ diff --git a/extras/emotes/public/default/boy.png b/extras/emotes/public/default/boy.png new file mode 100755 index 0000000..f79f1f2 Binary files /dev/null and b/extras/emotes/public/default/boy.png differ diff --git a/extras/emotes/public/default/bread.png b/extras/emotes/public/default/bread.png new file mode 100755 index 0000000..7e7c637 Binary files /dev/null and b/extras/emotes/public/default/bread.png differ diff --git a/extras/emotes/public/default/bride_with_veil.png b/extras/emotes/public/default/bride_with_veil.png new file mode 100755 index 0000000..dd0b0cf Binary files /dev/null and b/extras/emotes/public/default/bride_with_veil.png differ diff --git a/extras/emotes/public/default/bridge_at_night.png b/extras/emotes/public/default/bridge_at_night.png new file mode 100755 index 0000000..495b06c Binary files /dev/null and b/extras/emotes/public/default/bridge_at_night.png differ diff --git a/extras/emotes/public/default/briefcase.png b/extras/emotes/public/default/briefcase.png new file mode 100755 index 0000000..46e82b0 Binary files /dev/null and b/extras/emotes/public/default/briefcase.png differ diff --git a/extras/emotes/public/default/broken_heart.png b/extras/emotes/public/default/broken_heart.png new file mode 100755 index 0000000..a1bc850 Binary files /dev/null and b/extras/emotes/public/default/broken_heart.png differ diff --git a/extras/emotes/public/default/bug.png b/extras/emotes/public/default/bug.png new file mode 100755 index 0000000..c2eaf7a Binary files /dev/null and b/extras/emotes/public/default/bug.png differ diff --git a/extras/emotes/public/default/bulb.png b/extras/emotes/public/default/bulb.png new file mode 100755 index 0000000..23afca1 Binary files /dev/null and b/extras/emotes/public/default/bulb.png differ diff --git a/extras/emotes/public/default/bullettrain_front.png b/extras/emotes/public/default/bullettrain_front.png new file mode 100755 index 0000000..16651ac Binary files /dev/null and b/extras/emotes/public/default/bullettrain_front.png differ diff --git a/extras/emotes/public/default/bullettrain_side.png b/extras/emotes/public/default/bullettrain_side.png new file mode 100755 index 0000000..8eca368 Binary files /dev/null and b/extras/emotes/public/default/bullettrain_side.png differ diff --git a/extras/emotes/public/default/bus.png b/extras/emotes/public/default/bus.png new file mode 100755 index 0000000..823aa39 Binary files /dev/null and b/extras/emotes/public/default/bus.png differ diff --git a/extras/emotes/public/default/busstop.png b/extras/emotes/public/default/busstop.png new file mode 100755 index 0000000..9489484 Binary files /dev/null and b/extras/emotes/public/default/busstop.png differ diff --git a/extras/emotes/public/default/bust_in_silhouette.png b/extras/emotes/public/default/bust_in_silhouette.png new file mode 100755 index 0000000..dd7defe Binary files /dev/null and b/extras/emotes/public/default/bust_in_silhouette.png differ diff --git a/extras/emotes/public/default/busts_in_silhouette.png b/extras/emotes/public/default/busts_in_silhouette.png new file mode 100755 index 0000000..1f3aabc Binary files /dev/null and b/extras/emotes/public/default/busts_in_silhouette.png differ diff --git a/extras/emotes/public/default/cactus.png b/extras/emotes/public/default/cactus.png new file mode 100755 index 0000000..5a2c3cc Binary files /dev/null and b/extras/emotes/public/default/cactus.png differ diff --git a/extras/emotes/public/default/cake.png b/extras/emotes/public/default/cake.png new file mode 100755 index 0000000..efeb9b4 Binary files /dev/null and b/extras/emotes/public/default/cake.png differ diff --git a/extras/emotes/public/default/calendar.png b/extras/emotes/public/default/calendar.png new file mode 100755 index 0000000..900b868 Binary files /dev/null and b/extras/emotes/public/default/calendar.png differ diff --git a/extras/emotes/public/default/calling.png b/extras/emotes/public/default/calling.png new file mode 100755 index 0000000..837897f Binary files /dev/null and b/extras/emotes/public/default/calling.png differ diff --git a/extras/emotes/public/default/camel.png b/extras/emotes/public/default/camel.png new file mode 100755 index 0000000..496c186 Binary files /dev/null and b/extras/emotes/public/default/camel.png differ diff --git a/extras/emotes/public/default/camera.png b/extras/emotes/public/default/camera.png new file mode 100755 index 0000000..397d03b Binary files /dev/null and b/extras/emotes/public/default/camera.png differ diff --git a/extras/emotes/public/default/cancer.png b/extras/emotes/public/default/cancer.png new file mode 100755 index 0000000..ea43a4a Binary files /dev/null and b/extras/emotes/public/default/cancer.png differ diff --git a/extras/emotes/public/default/candy.png b/extras/emotes/public/default/candy.png new file mode 100755 index 0000000..33722f2 Binary files /dev/null and b/extras/emotes/public/default/candy.png differ diff --git a/extras/emotes/public/default/capital_abcd.png b/extras/emotes/public/default/capital_abcd.png new file mode 100755 index 0000000..ffc0cba Binary files /dev/null and b/extras/emotes/public/default/capital_abcd.png differ diff --git a/extras/emotes/public/default/capricorn.png b/extras/emotes/public/default/capricorn.png new file mode 100755 index 0000000..f2044e7 Binary files /dev/null and b/extras/emotes/public/default/capricorn.png differ diff --git a/extras/emotes/public/default/car.png b/extras/emotes/public/default/car.png new file mode 100755 index 0000000..d70a2f0 Binary files /dev/null and b/extras/emotes/public/default/car.png differ diff --git a/extras/emotes/public/default/card_index.png b/extras/emotes/public/default/card_index.png new file mode 100755 index 0000000..374e94e Binary files /dev/null and b/extras/emotes/public/default/card_index.png differ diff --git a/extras/emotes/public/default/carousel_horse.png b/extras/emotes/public/default/carousel_horse.png new file mode 100755 index 0000000..765d2c0 Binary files /dev/null and b/extras/emotes/public/default/carousel_horse.png differ diff --git a/extras/emotes/public/default/cat.png b/extras/emotes/public/default/cat.png new file mode 100755 index 0000000..09b9ef7 Binary files /dev/null and b/extras/emotes/public/default/cat.png differ diff --git a/extras/emotes/public/default/cat2.png b/extras/emotes/public/default/cat2.png new file mode 100755 index 0000000..6dbc4c7 Binary files /dev/null and b/extras/emotes/public/default/cat2.png differ diff --git a/extras/emotes/public/default/cd.png b/extras/emotes/public/default/cd.png new file mode 100755 index 0000000..baff835 Binary files /dev/null and b/extras/emotes/public/default/cd.png differ diff --git a/extras/emotes/public/default/chart.png b/extras/emotes/public/default/chart.png new file mode 100755 index 0000000..ac2c4bb Binary files /dev/null and b/extras/emotes/public/default/chart.png differ diff --git a/extras/emotes/public/default/chart_with_downwards_trend.png b/extras/emotes/public/default/chart_with_downwards_trend.png new file mode 100755 index 0000000..cb0d2a1 Binary files /dev/null and b/extras/emotes/public/default/chart_with_downwards_trend.png differ diff --git a/extras/emotes/public/default/chart_with_upwards_trend.png b/extras/emotes/public/default/chart_with_upwards_trend.png new file mode 100755 index 0000000..7c66745 Binary files /dev/null and b/extras/emotes/public/default/chart_with_upwards_trend.png differ diff --git a/extras/emotes/public/default/checkered_flag.png b/extras/emotes/public/default/checkered_flag.png new file mode 100755 index 0000000..ead4a68 Binary files /dev/null and b/extras/emotes/public/default/checkered_flag.png differ diff --git a/extras/emotes/public/default/cherries.png b/extras/emotes/public/default/cherries.png new file mode 100755 index 0000000..8d3e044 Binary files /dev/null and b/extras/emotes/public/default/cherries.png differ diff --git a/extras/emotes/public/default/cherry_blossom.png b/extras/emotes/public/default/cherry_blossom.png new file mode 100755 index 0000000..e031554 Binary files /dev/null and b/extras/emotes/public/default/cherry_blossom.png differ diff --git a/extras/emotes/public/default/chestnut.png b/extras/emotes/public/default/chestnut.png new file mode 100755 index 0000000..066fb6b Binary files /dev/null and b/extras/emotes/public/default/chestnut.png differ diff --git a/extras/emotes/public/default/chicken.png b/extras/emotes/public/default/chicken.png new file mode 100755 index 0000000..6d25c0e Binary files /dev/null and b/extras/emotes/public/default/chicken.png differ diff --git a/extras/emotes/public/default/children_crossing.png b/extras/emotes/public/default/children_crossing.png new file mode 100755 index 0000000..b0302ae Binary files /dev/null and b/extras/emotes/public/default/children_crossing.png differ diff --git a/extras/emotes/public/default/chocolate_bar.png b/extras/emotes/public/default/chocolate_bar.png new file mode 100755 index 0000000..c7ec19d Binary files /dev/null and b/extras/emotes/public/default/chocolate_bar.png differ diff --git a/extras/emotes/public/default/christmas_tree.png b/extras/emotes/public/default/christmas_tree.png new file mode 100755 index 0000000..d813b95 Binary files /dev/null and b/extras/emotes/public/default/christmas_tree.png differ diff --git a/extras/emotes/public/default/church.png b/extras/emotes/public/default/church.png new file mode 100755 index 0000000..4c07c6b Binary files /dev/null and b/extras/emotes/public/default/church.png differ diff --git a/extras/emotes/public/default/cinema.png b/extras/emotes/public/default/cinema.png new file mode 100755 index 0000000..a990ccf Binary files /dev/null and b/extras/emotes/public/default/cinema.png differ diff --git a/extras/emotes/public/default/circus_tent.png b/extras/emotes/public/default/circus_tent.png new file mode 100755 index 0000000..4af8719 Binary files /dev/null and b/extras/emotes/public/default/circus_tent.png differ diff --git a/extras/emotes/public/default/city_sunrise.png b/extras/emotes/public/default/city_sunrise.png new file mode 100755 index 0000000..91ca2a4 Binary files /dev/null and b/extras/emotes/public/default/city_sunrise.png differ diff --git a/extras/emotes/public/default/city_sunset.png b/extras/emotes/public/default/city_sunset.png new file mode 100755 index 0000000..7cb178a Binary files /dev/null and b/extras/emotes/public/default/city_sunset.png differ diff --git a/extras/emotes/public/default/cl.png b/extras/emotes/public/default/cl.png new file mode 100755 index 0000000..15ac675 Binary files /dev/null and b/extras/emotes/public/default/cl.png differ diff --git a/extras/emotes/public/default/clap.png b/extras/emotes/public/default/clap.png new file mode 100755 index 0000000..d01c982 Binary files /dev/null and b/extras/emotes/public/default/clap.png differ diff --git a/extras/emotes/public/default/clapper.png b/extras/emotes/public/default/clapper.png new file mode 100755 index 0000000..4e1dc11 Binary files /dev/null and b/extras/emotes/public/default/clapper.png differ diff --git a/extras/emotes/public/default/clipboard.png b/extras/emotes/public/default/clipboard.png new file mode 100755 index 0000000..e2c74e6 Binary files /dev/null and b/extras/emotes/public/default/clipboard.png differ diff --git a/extras/emotes/public/default/clock1.png b/extras/emotes/public/default/clock1.png new file mode 100755 index 0000000..9174d4e Binary files /dev/null and b/extras/emotes/public/default/clock1.png differ diff --git a/extras/emotes/public/default/clock10.png b/extras/emotes/public/default/clock10.png new file mode 100755 index 0000000..39f590d Binary files /dev/null and b/extras/emotes/public/default/clock10.png differ diff --git a/extras/emotes/public/default/clock1030.png b/extras/emotes/public/default/clock1030.png new file mode 100755 index 0000000..0483b30 Binary files /dev/null and b/extras/emotes/public/default/clock1030.png differ diff --git a/extras/emotes/public/default/clock11.png b/extras/emotes/public/default/clock11.png new file mode 100755 index 0000000..ddb53fa Binary files /dev/null and b/extras/emotes/public/default/clock11.png differ diff --git a/extras/emotes/public/default/clock1130.png b/extras/emotes/public/default/clock1130.png new file mode 100755 index 0000000..415999e Binary files /dev/null and b/extras/emotes/public/default/clock1130.png differ diff --git a/extras/emotes/public/default/clock12.png b/extras/emotes/public/default/clock12.png new file mode 100755 index 0000000..87b1328 Binary files /dev/null and b/extras/emotes/public/default/clock12.png differ diff --git a/extras/emotes/public/default/clock1230.png b/extras/emotes/public/default/clock1230.png new file mode 100755 index 0000000..a652715 Binary files /dev/null and b/extras/emotes/public/default/clock1230.png differ diff --git a/extras/emotes/public/default/clock130.png b/extras/emotes/public/default/clock130.png new file mode 100755 index 0000000..90ea5b9 Binary files /dev/null and b/extras/emotes/public/default/clock130.png differ diff --git a/extras/emotes/public/default/clock2.png b/extras/emotes/public/default/clock2.png new file mode 100755 index 0000000..65b3b3a Binary files /dev/null and b/extras/emotes/public/default/clock2.png differ diff --git a/extras/emotes/public/default/clock230.png b/extras/emotes/public/default/clock230.png new file mode 100755 index 0000000..f12c691 Binary files /dev/null and b/extras/emotes/public/default/clock230.png differ diff --git a/extras/emotes/public/default/clock3.png b/extras/emotes/public/default/clock3.png new file mode 100755 index 0000000..3e44d64 Binary files /dev/null and b/extras/emotes/public/default/clock3.png differ diff --git a/extras/emotes/public/default/clock330.png b/extras/emotes/public/default/clock330.png new file mode 100755 index 0000000..1dc9628 Binary files /dev/null and b/extras/emotes/public/default/clock330.png differ diff --git a/extras/emotes/public/default/clock4.png b/extras/emotes/public/default/clock4.png new file mode 100755 index 0000000..948ed1a Binary files /dev/null and b/extras/emotes/public/default/clock4.png differ diff --git a/extras/emotes/public/default/clock430.png b/extras/emotes/public/default/clock430.png new file mode 100755 index 0000000..5d6b16a Binary files /dev/null and b/extras/emotes/public/default/clock430.png differ diff --git a/extras/emotes/public/default/clock5.png b/extras/emotes/public/default/clock5.png new file mode 100755 index 0000000..b010b4f Binary files /dev/null and b/extras/emotes/public/default/clock5.png differ diff --git a/extras/emotes/public/default/clock530.png b/extras/emotes/public/default/clock530.png new file mode 100755 index 0000000..e08d4ad Binary files /dev/null and b/extras/emotes/public/default/clock530.png differ diff --git a/extras/emotes/public/default/clock6.png b/extras/emotes/public/default/clock6.png new file mode 100755 index 0000000..76bf8cf Binary files /dev/null and b/extras/emotes/public/default/clock6.png differ diff --git a/extras/emotes/public/default/clock630.png b/extras/emotes/public/default/clock630.png new file mode 100755 index 0000000..46f0681 Binary files /dev/null and b/extras/emotes/public/default/clock630.png differ diff --git a/extras/emotes/public/default/clock7.png b/extras/emotes/public/default/clock7.png new file mode 100755 index 0000000..d48f645 Binary files /dev/null and b/extras/emotes/public/default/clock7.png differ diff --git a/extras/emotes/public/default/clock730.png b/extras/emotes/public/default/clock730.png new file mode 100755 index 0000000..f2807de Binary files /dev/null and b/extras/emotes/public/default/clock730.png differ diff --git a/extras/emotes/public/default/clock8.png b/extras/emotes/public/default/clock8.png new file mode 100755 index 0000000..74c770d Binary files /dev/null and b/extras/emotes/public/default/clock8.png differ diff --git a/extras/emotes/public/default/clock830.png b/extras/emotes/public/default/clock830.png new file mode 100755 index 0000000..f58f3da Binary files /dev/null and b/extras/emotes/public/default/clock830.png differ diff --git a/extras/emotes/public/default/clock9.png b/extras/emotes/public/default/clock9.png new file mode 100755 index 0000000..f009d14 Binary files /dev/null and b/extras/emotes/public/default/clock9.png differ diff --git a/extras/emotes/public/default/clock930.png b/extras/emotes/public/default/clock930.png new file mode 100755 index 0000000..fd35221 Binary files /dev/null and b/extras/emotes/public/default/clock930.png differ diff --git a/extras/emotes/public/default/closed_book.png b/extras/emotes/public/default/closed_book.png new file mode 100755 index 0000000..484029c Binary files /dev/null and b/extras/emotes/public/default/closed_book.png differ diff --git a/extras/emotes/public/default/closed_lock_with_key.png b/extras/emotes/public/default/closed_lock_with_key.png new file mode 100755 index 0000000..e6fdf6c Binary files /dev/null and b/extras/emotes/public/default/closed_lock_with_key.png differ diff --git a/extras/emotes/public/default/closed_umbrella.png b/extras/emotes/public/default/closed_umbrella.png new file mode 100755 index 0000000..0b719f0 Binary files /dev/null and b/extras/emotes/public/default/closed_umbrella.png differ diff --git a/extras/emotes/public/default/cloud.png b/extras/emotes/public/default/cloud.png new file mode 100755 index 0000000..b31c08c Binary files /dev/null and b/extras/emotes/public/default/cloud.png differ diff --git a/extras/emotes/public/default/clubs.png b/extras/emotes/public/default/clubs.png new file mode 100755 index 0000000..bfab536 Binary files /dev/null and b/extras/emotes/public/default/clubs.png differ diff --git a/extras/emotes/public/default/cn.png b/extras/emotes/public/default/cn.png new file mode 100755 index 0000000..b30dcc5 Binary files /dev/null and b/extras/emotes/public/default/cn.png differ diff --git a/extras/emotes/public/default/cocktail.png b/extras/emotes/public/default/cocktail.png new file mode 100755 index 0000000..28b45ea Binary files /dev/null and b/extras/emotes/public/default/cocktail.png differ diff --git a/extras/emotes/public/default/coffee.png b/extras/emotes/public/default/coffee.png new file mode 100755 index 0000000..57e1adc Binary files /dev/null and b/extras/emotes/public/default/coffee.png differ diff --git a/extras/emotes/public/default/cold_sweat.png b/extras/emotes/public/default/cold_sweat.png new file mode 100755 index 0000000..b9e39bc Binary files /dev/null and b/extras/emotes/public/default/cold_sweat.png differ diff --git a/extras/emotes/public/default/collision.png b/extras/emotes/public/default/collision.png new file mode 100755 index 0000000..9d5bd04 Binary files /dev/null and b/extras/emotes/public/default/collision.png differ diff --git a/extras/emotes/public/default/computer.png b/extras/emotes/public/default/computer.png new file mode 100755 index 0000000..d4d2687 Binary files /dev/null and b/extras/emotes/public/default/computer.png differ diff --git a/extras/emotes/public/default/confetti_ball.png b/extras/emotes/public/default/confetti_ball.png new file mode 100755 index 0000000..bd293e3 Binary files /dev/null and b/extras/emotes/public/default/confetti_ball.png differ diff --git a/extras/emotes/public/default/confounded.png b/extras/emotes/public/default/confounded.png new file mode 100755 index 0000000..762c376 Binary files /dev/null and b/extras/emotes/public/default/confounded.png differ diff --git a/extras/emotes/public/default/confused.png b/extras/emotes/public/default/confused.png new file mode 100755 index 0000000..8dc494d Binary files /dev/null and b/extras/emotes/public/default/confused.png differ diff --git a/extras/emotes/public/default/congratulations.png b/extras/emotes/public/default/congratulations.png new file mode 100755 index 0000000..85814e3 Binary files /dev/null and b/extras/emotes/public/default/congratulations.png differ diff --git a/extras/emotes/public/default/construction.png b/extras/emotes/public/default/construction.png new file mode 100755 index 0000000..523e9f1 Binary files /dev/null and b/extras/emotes/public/default/construction.png differ diff --git a/extras/emotes/public/default/construction_worker.png b/extras/emotes/public/default/construction_worker.png new file mode 100755 index 0000000..4d64860 Binary files /dev/null and b/extras/emotes/public/default/construction_worker.png differ diff --git a/extras/emotes/public/default/convenience_store.png b/extras/emotes/public/default/convenience_store.png new file mode 100755 index 0000000..671696c Binary files /dev/null and b/extras/emotes/public/default/convenience_store.png differ diff --git a/extras/emotes/public/default/cookie.png b/extras/emotes/public/default/cookie.png new file mode 100755 index 0000000..653edb2 Binary files /dev/null and b/extras/emotes/public/default/cookie.png differ diff --git a/extras/emotes/public/default/cool.png b/extras/emotes/public/default/cool.png new file mode 100755 index 0000000..937dcd7 Binary files /dev/null and b/extras/emotes/public/default/cool.png differ diff --git a/extras/emotes/public/default/cop.png b/extras/emotes/public/default/cop.png new file mode 100755 index 0000000..43a5a84 Binary files /dev/null and b/extras/emotes/public/default/cop.png differ diff --git a/extras/emotes/public/default/copyright.png b/extras/emotes/public/default/copyright.png new file mode 100755 index 0000000..38493c3 Binary files /dev/null and b/extras/emotes/public/default/copyright.png differ diff --git a/extras/emotes/public/default/corn.png b/extras/emotes/public/default/corn.png new file mode 100755 index 0000000..fe5d8b1 Binary files /dev/null and b/extras/emotes/public/default/corn.png differ diff --git a/extras/emotes/public/default/couple.png b/extras/emotes/public/default/couple.png new file mode 100755 index 0000000..9e51f40 Binary files /dev/null and b/extras/emotes/public/default/couple.png differ diff --git a/extras/emotes/public/default/couple_with_heart.png b/extras/emotes/public/default/couple_with_heart.png new file mode 100755 index 0000000..c503f40 Binary files /dev/null and b/extras/emotes/public/default/couple_with_heart.png differ diff --git a/extras/emotes/public/default/couplekiss.png b/extras/emotes/public/default/couplekiss.png new file mode 100755 index 0000000..d027908 Binary files /dev/null and b/extras/emotes/public/default/couplekiss.png differ diff --git a/extras/emotes/public/default/cow.png b/extras/emotes/public/default/cow.png new file mode 100755 index 0000000..12e1ab6 Binary files /dev/null and b/extras/emotes/public/default/cow.png differ diff --git a/extras/emotes/public/default/cow2.png b/extras/emotes/public/default/cow2.png new file mode 100755 index 0000000..594c921 Binary files /dev/null and b/extras/emotes/public/default/cow2.png differ diff --git a/extras/emotes/public/default/credit_card.png b/extras/emotes/public/default/credit_card.png new file mode 100755 index 0000000..be1c1dd Binary files /dev/null and b/extras/emotes/public/default/credit_card.png differ diff --git a/extras/emotes/public/default/crocodile.png b/extras/emotes/public/default/crocodile.png new file mode 100755 index 0000000..7435d5a Binary files /dev/null and b/extras/emotes/public/default/crocodile.png differ diff --git a/extras/emotes/public/default/crossed_flags.png b/extras/emotes/public/default/crossed_flags.png new file mode 100755 index 0000000..2397bcd Binary files /dev/null and b/extras/emotes/public/default/crossed_flags.png differ diff --git a/extras/emotes/public/default/crown.png b/extras/emotes/public/default/crown.png new file mode 100755 index 0000000..39da1d5 Binary files /dev/null and b/extras/emotes/public/default/crown.png differ diff --git a/extras/emotes/public/default/cry.png b/extras/emotes/public/default/cry.png new file mode 100755 index 0000000..6d0d9af Binary files /dev/null and b/extras/emotes/public/default/cry.png differ diff --git a/extras/emotes/public/default/crying_cat_face.png b/extras/emotes/public/default/crying_cat_face.png new file mode 100755 index 0000000..42d4c27 Binary files /dev/null and b/extras/emotes/public/default/crying_cat_face.png differ diff --git a/extras/emotes/public/default/crystal_ball.png b/extras/emotes/public/default/crystal_ball.png new file mode 100755 index 0000000..6d2c6c4 Binary files /dev/null and b/extras/emotes/public/default/crystal_ball.png differ diff --git a/extras/emotes/public/default/cupid.png b/extras/emotes/public/default/cupid.png new file mode 100755 index 0000000..4987284 Binary files /dev/null and b/extras/emotes/public/default/cupid.png differ diff --git a/extras/emotes/public/default/curly_loop.png b/extras/emotes/public/default/curly_loop.png new file mode 100755 index 0000000..7dd841d Binary files /dev/null and b/extras/emotes/public/default/curly_loop.png differ diff --git a/extras/emotes/public/default/currency_exchange.png b/extras/emotes/public/default/currency_exchange.png new file mode 100755 index 0000000..6ebebe7 Binary files /dev/null and b/extras/emotes/public/default/currency_exchange.png differ diff --git a/extras/emotes/public/default/curry.png b/extras/emotes/public/default/curry.png new file mode 100755 index 0000000..7983c70 Binary files /dev/null and b/extras/emotes/public/default/curry.png differ diff --git a/extras/emotes/public/default/custard.png b/extras/emotes/public/default/custard.png new file mode 100755 index 0000000..9f843b4 Binary files /dev/null and b/extras/emotes/public/default/custard.png differ diff --git a/extras/emotes/public/default/customs.png b/extras/emotes/public/default/customs.png new file mode 100755 index 0000000..92691e3 Binary files /dev/null and b/extras/emotes/public/default/customs.png differ diff --git a/extras/emotes/public/default/cyclone.png b/extras/emotes/public/default/cyclone.png new file mode 100755 index 0000000..5fd2e45 Binary files /dev/null and b/extras/emotes/public/default/cyclone.png differ diff --git a/extras/emotes/public/default/dancer.png b/extras/emotes/public/default/dancer.png new file mode 100755 index 0000000..7a7bf59 Binary files /dev/null and b/extras/emotes/public/default/dancer.png differ diff --git a/extras/emotes/public/default/dancers.png b/extras/emotes/public/default/dancers.png new file mode 100755 index 0000000..2dfb451 Binary files /dev/null and b/extras/emotes/public/default/dancers.png differ diff --git a/extras/emotes/public/default/dango.png b/extras/emotes/public/default/dango.png new file mode 100755 index 0000000..2d042ae Binary files /dev/null and b/extras/emotes/public/default/dango.png differ diff --git a/extras/emotes/public/default/dart.png b/extras/emotes/public/default/dart.png new file mode 100755 index 0000000..5f16864 Binary files /dev/null and b/extras/emotes/public/default/dart.png differ diff --git a/extras/emotes/public/default/dash.png b/extras/emotes/public/default/dash.png new file mode 100755 index 0000000..dc2c0a8 Binary files /dev/null and b/extras/emotes/public/default/dash.png differ diff --git a/extras/emotes/public/default/date.png b/extras/emotes/public/default/date.png new file mode 100755 index 0000000..6ad2efa Binary files /dev/null and b/extras/emotes/public/default/date.png differ diff --git a/extras/emotes/public/default/de.png b/extras/emotes/public/default/de.png new file mode 100755 index 0000000..16a2854 Binary files /dev/null and b/extras/emotes/public/default/de.png differ diff --git a/extras/emotes/public/default/deciduous_tree.png b/extras/emotes/public/default/deciduous_tree.png new file mode 100755 index 0000000..3fdf8c0 Binary files /dev/null and b/extras/emotes/public/default/deciduous_tree.png differ diff --git a/extras/emotes/public/default/department_store.png b/extras/emotes/public/default/department_store.png new file mode 100755 index 0000000..68d959c Binary files /dev/null and b/extras/emotes/public/default/department_store.png differ diff --git a/extras/emotes/public/default/diamond_shape_with_a_dot_inside.png b/extras/emotes/public/default/diamond_shape_with_a_dot_inside.png new file mode 100755 index 0000000..dfd1098 Binary files /dev/null and b/extras/emotes/public/default/diamond_shape_with_a_dot_inside.png differ diff --git a/extras/emotes/public/default/diamonds.png b/extras/emotes/public/default/diamonds.png new file mode 100755 index 0000000..fe08277 Binary files /dev/null and b/extras/emotes/public/default/diamonds.png differ diff --git a/extras/emotes/public/default/disappointed.png b/extras/emotes/public/default/disappointed.png new file mode 100755 index 0000000..8255200 Binary files /dev/null and b/extras/emotes/public/default/disappointed.png differ diff --git a/extras/emotes/public/default/disappointed_relieved.png b/extras/emotes/public/default/disappointed_relieved.png new file mode 100755 index 0000000..fa5f9e7 Binary files /dev/null and b/extras/emotes/public/default/disappointed_relieved.png differ diff --git a/extras/emotes/public/default/dizzy.png b/extras/emotes/public/default/dizzy.png new file mode 100755 index 0000000..3702b61 Binary files /dev/null and b/extras/emotes/public/default/dizzy.png differ diff --git a/extras/emotes/public/default/dizzy_face.png b/extras/emotes/public/default/dizzy_face.png new file mode 100755 index 0000000..8001d6f Binary files /dev/null and b/extras/emotes/public/default/dizzy_face.png differ diff --git a/extras/emotes/public/default/do_not_litter.png b/extras/emotes/public/default/do_not_litter.png new file mode 100755 index 0000000..38c7ae7 Binary files /dev/null and b/extras/emotes/public/default/do_not_litter.png differ diff --git a/extras/emotes/public/default/dog.png b/extras/emotes/public/default/dog.png new file mode 100755 index 0000000..389a02b Binary files /dev/null and b/extras/emotes/public/default/dog.png differ diff --git a/extras/emotes/public/default/dog2.png b/extras/emotes/public/default/dog2.png new file mode 100755 index 0000000..c7f6a24 Binary files /dev/null and b/extras/emotes/public/default/dog2.png differ diff --git a/extras/emotes/public/default/dollar.png b/extras/emotes/public/default/dollar.png new file mode 100755 index 0000000..63de884 Binary files /dev/null and b/extras/emotes/public/default/dollar.png differ diff --git a/extras/emotes/public/default/dolls.png b/extras/emotes/public/default/dolls.png new file mode 100755 index 0000000..47ce339 Binary files /dev/null and b/extras/emotes/public/default/dolls.png differ diff --git a/extras/emotes/public/default/dolphin.png b/extras/emotes/public/default/dolphin.png new file mode 100755 index 0000000..9326077 Binary files /dev/null and b/extras/emotes/public/default/dolphin.png differ diff --git a/extras/emotes/public/default/donut.png b/extras/emotes/public/default/donut.png new file mode 100755 index 0000000..ccf8691 Binary files /dev/null and b/extras/emotes/public/default/donut.png differ diff --git a/extras/emotes/public/default/door.png b/extras/emotes/public/default/door.png new file mode 100755 index 0000000..83c819a Binary files /dev/null and b/extras/emotes/public/default/door.png differ diff --git a/extras/emotes/public/default/doughnut.png b/extras/emotes/public/default/doughnut.png new file mode 100755 index 0000000..ccf8691 Binary files /dev/null and b/extras/emotes/public/default/doughnut.png differ diff --git a/extras/emotes/public/default/dragon.png b/extras/emotes/public/default/dragon.png new file mode 100755 index 0000000..88d4784 Binary files /dev/null and b/extras/emotes/public/default/dragon.png differ diff --git a/extras/emotes/public/default/dragon_face.png b/extras/emotes/public/default/dragon_face.png new file mode 100755 index 0000000..e5e556b Binary files /dev/null and b/extras/emotes/public/default/dragon_face.png differ diff --git a/extras/emotes/public/default/dress.png b/extras/emotes/public/default/dress.png new file mode 100755 index 0000000..6434e2e Binary files /dev/null and b/extras/emotes/public/default/dress.png differ diff --git a/extras/emotes/public/default/dromedary_camel.png b/extras/emotes/public/default/dromedary_camel.png new file mode 100755 index 0000000..c8c7b9f Binary files /dev/null and b/extras/emotes/public/default/dromedary_camel.png differ diff --git a/extras/emotes/public/default/droplet.png b/extras/emotes/public/default/droplet.png new file mode 100755 index 0000000..cae7f49 Binary files /dev/null and b/extras/emotes/public/default/droplet.png differ diff --git a/extras/emotes/public/default/dvd.png b/extras/emotes/public/default/dvd.png new file mode 100755 index 0000000..363c83d Binary files /dev/null and b/extras/emotes/public/default/dvd.png differ diff --git a/extras/emotes/public/default/e-mail.png b/extras/emotes/public/default/e-mail.png new file mode 100755 index 0000000..176a8e1 Binary files /dev/null and b/extras/emotes/public/default/e-mail.png differ diff --git a/extras/emotes/public/default/ear.png b/extras/emotes/public/default/ear.png new file mode 100755 index 0000000..2bbbf10 Binary files /dev/null and b/extras/emotes/public/default/ear.png differ diff --git a/extras/emotes/public/default/ear_of_rice.png b/extras/emotes/public/default/ear_of_rice.png new file mode 100755 index 0000000..a9bba5c Binary files /dev/null and b/extras/emotes/public/default/ear_of_rice.png differ diff --git a/extras/emotes/public/default/earth_africa.png b/extras/emotes/public/default/earth_africa.png new file mode 100755 index 0000000..44ce5ec Binary files /dev/null and b/extras/emotes/public/default/earth_africa.png differ diff --git a/extras/emotes/public/default/earth_americas.png b/extras/emotes/public/default/earth_americas.png new file mode 100755 index 0000000..97d7176 Binary files /dev/null and b/extras/emotes/public/default/earth_americas.png differ diff --git a/extras/emotes/public/default/earth_asia.png b/extras/emotes/public/default/earth_asia.png new file mode 100755 index 0000000..95ec357 Binary files /dev/null and b/extras/emotes/public/default/earth_asia.png differ diff --git a/extras/emotes/public/default/egg.png b/extras/emotes/public/default/egg.png new file mode 100755 index 0000000..c3de6ae Binary files /dev/null and b/extras/emotes/public/default/egg.png differ diff --git a/extras/emotes/public/default/eggplant.png b/extras/emotes/public/default/eggplant.png new file mode 100755 index 0000000..66f25fc Binary files /dev/null and b/extras/emotes/public/default/eggplant.png differ diff --git a/extras/emotes/public/default/eight.png b/extras/emotes/public/default/eight.png new file mode 100755 index 0000000..7bdb422 Binary files /dev/null and b/extras/emotes/public/default/eight.png differ diff --git a/extras/emotes/public/default/eight_pointed_black_star.png b/extras/emotes/public/default/eight_pointed_black_star.png new file mode 100755 index 0000000..2420a77 Binary files /dev/null and b/extras/emotes/public/default/eight_pointed_black_star.png differ diff --git a/extras/emotes/public/default/eight_spoked_asterisk.png b/extras/emotes/public/default/eight_spoked_asterisk.png new file mode 100755 index 0000000..946a203 Binary files /dev/null and b/extras/emotes/public/default/eight_spoked_asterisk.png differ diff --git a/extras/emotes/public/default/electric_plug.png b/extras/emotes/public/default/electric_plug.png new file mode 100755 index 0000000..2837bab Binary files /dev/null and b/extras/emotes/public/default/electric_plug.png differ diff --git a/extras/emotes/public/default/elephant.png b/extras/emotes/public/default/elephant.png new file mode 100755 index 0000000..5ca0457 Binary files /dev/null and b/extras/emotes/public/default/elephant.png differ diff --git a/extras/emotes/public/default/email.png b/extras/emotes/public/default/email.png new file mode 100755 index 0000000..0e01fd5 Binary files /dev/null and b/extras/emotes/public/default/email.png differ diff --git a/extras/emotes/public/default/end.png b/extras/emotes/public/default/end.png new file mode 100755 index 0000000..61a4399 Binary files /dev/null and b/extras/emotes/public/default/end.png differ diff --git a/extras/emotes/public/default/envelope.png b/extras/emotes/public/default/envelope.png new file mode 100755 index 0000000..3631861 Binary files /dev/null and b/extras/emotes/public/default/envelope.png differ diff --git a/extras/emotes/public/default/es.png b/extras/emotes/public/default/es.png new file mode 100755 index 0000000..71b30bf Binary files /dev/null and b/extras/emotes/public/default/es.png differ diff --git a/extras/emotes/public/default/euro.png b/extras/emotes/public/default/euro.png new file mode 100755 index 0000000..1c5904b Binary files /dev/null and b/extras/emotes/public/default/euro.png differ diff --git a/extras/emotes/public/default/european_castle.png b/extras/emotes/public/default/european_castle.png new file mode 100755 index 0000000..8229b8a Binary files /dev/null and b/extras/emotes/public/default/european_castle.png differ diff --git a/extras/emotes/public/default/european_post_office.png b/extras/emotes/public/default/european_post_office.png new file mode 100755 index 0000000..0f65b14 Binary files /dev/null and b/extras/emotes/public/default/european_post_office.png differ diff --git a/extras/emotes/public/default/evergreen_tree.png b/extras/emotes/public/default/evergreen_tree.png new file mode 100755 index 0000000..ae8ad10 Binary files /dev/null and b/extras/emotes/public/default/evergreen_tree.png differ diff --git a/extras/emotes/public/default/exclamation.png b/extras/emotes/public/default/exclamation.png new file mode 100755 index 0000000..77bbdea Binary files /dev/null and b/extras/emotes/public/default/exclamation.png differ diff --git a/extras/emotes/public/default/expressionless.png b/extras/emotes/public/default/expressionless.png new file mode 100755 index 0000000..913ff4e Binary files /dev/null and b/extras/emotes/public/default/expressionless.png differ diff --git a/extras/emotes/public/default/eyeglasses.png b/extras/emotes/public/default/eyeglasses.png new file mode 100755 index 0000000..a3cf75a Binary files /dev/null and b/extras/emotes/public/default/eyeglasses.png differ diff --git a/extras/emotes/public/default/eyes.png b/extras/emotes/public/default/eyes.png new file mode 100755 index 0000000..1ac24a6 Binary files /dev/null and b/extras/emotes/public/default/eyes.png differ diff --git a/extras/emotes/public/default/facepunch.png b/extras/emotes/public/default/facepunch.png new file mode 100755 index 0000000..277047b Binary files /dev/null and b/extras/emotes/public/default/facepunch.png differ diff --git a/extras/emotes/public/default/factory.png b/extras/emotes/public/default/factory.png new file mode 100755 index 0000000..6404634 Binary files /dev/null and b/extras/emotes/public/default/factory.png differ diff --git a/extras/emotes/public/default/fallen_leaf.png b/extras/emotes/public/default/fallen_leaf.png new file mode 100755 index 0000000..d49f9c1 Binary files /dev/null and b/extras/emotes/public/default/fallen_leaf.png differ diff --git a/extras/emotes/public/default/family.png b/extras/emotes/public/default/family.png new file mode 100755 index 0000000..b4b365f Binary files /dev/null and b/extras/emotes/public/default/family.png differ diff --git a/extras/emotes/public/default/fast_forward.png b/extras/emotes/public/default/fast_forward.png new file mode 100755 index 0000000..8830e14 Binary files /dev/null and b/extras/emotes/public/default/fast_forward.png differ diff --git a/extras/emotes/public/default/fax.png b/extras/emotes/public/default/fax.png new file mode 100755 index 0000000..62be2c9 Binary files /dev/null and b/extras/emotes/public/default/fax.png differ diff --git a/extras/emotes/public/default/fearful.png b/extras/emotes/public/default/fearful.png new file mode 100755 index 0000000..513fce4 Binary files /dev/null and b/extras/emotes/public/default/fearful.png differ diff --git a/extras/emotes/public/default/feelsgood.png b/extras/emotes/public/default/feelsgood.png new file mode 100755 index 0000000..361f969 Binary files /dev/null and b/extras/emotes/public/default/feelsgood.png differ diff --git a/extras/emotes/public/default/feet.png b/extras/emotes/public/default/feet.png new file mode 100755 index 0000000..1b0147b Binary files /dev/null and b/extras/emotes/public/default/feet.png differ diff --git a/extras/emotes/public/default/ferris_wheel.png b/extras/emotes/public/default/ferris_wheel.png new file mode 100755 index 0000000..54a1dcf Binary files /dev/null and b/extras/emotes/public/default/ferris_wheel.png differ diff --git a/extras/emotes/public/default/file_folder.png b/extras/emotes/public/default/file_folder.png new file mode 100755 index 0000000..4d8bebf Binary files /dev/null and b/extras/emotes/public/default/file_folder.png differ diff --git a/extras/emotes/public/default/finnadie.png b/extras/emotes/public/default/finnadie.png new file mode 100755 index 0000000..bfc5a0d Binary files /dev/null and b/extras/emotes/public/default/finnadie.png differ diff --git a/extras/emotes/public/default/fire.png b/extras/emotes/public/default/fire.png new file mode 100755 index 0000000..f2a3149 Binary files /dev/null and b/extras/emotes/public/default/fire.png differ diff --git a/extras/emotes/public/default/fire_engine.png b/extras/emotes/public/default/fire_engine.png new file mode 100755 index 0000000..9e6c59c Binary files /dev/null and b/extras/emotes/public/default/fire_engine.png differ diff --git a/extras/emotes/public/default/fireworks.png b/extras/emotes/public/default/fireworks.png new file mode 100755 index 0000000..b4eccd5 Binary files /dev/null and b/extras/emotes/public/default/fireworks.png differ diff --git a/extras/emotes/public/default/first_quarter_moon.png b/extras/emotes/public/default/first_quarter_moon.png new file mode 100755 index 0000000..f38c236 Binary files /dev/null and b/extras/emotes/public/default/first_quarter_moon.png differ diff --git a/extras/emotes/public/default/first_quarter_moon_with_face.png b/extras/emotes/public/default/first_quarter_moon_with_face.png new file mode 100755 index 0000000..85ae2ce Binary files /dev/null and b/extras/emotes/public/default/first_quarter_moon_with_face.png differ diff --git a/extras/emotes/public/default/fish.png b/extras/emotes/public/default/fish.png new file mode 100755 index 0000000..90bdda2 Binary files /dev/null and b/extras/emotes/public/default/fish.png differ diff --git a/extras/emotes/public/default/fish_cake.png b/extras/emotes/public/default/fish_cake.png new file mode 100755 index 0000000..a8f2261 Binary files /dev/null and b/extras/emotes/public/default/fish_cake.png differ diff --git a/extras/emotes/public/default/fishing_pole_and_fish.png b/extras/emotes/public/default/fishing_pole_and_fish.png new file mode 100755 index 0000000..d84609c Binary files /dev/null and b/extras/emotes/public/default/fishing_pole_and_fish.png differ diff --git a/extras/emotes/public/default/fist.png b/extras/emotes/public/default/fist.png new file mode 100755 index 0000000..ecc8874 Binary files /dev/null and b/extras/emotes/public/default/fist.png differ diff --git a/extras/emotes/public/default/five.png b/extras/emotes/public/default/five.png new file mode 100755 index 0000000..794321a Binary files /dev/null and b/extras/emotes/public/default/five.png differ diff --git a/extras/emotes/public/default/flags.png b/extras/emotes/public/default/flags.png new file mode 100755 index 0000000..540164e Binary files /dev/null and b/extras/emotes/public/default/flags.png differ diff --git a/extras/emotes/public/default/flashlight.png b/extras/emotes/public/default/flashlight.png new file mode 100755 index 0000000..215940a Binary files /dev/null and b/extras/emotes/public/default/flashlight.png differ diff --git a/extras/emotes/public/default/floppy_disk.png b/extras/emotes/public/default/floppy_disk.png new file mode 100755 index 0000000..4ad5631 Binary files /dev/null and b/extras/emotes/public/default/floppy_disk.png differ diff --git a/extras/emotes/public/default/flower_playing_cards.png b/extras/emotes/public/default/flower_playing_cards.png new file mode 100755 index 0000000..cc46a6a Binary files /dev/null and b/extras/emotes/public/default/flower_playing_cards.png differ diff --git a/extras/emotes/public/default/flushed.png b/extras/emotes/public/default/flushed.png new file mode 100755 index 0000000..74b78c9 Binary files /dev/null and b/extras/emotes/public/default/flushed.png differ diff --git a/extras/emotes/public/default/foggy.png b/extras/emotes/public/default/foggy.png new file mode 100755 index 0000000..3c7b8b0 Binary files /dev/null and b/extras/emotes/public/default/foggy.png differ diff --git a/extras/emotes/public/default/football.png b/extras/emotes/public/default/football.png new file mode 100755 index 0000000..0e4e168 Binary files /dev/null and b/extras/emotes/public/default/football.png differ diff --git a/extras/emotes/public/default/fork_and_knife.png b/extras/emotes/public/default/fork_and_knife.png new file mode 100755 index 0000000..8ba4bc6 Binary files /dev/null and b/extras/emotes/public/default/fork_and_knife.png differ diff --git a/extras/emotes/public/default/fountain.png b/extras/emotes/public/default/fountain.png new file mode 100755 index 0000000..da126e6 Binary files /dev/null and b/extras/emotes/public/default/fountain.png differ diff --git a/extras/emotes/public/default/four.png b/extras/emotes/public/default/four.png new file mode 100755 index 0000000..14782ba Binary files /dev/null and b/extras/emotes/public/default/four.png differ diff --git a/extras/emotes/public/default/four_leaf_clover.png b/extras/emotes/public/default/four_leaf_clover.png new file mode 100755 index 0000000..f2014be Binary files /dev/null and b/extras/emotes/public/default/four_leaf_clover.png differ diff --git a/extras/emotes/public/default/fr.png b/extras/emotes/public/default/fr.png new file mode 100755 index 0000000..6311c91 Binary files /dev/null and b/extras/emotes/public/default/fr.png differ diff --git a/extras/emotes/public/default/free.png b/extras/emotes/public/default/free.png new file mode 100755 index 0000000..c886cf2 Binary files /dev/null and b/extras/emotes/public/default/free.png differ diff --git a/extras/emotes/public/default/fried_shrimp.png b/extras/emotes/public/default/fried_shrimp.png new file mode 100755 index 0000000..c8c284b Binary files /dev/null and b/extras/emotes/public/default/fried_shrimp.png differ diff --git a/extras/emotes/public/default/fries.png b/extras/emotes/public/default/fries.png new file mode 100755 index 0000000..cfef669 Binary files /dev/null and b/extras/emotes/public/default/fries.png differ diff --git a/extras/emotes/public/default/frog.png b/extras/emotes/public/default/frog.png new file mode 100755 index 0000000..cfe11b1 Binary files /dev/null and b/extras/emotes/public/default/frog.png differ diff --git a/extras/emotes/public/default/frowning.png b/extras/emotes/public/default/frowning.png new file mode 100755 index 0000000..487b770 Binary files /dev/null and b/extras/emotes/public/default/frowning.png differ diff --git a/extras/emotes/public/default/fu.png b/extras/emotes/public/default/fu.png new file mode 100755 index 0000000..61a3fee Binary files /dev/null and b/extras/emotes/public/default/fu.png differ diff --git a/extras/emotes/public/default/fuelpump.png b/extras/emotes/public/default/fuelpump.png new file mode 100755 index 0000000..54c29ae Binary files /dev/null and b/extras/emotes/public/default/fuelpump.png differ diff --git a/extras/emotes/public/default/full_moon.png b/extras/emotes/public/default/full_moon.png new file mode 100755 index 0000000..8ff657a Binary files /dev/null and b/extras/emotes/public/default/full_moon.png differ diff --git a/extras/emotes/public/default/full_moon_with_face.png b/extras/emotes/public/default/full_moon_with_face.png new file mode 100755 index 0000000..d42b3f0 Binary files /dev/null and b/extras/emotes/public/default/full_moon_with_face.png differ diff --git a/extras/emotes/public/default/game_die.png b/extras/emotes/public/default/game_die.png new file mode 100755 index 0000000..cff2bd8 Binary files /dev/null and b/extras/emotes/public/default/game_die.png differ diff --git a/extras/emotes/public/default/gb.png b/extras/emotes/public/default/gb.png new file mode 100755 index 0000000..2a62c7a Binary files /dev/null and b/extras/emotes/public/default/gb.png differ diff --git a/extras/emotes/public/default/gem.png b/extras/emotes/public/default/gem.png new file mode 100755 index 0000000..8a5d8da Binary files /dev/null and b/extras/emotes/public/default/gem.png differ diff --git a/extras/emotes/public/default/gemini.png b/extras/emotes/public/default/gemini.png new file mode 100755 index 0000000..d926f6e Binary files /dev/null and b/extras/emotes/public/default/gemini.png differ diff --git a/extras/emotes/public/default/ghost.png b/extras/emotes/public/default/ghost.png new file mode 100755 index 0000000..671dd0c Binary files /dev/null and b/extras/emotes/public/default/ghost.png differ diff --git a/extras/emotes/public/default/gift.png b/extras/emotes/public/default/gift.png new file mode 100755 index 0000000..552cfdc Binary files /dev/null and b/extras/emotes/public/default/gift.png differ diff --git a/extras/emotes/public/default/gift_heart.png b/extras/emotes/public/default/gift_heart.png new file mode 100755 index 0000000..f31c26a Binary files /dev/null and b/extras/emotes/public/default/gift_heart.png differ diff --git a/extras/emotes/public/default/girl.png b/extras/emotes/public/default/girl.png new file mode 100755 index 0000000..ea41269 Binary files /dev/null and b/extras/emotes/public/default/girl.png differ diff --git a/extras/emotes/public/default/globe_with_meridians.png b/extras/emotes/public/default/globe_with_meridians.png new file mode 100755 index 0000000..b198646 Binary files /dev/null and b/extras/emotes/public/default/globe_with_meridians.png differ diff --git a/extras/emotes/public/default/goat.png b/extras/emotes/public/default/goat.png new file mode 100755 index 0000000..4be9cf3 Binary files /dev/null and b/extras/emotes/public/default/goat.png differ diff --git a/extras/emotes/public/default/goberserk.png b/extras/emotes/public/default/goberserk.png new file mode 100755 index 0000000..59a742a Binary files /dev/null and b/extras/emotes/public/default/goberserk.png differ diff --git a/extras/emotes/public/default/godmode.png b/extras/emotes/public/default/godmode.png new file mode 100755 index 0000000..7e75ab2 Binary files /dev/null and b/extras/emotes/public/default/godmode.png differ diff --git a/extras/emotes/public/default/golf.png b/extras/emotes/public/default/golf.png new file mode 100755 index 0000000..cba2116 Binary files /dev/null and b/extras/emotes/public/default/golf.png differ diff --git a/extras/emotes/public/default/grapes.png b/extras/emotes/public/default/grapes.png new file mode 100755 index 0000000..0f9f007 Binary files /dev/null and b/extras/emotes/public/default/grapes.png differ diff --git a/extras/emotes/public/default/green_apple.png b/extras/emotes/public/default/green_apple.png new file mode 100755 index 0000000..337205c Binary files /dev/null and b/extras/emotes/public/default/green_apple.png differ diff --git a/extras/emotes/public/default/green_book.png b/extras/emotes/public/default/green_book.png new file mode 100755 index 0000000..e86651e Binary files /dev/null and b/extras/emotes/public/default/green_book.png differ diff --git a/extras/emotes/public/default/green_heart.png b/extras/emotes/public/default/green_heart.png new file mode 100755 index 0000000..7289cb8 Binary files /dev/null and b/extras/emotes/public/default/green_heart.png differ diff --git a/extras/emotes/public/default/grey_exclamation.png b/extras/emotes/public/default/grey_exclamation.png new file mode 100755 index 0000000..cf027dd Binary files /dev/null and b/extras/emotes/public/default/grey_exclamation.png differ diff --git a/extras/emotes/public/default/grey_question.png b/extras/emotes/public/default/grey_question.png new file mode 100755 index 0000000..fb97ba7 Binary files /dev/null and b/extras/emotes/public/default/grey_question.png differ diff --git a/extras/emotes/public/default/grimacing.png b/extras/emotes/public/default/grimacing.png new file mode 100755 index 0000000..1219ba7 Binary files /dev/null and b/extras/emotes/public/default/grimacing.png differ diff --git a/extras/emotes/public/default/grin.png b/extras/emotes/public/default/grin.png new file mode 100755 index 0000000..591cfce Binary files /dev/null and b/extras/emotes/public/default/grin.png differ diff --git a/extras/emotes/public/default/grinning.png b/extras/emotes/public/default/grinning.png new file mode 100755 index 0000000..7e812b7 Binary files /dev/null and b/extras/emotes/public/default/grinning.png differ diff --git a/extras/emotes/public/default/guardsman.png b/extras/emotes/public/default/guardsman.png new file mode 100755 index 0000000..b67b335 Binary files /dev/null and b/extras/emotes/public/default/guardsman.png differ diff --git a/extras/emotes/public/default/guitar.png b/extras/emotes/public/default/guitar.png new file mode 100755 index 0000000..2b7fa43 Binary files /dev/null and b/extras/emotes/public/default/guitar.png differ diff --git a/extras/emotes/public/default/gun.png b/extras/emotes/public/default/gun.png new file mode 100755 index 0000000..c49dc52 Binary files /dev/null and b/extras/emotes/public/default/gun.png differ diff --git a/extras/emotes/public/default/haircut.png b/extras/emotes/public/default/haircut.png new file mode 100755 index 0000000..902d273 Binary files /dev/null and b/extras/emotes/public/default/haircut.png differ diff --git a/extras/emotes/public/default/hamburger.png b/extras/emotes/public/default/hamburger.png new file mode 100755 index 0000000..9f1a3fd Binary files /dev/null and b/extras/emotes/public/default/hamburger.png differ diff --git a/extras/emotes/public/default/hammer.png b/extras/emotes/public/default/hammer.png new file mode 100755 index 0000000..482b1c7 Binary files /dev/null and b/extras/emotes/public/default/hammer.png differ diff --git a/extras/emotes/public/default/hamster.png b/extras/emotes/public/default/hamster.png new file mode 100755 index 0000000..addfd2e Binary files /dev/null and b/extras/emotes/public/default/hamster.png differ diff --git a/extras/emotes/public/default/hand.png b/extras/emotes/public/default/hand.png new file mode 100755 index 0000000..5e45c25 Binary files /dev/null and b/extras/emotes/public/default/hand.png differ diff --git a/extras/emotes/public/default/handbag.png b/extras/emotes/public/default/handbag.png new file mode 100755 index 0000000..d7adf04 Binary files /dev/null and b/extras/emotes/public/default/handbag.png differ diff --git a/extras/emotes/public/default/hankey.png b/extras/emotes/public/default/hankey.png new file mode 100755 index 0000000..73a4dc8 Binary files /dev/null and b/extras/emotes/public/default/hankey.png differ diff --git a/extras/emotes/public/default/hash.png b/extras/emotes/public/default/hash.png new file mode 100755 index 0000000..6765d7d Binary files /dev/null and b/extras/emotes/public/default/hash.png differ diff --git a/extras/emotes/public/default/hatched_chick.png b/extras/emotes/public/default/hatched_chick.png new file mode 100755 index 0000000..39c25bc Binary files /dev/null and b/extras/emotes/public/default/hatched_chick.png differ diff --git a/extras/emotes/public/default/hatching_chick.png b/extras/emotes/public/default/hatching_chick.png new file mode 100755 index 0000000..005a555 Binary files /dev/null and b/extras/emotes/public/default/hatching_chick.png differ diff --git a/extras/emotes/public/default/headphones.png b/extras/emotes/public/default/headphones.png new file mode 100755 index 0000000..ad83000 Binary files /dev/null and b/extras/emotes/public/default/headphones.png differ diff --git a/extras/emotes/public/default/hear_no_evil.png b/extras/emotes/public/default/hear_no_evil.png new file mode 100755 index 0000000..f97a1f9 Binary files /dev/null and b/extras/emotes/public/default/hear_no_evil.png differ diff --git a/extras/emotes/public/default/heart.png b/extras/emotes/public/default/heart.png new file mode 100755 index 0000000..7d7790c Binary files /dev/null and b/extras/emotes/public/default/heart.png differ diff --git a/extras/emotes/public/default/heart_decoration.png b/extras/emotes/public/default/heart_decoration.png new file mode 100755 index 0000000..b8be44d Binary files /dev/null and b/extras/emotes/public/default/heart_decoration.png differ diff --git a/extras/emotes/public/default/heart_eyes.png b/extras/emotes/public/default/heart_eyes.png new file mode 100755 index 0000000..0e57942 Binary files /dev/null and b/extras/emotes/public/default/heart_eyes.png differ diff --git a/extras/emotes/public/default/heart_eyes_cat.png b/extras/emotes/public/default/heart_eyes_cat.png new file mode 100755 index 0000000..eeba240 Binary files /dev/null and b/extras/emotes/public/default/heart_eyes_cat.png differ diff --git a/extras/emotes/public/default/heartbeat.png b/extras/emotes/public/default/heartbeat.png new file mode 100755 index 0000000..b6628f6 Binary files /dev/null and b/extras/emotes/public/default/heartbeat.png differ diff --git a/extras/emotes/public/default/heartpulse.png b/extras/emotes/public/default/heartpulse.png new file mode 100755 index 0000000..a7491cb Binary files /dev/null and b/extras/emotes/public/default/heartpulse.png differ diff --git a/extras/emotes/public/default/hearts.png b/extras/emotes/public/default/hearts.png new file mode 100755 index 0000000..e894715 Binary files /dev/null and b/extras/emotes/public/default/hearts.png differ diff --git a/extras/emotes/public/default/heavy_check_mark.png b/extras/emotes/public/default/heavy_check_mark.png new file mode 100755 index 0000000..d0f010b Binary files /dev/null and b/extras/emotes/public/default/heavy_check_mark.png differ diff --git a/extras/emotes/public/default/heavy_division_sign.png b/extras/emotes/public/default/heavy_division_sign.png new file mode 100755 index 0000000..e193fd2 Binary files /dev/null and b/extras/emotes/public/default/heavy_division_sign.png differ diff --git a/extras/emotes/public/default/heavy_dollar_sign.png b/extras/emotes/public/default/heavy_dollar_sign.png new file mode 100755 index 0000000..5eddfc5 Binary files /dev/null and b/extras/emotes/public/default/heavy_dollar_sign.png differ diff --git a/extras/emotes/public/default/heavy_exclamation_mark.png b/extras/emotes/public/default/heavy_exclamation_mark.png new file mode 100755 index 0000000..4c560f5 Binary files /dev/null and b/extras/emotes/public/default/heavy_exclamation_mark.png differ diff --git a/extras/emotes/public/default/heavy_minus_sign.png b/extras/emotes/public/default/heavy_minus_sign.png new file mode 100755 index 0000000..4a33f90 Binary files /dev/null and b/extras/emotes/public/default/heavy_minus_sign.png differ diff --git a/extras/emotes/public/default/heavy_multiplication_x.png b/extras/emotes/public/default/heavy_multiplication_x.png new file mode 100755 index 0000000..13d6660 Binary files /dev/null and b/extras/emotes/public/default/heavy_multiplication_x.png differ diff --git a/extras/emotes/public/default/heavy_plus_sign.png b/extras/emotes/public/default/heavy_plus_sign.png new file mode 100755 index 0000000..ade3c3a Binary files /dev/null and b/extras/emotes/public/default/heavy_plus_sign.png differ diff --git a/extras/emotes/public/default/helicopter.png b/extras/emotes/public/default/helicopter.png new file mode 100755 index 0000000..8e82a0d Binary files /dev/null and b/extras/emotes/public/default/helicopter.png differ diff --git a/extras/emotes/public/default/herb.png b/extras/emotes/public/default/herb.png new file mode 100755 index 0000000..de1ff1b Binary files /dev/null and b/extras/emotes/public/default/herb.png differ diff --git a/extras/emotes/public/default/hibiscus.png b/extras/emotes/public/default/hibiscus.png new file mode 100755 index 0000000..9365ae2 Binary files /dev/null and b/extras/emotes/public/default/hibiscus.png differ diff --git a/extras/emotes/public/default/high_brightness.png b/extras/emotes/public/default/high_brightness.png new file mode 100755 index 0000000..ba9de7d Binary files /dev/null and b/extras/emotes/public/default/high_brightness.png differ diff --git a/extras/emotes/public/default/high_heel.png b/extras/emotes/public/default/high_heel.png new file mode 100755 index 0000000..525b6a0 Binary files /dev/null and b/extras/emotes/public/default/high_heel.png differ diff --git a/extras/emotes/public/default/hocho.png b/extras/emotes/public/default/hocho.png new file mode 100755 index 0000000..3f05193 Binary files /dev/null and b/extras/emotes/public/default/hocho.png differ diff --git a/extras/emotes/public/default/honey_pot.png b/extras/emotes/public/default/honey_pot.png new file mode 100755 index 0000000..7327889 Binary files /dev/null and b/extras/emotes/public/default/honey_pot.png differ diff --git a/extras/emotes/public/default/honeybee.png b/extras/emotes/public/default/honeybee.png new file mode 100755 index 0000000..f537339 Binary files /dev/null and b/extras/emotes/public/default/honeybee.png differ diff --git a/extras/emotes/public/default/horse.png b/extras/emotes/public/default/horse.png new file mode 100755 index 0000000..78d580a Binary files /dev/null and b/extras/emotes/public/default/horse.png differ diff --git a/extras/emotes/public/default/horse_racing.png b/extras/emotes/public/default/horse_racing.png new file mode 100755 index 0000000..e3bbaec Binary files /dev/null and b/extras/emotes/public/default/horse_racing.png differ diff --git a/extras/emotes/public/default/hospital.png b/extras/emotes/public/default/hospital.png new file mode 100755 index 0000000..c05c493 Binary files /dev/null and b/extras/emotes/public/default/hospital.png differ diff --git a/extras/emotes/public/default/hotel.png b/extras/emotes/public/default/hotel.png new file mode 100755 index 0000000..d29f276 Binary files /dev/null and b/extras/emotes/public/default/hotel.png differ diff --git a/extras/emotes/public/default/hotsprings.png b/extras/emotes/public/default/hotsprings.png new file mode 100755 index 0000000..a0bc9d7 Binary files /dev/null and b/extras/emotes/public/default/hotsprings.png differ diff --git a/extras/emotes/public/default/hourglass.png b/extras/emotes/public/default/hourglass.png new file mode 100755 index 0000000..405aab4 Binary files /dev/null and b/extras/emotes/public/default/hourglass.png differ diff --git a/extras/emotes/public/default/hourglass_flowing_sand.png b/extras/emotes/public/default/hourglass_flowing_sand.png new file mode 100755 index 0000000..b68eb69 Binary files /dev/null and b/extras/emotes/public/default/hourglass_flowing_sand.png differ diff --git a/extras/emotes/public/default/house.png b/extras/emotes/public/default/house.png new file mode 100755 index 0000000..95b9ee0 Binary files /dev/null and b/extras/emotes/public/default/house.png differ diff --git a/extras/emotes/public/default/house_with_garden.png b/extras/emotes/public/default/house_with_garden.png new file mode 100755 index 0000000..3338fb7 Binary files /dev/null and b/extras/emotes/public/default/house_with_garden.png differ diff --git a/extras/emotes/public/default/hurtrealbad.png b/extras/emotes/public/default/hurtrealbad.png new file mode 100755 index 0000000..146ef1a Binary files /dev/null and b/extras/emotes/public/default/hurtrealbad.png differ diff --git a/extras/emotes/public/default/hushed.png b/extras/emotes/public/default/hushed.png new file mode 100755 index 0000000..bbd2cd4 Binary files /dev/null and b/extras/emotes/public/default/hushed.png differ diff --git a/extras/emotes/public/default/ice_cream.png b/extras/emotes/public/default/ice_cream.png new file mode 100755 index 0000000..190be01 Binary files /dev/null and b/extras/emotes/public/default/ice_cream.png differ diff --git a/extras/emotes/public/default/icecream.png b/extras/emotes/public/default/icecream.png new file mode 100755 index 0000000..871ce09 Binary files /dev/null and b/extras/emotes/public/default/icecream.png differ diff --git a/extras/emotes/public/default/id.png b/extras/emotes/public/default/id.png new file mode 100755 index 0000000..47437a7 Binary files /dev/null and b/extras/emotes/public/default/id.png differ diff --git a/extras/emotes/public/default/ideograph_advantage.png b/extras/emotes/public/default/ideograph_advantage.png new file mode 100755 index 0000000..3c1334d Binary files /dev/null and b/extras/emotes/public/default/ideograph_advantage.png differ diff --git a/extras/emotes/public/default/imp.png b/extras/emotes/public/default/imp.png new file mode 100755 index 0000000..fa7d9dc Binary files /dev/null and b/extras/emotes/public/default/imp.png differ diff --git a/extras/emotes/public/default/inbox_tray.png b/extras/emotes/public/default/inbox_tray.png new file mode 100755 index 0000000..e2df0f8 Binary files /dev/null and b/extras/emotes/public/default/inbox_tray.png differ diff --git a/extras/emotes/public/default/incoming_envelope.png b/extras/emotes/public/default/incoming_envelope.png new file mode 100755 index 0000000..afc8271 Binary files /dev/null and b/extras/emotes/public/default/incoming_envelope.png differ diff --git a/extras/emotes/public/default/information_desk_person.png b/extras/emotes/public/default/information_desk_person.png new file mode 100755 index 0000000..52c0a50 Binary files /dev/null and b/extras/emotes/public/default/information_desk_person.png differ diff --git a/extras/emotes/public/default/information_source.png b/extras/emotes/public/default/information_source.png new file mode 100755 index 0000000..9cb8b09 Binary files /dev/null and b/extras/emotes/public/default/information_source.png differ diff --git a/extras/emotes/public/default/innocent.png b/extras/emotes/public/default/innocent.png new file mode 100755 index 0000000..503b614 Binary files /dev/null and b/extras/emotes/public/default/innocent.png differ diff --git a/extras/emotes/public/default/interrobang.png b/extras/emotes/public/default/interrobang.png new file mode 100755 index 0000000..64304b9 Binary files /dev/null and b/extras/emotes/public/default/interrobang.png differ diff --git a/extras/emotes/public/default/iphone.png b/extras/emotes/public/default/iphone.png new file mode 100755 index 0000000..df00710 Binary files /dev/null and b/extras/emotes/public/default/iphone.png differ diff --git a/extras/emotes/public/default/it.png b/extras/emotes/public/default/it.png new file mode 100755 index 0000000..70bc9f3 Binary files /dev/null and b/extras/emotes/public/default/it.png differ diff --git a/extras/emotes/public/default/izakaya_lantern.png b/extras/emotes/public/default/izakaya_lantern.png new file mode 100755 index 0000000..18730ad Binary files /dev/null and b/extras/emotes/public/default/izakaya_lantern.png differ diff --git a/extras/emotes/public/default/jack_o_lantern.png b/extras/emotes/public/default/jack_o_lantern.png new file mode 100755 index 0000000..1f7667e Binary files /dev/null and b/extras/emotes/public/default/jack_o_lantern.png differ diff --git a/extras/emotes/public/default/japan.png b/extras/emotes/public/default/japan.png new file mode 100755 index 0000000..4593280 Binary files /dev/null and b/extras/emotes/public/default/japan.png differ diff --git a/extras/emotes/public/default/japanese_castle.png b/extras/emotes/public/default/japanese_castle.png new file mode 100755 index 0000000..f225ab2 Binary files /dev/null and b/extras/emotes/public/default/japanese_castle.png differ diff --git a/extras/emotes/public/default/japanese_goblin.png b/extras/emotes/public/default/japanese_goblin.png new file mode 100755 index 0000000..bd21b18 Binary files /dev/null and b/extras/emotes/public/default/japanese_goblin.png differ diff --git a/extras/emotes/public/default/japanese_ogre.png b/extras/emotes/public/default/japanese_ogre.png new file mode 100755 index 0000000..e9f5471 Binary files /dev/null and b/extras/emotes/public/default/japanese_ogre.png differ diff --git a/extras/emotes/public/default/jeans.png b/extras/emotes/public/default/jeans.png new file mode 100755 index 0000000..d721cea Binary files /dev/null and b/extras/emotes/public/default/jeans.png differ diff --git a/extras/emotes/public/default/joy.png b/extras/emotes/public/default/joy.png new file mode 100755 index 0000000..47df693 Binary files /dev/null and b/extras/emotes/public/default/joy.png differ diff --git a/extras/emotes/public/default/joy_cat.png b/extras/emotes/public/default/joy_cat.png new file mode 100755 index 0000000..6c60cb0 Binary files /dev/null and b/extras/emotes/public/default/joy_cat.png differ diff --git a/extras/emotes/public/default/jp.png b/extras/emotes/public/default/jp.png new file mode 100755 index 0000000..b786efb Binary files /dev/null and b/extras/emotes/public/default/jp.png differ diff --git a/extras/emotes/public/default/key.png b/extras/emotes/public/default/key.png new file mode 100755 index 0000000..3467321 Binary files /dev/null and b/extras/emotes/public/default/key.png differ diff --git a/extras/emotes/public/default/keycap_ten.png b/extras/emotes/public/default/keycap_ten.png new file mode 100755 index 0000000..71dac1c Binary files /dev/null and b/extras/emotes/public/default/keycap_ten.png differ diff --git a/extras/emotes/public/default/kimono.png b/extras/emotes/public/default/kimono.png new file mode 100755 index 0000000..34ffe13 Binary files /dev/null and b/extras/emotes/public/default/kimono.png differ diff --git a/extras/emotes/public/default/kiss.png b/extras/emotes/public/default/kiss.png new file mode 100755 index 0000000..14fd991 Binary files /dev/null and b/extras/emotes/public/default/kiss.png differ diff --git a/extras/emotes/public/default/kissing.png b/extras/emotes/public/default/kissing.png new file mode 100755 index 0000000..f3c8dcd Binary files /dev/null and b/extras/emotes/public/default/kissing.png differ diff --git a/extras/emotes/public/default/kissing_cat.png b/extras/emotes/public/default/kissing_cat.png new file mode 100755 index 0000000..adc62fb Binary files /dev/null and b/extras/emotes/public/default/kissing_cat.png differ diff --git a/extras/emotes/public/default/kissing_closed_eyes.png b/extras/emotes/public/default/kissing_closed_eyes.png new file mode 100755 index 0000000..449de19 Binary files /dev/null and b/extras/emotes/public/default/kissing_closed_eyes.png differ diff --git a/extras/emotes/public/default/kissing_face.png b/extras/emotes/public/default/kissing_face.png new file mode 100755 index 0000000..449de19 Binary files /dev/null and b/extras/emotes/public/default/kissing_face.png differ diff --git a/extras/emotes/public/default/kissing_heart.png b/extras/emotes/public/default/kissing_heart.png new file mode 100755 index 0000000..af9a80b Binary files /dev/null and b/extras/emotes/public/default/kissing_heart.png differ diff --git a/extras/emotes/public/default/kissing_smiling_eyes.png b/extras/emotes/public/default/kissing_smiling_eyes.png new file mode 100755 index 0000000..57f7b49 Binary files /dev/null and b/extras/emotes/public/default/kissing_smiling_eyes.png differ diff --git a/extras/emotes/public/default/koala.png b/extras/emotes/public/default/koala.png new file mode 100755 index 0000000..e17bd3c Binary files /dev/null and b/extras/emotes/public/default/koala.png differ diff --git a/extras/emotes/public/default/koko.png b/extras/emotes/public/default/koko.png new file mode 100755 index 0000000..3bef28c Binary files /dev/null and b/extras/emotes/public/default/koko.png differ diff --git a/extras/emotes/public/default/kr.png b/extras/emotes/public/default/kr.png new file mode 100755 index 0000000..b4c0c1b Binary files /dev/null and b/extras/emotes/public/default/kr.png differ diff --git a/extras/emotes/public/default/large_blue_circle.png b/extras/emotes/public/default/large_blue_circle.png new file mode 100755 index 0000000..a5b4ad4 Binary files /dev/null and b/extras/emotes/public/default/large_blue_circle.png differ diff --git a/extras/emotes/public/default/large_blue_diamond.png b/extras/emotes/public/default/large_blue_diamond.png new file mode 100755 index 0000000..f4598ec Binary files /dev/null and b/extras/emotes/public/default/large_blue_diamond.png differ diff --git a/extras/emotes/public/default/large_orange_diamond.png b/extras/emotes/public/default/large_orange_diamond.png new file mode 100755 index 0000000..803725a Binary files /dev/null and b/extras/emotes/public/default/large_orange_diamond.png differ diff --git a/extras/emotes/public/default/last_quarter_moon.png b/extras/emotes/public/default/last_quarter_moon.png new file mode 100755 index 0000000..6ae30d6 Binary files /dev/null and b/extras/emotes/public/default/last_quarter_moon.png differ diff --git a/extras/emotes/public/default/last_quarter_moon_with_face.png b/extras/emotes/public/default/last_quarter_moon_with_face.png new file mode 100755 index 0000000..9ece82d Binary files /dev/null and b/extras/emotes/public/default/last_quarter_moon_with_face.png differ diff --git a/extras/emotes/public/default/laughing.png b/extras/emotes/public/default/laughing.png new file mode 100755 index 0000000..11c91eb Binary files /dev/null and b/extras/emotes/public/default/laughing.png differ diff --git a/extras/emotes/public/default/leaves.png b/extras/emotes/public/default/leaves.png new file mode 100755 index 0000000..5229e06 Binary files /dev/null and b/extras/emotes/public/default/leaves.png differ diff --git a/extras/emotes/public/default/ledger.png b/extras/emotes/public/default/ledger.png new file mode 100755 index 0000000..e4f72ac Binary files /dev/null and b/extras/emotes/public/default/ledger.png differ diff --git a/extras/emotes/public/default/left_luggage.png b/extras/emotes/public/default/left_luggage.png new file mode 100755 index 0000000..1c08b46 Binary files /dev/null and b/extras/emotes/public/default/left_luggage.png differ diff --git a/extras/emotes/public/default/left_right_arrow.png b/extras/emotes/public/default/left_right_arrow.png new file mode 100755 index 0000000..b9fd11c Binary files /dev/null and b/extras/emotes/public/default/left_right_arrow.png differ diff --git a/extras/emotes/public/default/leftwards_arrow_with_hook.png b/extras/emotes/public/default/leftwards_arrow_with_hook.png new file mode 100755 index 0000000..bc45dfe Binary files /dev/null and b/extras/emotes/public/default/leftwards_arrow_with_hook.png differ diff --git a/extras/emotes/public/default/lemon.png b/extras/emotes/public/default/lemon.png new file mode 100755 index 0000000..9814dc9 Binary files /dev/null and b/extras/emotes/public/default/lemon.png differ diff --git a/extras/emotes/public/default/leo.png b/extras/emotes/public/default/leo.png new file mode 100755 index 0000000..e025933 Binary files /dev/null and b/extras/emotes/public/default/leo.png differ diff --git a/extras/emotes/public/default/leopard.png b/extras/emotes/public/default/leopard.png new file mode 100755 index 0000000..3e738d2 Binary files /dev/null and b/extras/emotes/public/default/leopard.png differ diff --git a/extras/emotes/public/default/libra.png b/extras/emotes/public/default/libra.png new file mode 100755 index 0000000..6f4a927 Binary files /dev/null and b/extras/emotes/public/default/libra.png differ diff --git a/extras/emotes/public/default/light_rail.png b/extras/emotes/public/default/light_rail.png new file mode 100755 index 0000000..bcfe801 Binary files /dev/null and b/extras/emotes/public/default/light_rail.png differ diff --git a/extras/emotes/public/default/link.png b/extras/emotes/public/default/link.png new file mode 100755 index 0000000..0239e48 Binary files /dev/null and b/extras/emotes/public/default/link.png differ diff --git a/extras/emotes/public/default/lips.png b/extras/emotes/public/default/lips.png new file mode 100755 index 0000000..826ed11 Binary files /dev/null and b/extras/emotes/public/default/lips.png differ diff --git a/extras/emotes/public/default/lipstick.png b/extras/emotes/public/default/lipstick.png new file mode 100755 index 0000000..82f990c Binary files /dev/null and b/extras/emotes/public/default/lipstick.png differ diff --git a/extras/emotes/public/default/lock.png b/extras/emotes/public/default/lock.png new file mode 100755 index 0000000..4892b02 Binary files /dev/null and b/extras/emotes/public/default/lock.png differ diff --git a/extras/emotes/public/default/lock_with_ink_pen.png b/extras/emotes/public/default/lock_with_ink_pen.png new file mode 100755 index 0000000..375e67e Binary files /dev/null and b/extras/emotes/public/default/lock_with_ink_pen.png differ diff --git a/extras/emotes/public/default/lollipop.png b/extras/emotes/public/default/lollipop.png new file mode 100755 index 0000000..ba55e70 Binary files /dev/null and b/extras/emotes/public/default/lollipop.png differ diff --git a/extras/emotes/public/default/loop.png b/extras/emotes/public/default/loop.png new file mode 100755 index 0000000..ef34df3 Binary files /dev/null and b/extras/emotes/public/default/loop.png differ diff --git a/extras/emotes/public/default/loudspeaker.png b/extras/emotes/public/default/loudspeaker.png new file mode 100755 index 0000000..752385e Binary files /dev/null and b/extras/emotes/public/default/loudspeaker.png differ diff --git a/extras/emotes/public/default/love_hotel.png b/extras/emotes/public/default/love_hotel.png new file mode 100755 index 0000000..44d7db8 Binary files /dev/null and b/extras/emotes/public/default/love_hotel.png differ diff --git a/extras/emotes/public/default/love_letter.png b/extras/emotes/public/default/love_letter.png new file mode 100755 index 0000000..e29981f Binary files /dev/null and b/extras/emotes/public/default/love_letter.png differ diff --git a/extras/emotes/public/default/low_brightness.png b/extras/emotes/public/default/low_brightness.png new file mode 100755 index 0000000..ea15bde Binary files /dev/null and b/extras/emotes/public/default/low_brightness.png differ diff --git a/extras/emotes/public/default/m.png b/extras/emotes/public/default/m.png new file mode 100755 index 0000000..7e3a3bf Binary files /dev/null and b/extras/emotes/public/default/m.png differ diff --git a/extras/emotes/public/default/mag.png b/extras/emotes/public/default/mag.png new file mode 100755 index 0000000..aa5b1d7 Binary files /dev/null and b/extras/emotes/public/default/mag.png differ diff --git a/extras/emotes/public/default/mag_right.png b/extras/emotes/public/default/mag_right.png new file mode 100755 index 0000000..6e6cf11 Binary files /dev/null and b/extras/emotes/public/default/mag_right.png differ diff --git a/extras/emotes/public/default/mahjong.png b/extras/emotes/public/default/mahjong.png new file mode 100755 index 0000000..f51ce65 Binary files /dev/null and b/extras/emotes/public/default/mahjong.png differ diff --git a/extras/emotes/public/default/mailbox.png b/extras/emotes/public/default/mailbox.png new file mode 100755 index 0000000..8351e70 Binary files /dev/null and b/extras/emotes/public/default/mailbox.png differ diff --git a/extras/emotes/public/default/mailbox_closed.png b/extras/emotes/public/default/mailbox_closed.png new file mode 100755 index 0000000..a5982b6 Binary files /dev/null and b/extras/emotes/public/default/mailbox_closed.png differ diff --git a/extras/emotes/public/default/mailbox_with_mail.png b/extras/emotes/public/default/mailbox_with_mail.png new file mode 100755 index 0000000..dae3459 Binary files /dev/null and b/extras/emotes/public/default/mailbox_with_mail.png differ diff --git a/extras/emotes/public/default/mailbox_with_no_mail.png b/extras/emotes/public/default/mailbox_with_no_mail.png new file mode 100755 index 0000000..59f15c5 Binary files /dev/null and b/extras/emotes/public/default/mailbox_with_no_mail.png differ diff --git a/extras/emotes/public/default/man.png b/extras/emotes/public/default/man.png new file mode 100755 index 0000000..d9bfa26 Binary files /dev/null and b/extras/emotes/public/default/man.png differ diff --git a/extras/emotes/public/default/man_with_gua_pi_mao.png b/extras/emotes/public/default/man_with_gua_pi_mao.png new file mode 100755 index 0000000..7aad74b Binary files /dev/null and b/extras/emotes/public/default/man_with_gua_pi_mao.png differ diff --git a/extras/emotes/public/default/man_with_turban.png b/extras/emotes/public/default/man_with_turban.png new file mode 100755 index 0000000..036604c Binary files /dev/null and b/extras/emotes/public/default/man_with_turban.png differ diff --git a/extras/emotes/public/default/mans_shoe.png b/extras/emotes/public/default/mans_shoe.png new file mode 100755 index 0000000..ecba9ba Binary files /dev/null and b/extras/emotes/public/default/mans_shoe.png differ diff --git a/extras/emotes/public/default/maple_leaf.png b/extras/emotes/public/default/maple_leaf.png new file mode 100755 index 0000000..4e9b472 Binary files /dev/null and b/extras/emotes/public/default/maple_leaf.png differ diff --git a/extras/emotes/public/default/mask.png b/extras/emotes/public/default/mask.png new file mode 100755 index 0000000..05887e9 Binary files /dev/null and b/extras/emotes/public/default/mask.png differ diff --git a/extras/emotes/public/default/massage.png b/extras/emotes/public/default/massage.png new file mode 100755 index 0000000..dd30d15 Binary files /dev/null and b/extras/emotes/public/default/massage.png differ diff --git a/extras/emotes/public/default/meat_on_bone.png b/extras/emotes/public/default/meat_on_bone.png new file mode 100755 index 0000000..5b79a66 Binary files /dev/null and b/extras/emotes/public/default/meat_on_bone.png differ diff --git a/extras/emotes/public/default/mega.png b/extras/emotes/public/default/mega.png new file mode 100755 index 0000000..022df2f Binary files /dev/null and b/extras/emotes/public/default/mega.png differ diff --git a/extras/emotes/public/default/melon.png b/extras/emotes/public/default/melon.png new file mode 100755 index 0000000..11c13cb Binary files /dev/null and b/extras/emotes/public/default/melon.png differ diff --git a/extras/emotes/public/default/memo.png b/extras/emotes/public/default/memo.png new file mode 100755 index 0000000..fc97ddb Binary files /dev/null and b/extras/emotes/public/default/memo.png differ diff --git a/extras/emotes/public/default/mens.png b/extras/emotes/public/default/mens.png new file mode 100755 index 0000000..abccfc9 Binary files /dev/null and b/extras/emotes/public/default/mens.png differ diff --git a/extras/emotes/public/default/metal.png b/extras/emotes/public/default/metal.png new file mode 100755 index 0000000..94f1fda Binary files /dev/null and b/extras/emotes/public/default/metal.png differ diff --git a/extras/emotes/public/default/metro.png b/extras/emotes/public/default/metro.png new file mode 100755 index 0000000..4acf5ab Binary files /dev/null and b/extras/emotes/public/default/metro.png differ diff --git a/extras/emotes/public/default/microphone.png b/extras/emotes/public/default/microphone.png new file mode 100755 index 0000000..68c74ad Binary files /dev/null and b/extras/emotes/public/default/microphone.png differ diff --git a/extras/emotes/public/default/microscope.png b/extras/emotes/public/default/microscope.png new file mode 100755 index 0000000..8b7a5e4 Binary files /dev/null and b/extras/emotes/public/default/microscope.png differ diff --git a/extras/emotes/public/default/milky_way.png b/extras/emotes/public/default/milky_way.png new file mode 100755 index 0000000..901090a Binary files /dev/null and b/extras/emotes/public/default/milky_way.png differ diff --git a/extras/emotes/public/default/minibus.png b/extras/emotes/public/default/minibus.png new file mode 100755 index 0000000..c52cef2 Binary files /dev/null and b/extras/emotes/public/default/minibus.png differ diff --git a/extras/emotes/public/default/minidisc.png b/extras/emotes/public/default/minidisc.png new file mode 100755 index 0000000..e19cc5d Binary files /dev/null and b/extras/emotes/public/default/minidisc.png differ diff --git a/extras/emotes/public/default/mobile_phone_off.png b/extras/emotes/public/default/mobile_phone_off.png new file mode 100755 index 0000000..fa16c76 Binary files /dev/null and b/extras/emotes/public/default/mobile_phone_off.png differ diff --git a/extras/emotes/public/default/money_with_wings.png b/extras/emotes/public/default/money_with_wings.png new file mode 100755 index 0000000..581a824 Binary files /dev/null and b/extras/emotes/public/default/money_with_wings.png differ diff --git a/extras/emotes/public/default/moneybag.png b/extras/emotes/public/default/moneybag.png new file mode 100755 index 0000000..5546c04 Binary files /dev/null and b/extras/emotes/public/default/moneybag.png differ diff --git a/extras/emotes/public/default/monkey.png b/extras/emotes/public/default/monkey.png new file mode 100755 index 0000000..6407035 Binary files /dev/null and b/extras/emotes/public/default/monkey.png differ diff --git a/extras/emotes/public/default/monkey_face.png b/extras/emotes/public/default/monkey_face.png new file mode 100755 index 0000000..6964cf4 Binary files /dev/null and b/extras/emotes/public/default/monkey_face.png differ diff --git a/extras/emotes/public/default/monorail.png b/extras/emotes/public/default/monorail.png new file mode 100755 index 0000000..913d300 Binary files /dev/null and b/extras/emotes/public/default/monorail.png differ diff --git a/extras/emotes/public/default/moon.png b/extras/emotes/public/default/moon.png new file mode 100755 index 0000000..afdb450 Binary files /dev/null and b/extras/emotes/public/default/moon.png differ diff --git a/extras/emotes/public/default/mortar_board.png b/extras/emotes/public/default/mortar_board.png new file mode 100755 index 0000000..84513f6 Binary files /dev/null and b/extras/emotes/public/default/mortar_board.png differ diff --git a/extras/emotes/public/default/mount_fuji.png b/extras/emotes/public/default/mount_fuji.png new file mode 100755 index 0000000..4c313e5 Binary files /dev/null and b/extras/emotes/public/default/mount_fuji.png differ diff --git a/extras/emotes/public/default/mountain_bicyclist.png b/extras/emotes/public/default/mountain_bicyclist.png new file mode 100755 index 0000000..b698897 Binary files /dev/null and b/extras/emotes/public/default/mountain_bicyclist.png differ diff --git a/extras/emotes/public/default/mountain_cableway.png b/extras/emotes/public/default/mountain_cableway.png new file mode 100755 index 0000000..5688bb2 Binary files /dev/null and b/extras/emotes/public/default/mountain_cableway.png differ diff --git a/extras/emotes/public/default/mountain_railway.png b/extras/emotes/public/default/mountain_railway.png new file mode 100755 index 0000000..1f3d1aa Binary files /dev/null and b/extras/emotes/public/default/mountain_railway.png differ diff --git a/extras/emotes/public/default/mouse.png b/extras/emotes/public/default/mouse.png new file mode 100755 index 0000000..8ff162e Binary files /dev/null and b/extras/emotes/public/default/mouse.png differ diff --git a/extras/emotes/public/default/mouse2.png b/extras/emotes/public/default/mouse2.png new file mode 100755 index 0000000..2d777e5 Binary files /dev/null and b/extras/emotes/public/default/mouse2.png differ diff --git a/extras/emotes/public/default/movie_camera.png b/extras/emotes/public/default/movie_camera.png new file mode 100755 index 0000000..9c14384 Binary files /dev/null and b/extras/emotes/public/default/movie_camera.png differ diff --git a/extras/emotes/public/default/moyai.png b/extras/emotes/public/default/moyai.png new file mode 100755 index 0000000..61a1a9c Binary files /dev/null and b/extras/emotes/public/default/moyai.png differ diff --git a/extras/emotes/public/default/muscle.png b/extras/emotes/public/default/muscle.png new file mode 100755 index 0000000..19f92ef Binary files /dev/null and b/extras/emotes/public/default/muscle.png differ diff --git a/extras/emotes/public/default/mushroom.png b/extras/emotes/public/default/mushroom.png new file mode 100755 index 0000000..5eeed8e Binary files /dev/null and b/extras/emotes/public/default/mushroom.png differ diff --git a/extras/emotes/public/default/musical_keyboard.png b/extras/emotes/public/default/musical_keyboard.png new file mode 100755 index 0000000..93647a4 Binary files /dev/null and b/extras/emotes/public/default/musical_keyboard.png differ diff --git a/extras/emotes/public/default/musical_note.png b/extras/emotes/public/default/musical_note.png new file mode 100755 index 0000000..68b261b Binary files /dev/null and b/extras/emotes/public/default/musical_note.png differ diff --git a/extras/emotes/public/default/musical_score.png b/extras/emotes/public/default/musical_score.png new file mode 100755 index 0000000..c99e338 Binary files /dev/null and b/extras/emotes/public/default/musical_score.png differ diff --git a/extras/emotes/public/default/mute.png b/extras/emotes/public/default/mute.png new file mode 100755 index 0000000..4cf67c3 Binary files /dev/null and b/extras/emotes/public/default/mute.png differ diff --git a/extras/emotes/public/default/nail_care.png b/extras/emotes/public/default/nail_care.png new file mode 100755 index 0000000..6a66e63 Binary files /dev/null and b/extras/emotes/public/default/nail_care.png differ diff --git a/extras/emotes/public/default/name_badge.png b/extras/emotes/public/default/name_badge.png new file mode 100755 index 0000000..2b712dc Binary files /dev/null and b/extras/emotes/public/default/name_badge.png differ diff --git a/extras/emotes/public/default/neckbeard.png b/extras/emotes/public/default/neckbeard.png new file mode 100755 index 0000000..6e31d16 Binary files /dev/null and b/extras/emotes/public/default/neckbeard.png differ diff --git a/extras/emotes/public/default/necktie.png b/extras/emotes/public/default/necktie.png new file mode 100755 index 0000000..80461c6 Binary files /dev/null and b/extras/emotes/public/default/necktie.png differ diff --git a/extras/emotes/public/default/negative_squared_cross_mark.png b/extras/emotes/public/default/negative_squared_cross_mark.png new file mode 100755 index 0000000..b47a0ce Binary files /dev/null and b/extras/emotes/public/default/negative_squared_cross_mark.png differ diff --git a/extras/emotes/public/default/neutral_face.png b/extras/emotes/public/default/neutral_face.png new file mode 100755 index 0000000..682a1ba Binary files /dev/null and b/extras/emotes/public/default/neutral_face.png differ diff --git a/extras/emotes/public/default/new.png b/extras/emotes/public/default/new.png new file mode 100755 index 0000000..28d1570 Binary files /dev/null and b/extras/emotes/public/default/new.png differ diff --git a/extras/emotes/public/default/new_moon.png b/extras/emotes/public/default/new_moon.png new file mode 100755 index 0000000..72492cb Binary files /dev/null and b/extras/emotes/public/default/new_moon.png differ diff --git a/extras/emotes/public/default/new_moon_with_face.png b/extras/emotes/public/default/new_moon_with_face.png new file mode 100755 index 0000000..21a696e Binary files /dev/null and b/extras/emotes/public/default/new_moon_with_face.png differ diff --git a/extras/emotes/public/default/newspaper.png b/extras/emotes/public/default/newspaper.png new file mode 100755 index 0000000..60c3394 Binary files /dev/null and b/extras/emotes/public/default/newspaper.png differ diff --git a/extras/emotes/public/default/ng.png b/extras/emotes/public/default/ng.png new file mode 100755 index 0000000..2ca180a Binary files /dev/null and b/extras/emotes/public/default/ng.png differ diff --git a/extras/emotes/public/default/nine.png b/extras/emotes/public/default/nine.png new file mode 100755 index 0000000..8006cc9 Binary files /dev/null and b/extras/emotes/public/default/nine.png differ diff --git a/extras/emotes/public/default/no_bell.png b/extras/emotes/public/default/no_bell.png new file mode 100755 index 0000000..613b81c Binary files /dev/null and b/extras/emotes/public/default/no_bell.png differ diff --git a/extras/emotes/public/default/no_bicycles.png b/extras/emotes/public/default/no_bicycles.png new file mode 100755 index 0000000..4b26216 Binary files /dev/null and b/extras/emotes/public/default/no_bicycles.png differ diff --git a/extras/emotes/public/default/no_entry.png b/extras/emotes/public/default/no_entry.png new file mode 100755 index 0000000..cf2086a Binary files /dev/null and b/extras/emotes/public/default/no_entry.png differ diff --git a/extras/emotes/public/default/no_entry_sign.png b/extras/emotes/public/default/no_entry_sign.png new file mode 100755 index 0000000..b3231f6 Binary files /dev/null and b/extras/emotes/public/default/no_entry_sign.png differ diff --git a/extras/emotes/public/default/no_good.png b/extras/emotes/public/default/no_good.png new file mode 100755 index 0000000..d459a35 Binary files /dev/null and b/extras/emotes/public/default/no_good.png differ diff --git a/extras/emotes/public/default/no_mobile_phones.png b/extras/emotes/public/default/no_mobile_phones.png new file mode 100755 index 0000000..41df57c Binary files /dev/null and b/extras/emotes/public/default/no_mobile_phones.png differ diff --git a/extras/emotes/public/default/no_mouth.png b/extras/emotes/public/default/no_mouth.png new file mode 100755 index 0000000..e678020 Binary files /dev/null and b/extras/emotes/public/default/no_mouth.png differ diff --git a/extras/emotes/public/default/no_pedestrians.png b/extras/emotes/public/default/no_pedestrians.png new file mode 100755 index 0000000..53ee0f9 Binary files /dev/null and b/extras/emotes/public/default/no_pedestrians.png differ diff --git a/extras/emotes/public/default/no_smoking.png b/extras/emotes/public/default/no_smoking.png new file mode 100755 index 0000000..5880ddf Binary files /dev/null and b/extras/emotes/public/default/no_smoking.png differ diff --git a/extras/emotes/public/default/non-potable_water.png b/extras/emotes/public/default/non-potable_water.png new file mode 100755 index 0000000..1b29d35 Binary files /dev/null and b/extras/emotes/public/default/non-potable_water.png differ diff --git a/extras/emotes/public/default/nose.png b/extras/emotes/public/default/nose.png new file mode 100755 index 0000000..ad17c16 Binary files /dev/null and b/extras/emotes/public/default/nose.png differ diff --git a/extras/emotes/public/default/notebook.png b/extras/emotes/public/default/notebook.png new file mode 100755 index 0000000..5f0a5f6 Binary files /dev/null and b/extras/emotes/public/default/notebook.png differ diff --git a/extras/emotes/public/default/notebook_with_decorative_cover.png b/extras/emotes/public/default/notebook_with_decorative_cover.png new file mode 100755 index 0000000..4f3b14c Binary files /dev/null and b/extras/emotes/public/default/notebook_with_decorative_cover.png differ diff --git a/extras/emotes/public/default/notes.png b/extras/emotes/public/default/notes.png new file mode 100755 index 0000000..0956d6a Binary files /dev/null and b/extras/emotes/public/default/notes.png differ diff --git a/extras/emotes/public/default/nut_and_bolt.png b/extras/emotes/public/default/nut_and_bolt.png new file mode 100755 index 0000000..bddfa72 Binary files /dev/null and b/extras/emotes/public/default/nut_and_bolt.png differ diff --git a/extras/emotes/public/default/o.png b/extras/emotes/public/default/o.png new file mode 100755 index 0000000..1ff846c Binary files /dev/null and b/extras/emotes/public/default/o.png differ diff --git a/extras/emotes/public/default/o2.png b/extras/emotes/public/default/o2.png new file mode 100755 index 0000000..d85f9fb Binary files /dev/null and b/extras/emotes/public/default/o2.png differ diff --git a/extras/emotes/public/default/ocean.png b/extras/emotes/public/default/ocean.png new file mode 100755 index 0000000..f8d520c Binary files /dev/null and b/extras/emotes/public/default/ocean.png differ diff --git a/extras/emotes/public/default/octocat.png b/extras/emotes/public/default/octocat.png new file mode 100755 index 0000000..d296f25 Binary files /dev/null and b/extras/emotes/public/default/octocat.png differ diff --git a/extras/emotes/public/default/octopus.png b/extras/emotes/public/default/octopus.png new file mode 100755 index 0000000..52ce64b Binary files /dev/null and b/extras/emotes/public/default/octopus.png differ diff --git a/extras/emotes/public/default/oden.png b/extras/emotes/public/default/oden.png new file mode 100755 index 0000000..73add1c Binary files /dev/null and b/extras/emotes/public/default/oden.png differ diff --git a/extras/emotes/public/default/office.png b/extras/emotes/public/default/office.png new file mode 100755 index 0000000..53c3ef8 Binary files /dev/null and b/extras/emotes/public/default/office.png differ diff --git a/extras/emotes/public/default/ok.png b/extras/emotes/public/default/ok.png new file mode 100755 index 0000000..6433d1a Binary files /dev/null and b/extras/emotes/public/default/ok.png differ diff --git a/extras/emotes/public/default/ok_hand.png b/extras/emotes/public/default/ok_hand.png new file mode 100755 index 0000000..80c5aeb Binary files /dev/null and b/extras/emotes/public/default/ok_hand.png differ diff --git a/extras/emotes/public/default/ok_woman.png b/extras/emotes/public/default/ok_woman.png new file mode 100755 index 0000000..e8b9819 Binary files /dev/null and b/extras/emotes/public/default/ok_woman.png differ diff --git a/extras/emotes/public/default/older_man.png b/extras/emotes/public/default/older_man.png new file mode 100755 index 0000000..149f0cf Binary files /dev/null and b/extras/emotes/public/default/older_man.png differ diff --git a/extras/emotes/public/default/older_woman.png b/extras/emotes/public/default/older_woman.png new file mode 100755 index 0000000..f839565 Binary files /dev/null and b/extras/emotes/public/default/older_woman.png differ diff --git a/extras/emotes/public/default/on.png b/extras/emotes/public/default/on.png new file mode 100755 index 0000000..4cd69a1 Binary files /dev/null and b/extras/emotes/public/default/on.png differ diff --git a/extras/emotes/public/default/oncoming_automobile.png b/extras/emotes/public/default/oncoming_automobile.png new file mode 100755 index 0000000..cb46de2 Binary files /dev/null and b/extras/emotes/public/default/oncoming_automobile.png differ diff --git a/extras/emotes/public/default/oncoming_bus.png b/extras/emotes/public/default/oncoming_bus.png new file mode 100755 index 0000000..3695f76 Binary files /dev/null and b/extras/emotes/public/default/oncoming_bus.png differ diff --git a/extras/emotes/public/default/oncoming_police_car.png b/extras/emotes/public/default/oncoming_police_car.png new file mode 100755 index 0000000..af20e7e Binary files /dev/null and b/extras/emotes/public/default/oncoming_police_car.png differ diff --git a/extras/emotes/public/default/oncoming_taxi.png b/extras/emotes/public/default/oncoming_taxi.png new file mode 100755 index 0000000..f78cf31 Binary files /dev/null and b/extras/emotes/public/default/oncoming_taxi.png differ diff --git a/extras/emotes/public/default/one.png b/extras/emotes/public/default/one.png new file mode 100755 index 0000000..2d1f9f8 Binary files /dev/null and b/extras/emotes/public/default/one.png differ diff --git a/extras/emotes/public/default/open_file_folder.png b/extras/emotes/public/default/open_file_folder.png new file mode 100755 index 0000000..2bbbbf5 Binary files /dev/null and b/extras/emotes/public/default/open_file_folder.png differ diff --git a/extras/emotes/public/default/open_hands.png b/extras/emotes/public/default/open_hands.png new file mode 100755 index 0000000..cef9f42 Binary files /dev/null and b/extras/emotes/public/default/open_hands.png differ diff --git a/extras/emotes/public/default/open_mouth.png b/extras/emotes/public/default/open_mouth.png new file mode 100755 index 0000000..daf9142 Binary files /dev/null and b/extras/emotes/public/default/open_mouth.png differ diff --git a/extras/emotes/public/default/ophiuchus.png b/extras/emotes/public/default/ophiuchus.png new file mode 100755 index 0000000..4eef715 Binary files /dev/null and b/extras/emotes/public/default/ophiuchus.png differ diff --git a/extras/emotes/public/default/orange_book.png b/extras/emotes/public/default/orange_book.png new file mode 100755 index 0000000..49650d5 Binary files /dev/null and b/extras/emotes/public/default/orange_book.png differ diff --git a/extras/emotes/public/default/outbox_tray.png b/extras/emotes/public/default/outbox_tray.png new file mode 100755 index 0000000..7ad15e6 Binary files /dev/null and b/extras/emotes/public/default/outbox_tray.png differ diff --git a/extras/emotes/public/default/ox.png b/extras/emotes/public/default/ox.png new file mode 100755 index 0000000..8d98194 Binary files /dev/null and b/extras/emotes/public/default/ox.png differ diff --git a/extras/emotes/public/default/package.png b/extras/emotes/public/default/package.png new file mode 100755 index 0000000..26602af Binary files /dev/null and b/extras/emotes/public/default/package.png differ diff --git a/extras/emotes/public/default/page_facing_up.png b/extras/emotes/public/default/page_facing_up.png new file mode 100755 index 0000000..804c0d7 Binary files /dev/null and b/extras/emotes/public/default/page_facing_up.png differ diff --git a/extras/emotes/public/default/page_with_curl.png b/extras/emotes/public/default/page_with_curl.png new file mode 100755 index 0000000..37cb4de Binary files /dev/null and b/extras/emotes/public/default/page_with_curl.png differ diff --git a/extras/emotes/public/default/pager.png b/extras/emotes/public/default/pager.png new file mode 100755 index 0000000..e3e1fc4 Binary files /dev/null and b/extras/emotes/public/default/pager.png differ diff --git a/extras/emotes/public/default/palm_tree.png b/extras/emotes/public/default/palm_tree.png new file mode 100755 index 0000000..d13b7c6 Binary files /dev/null and b/extras/emotes/public/default/palm_tree.png differ diff --git a/extras/emotes/public/default/panda_face.png b/extras/emotes/public/default/panda_face.png new file mode 100755 index 0000000..a794fb1 Binary files /dev/null and b/extras/emotes/public/default/panda_face.png differ diff --git a/extras/emotes/public/default/paperclip.png b/extras/emotes/public/default/paperclip.png new file mode 100755 index 0000000..677669a Binary files /dev/null and b/extras/emotes/public/default/paperclip.png differ diff --git a/extras/emotes/public/default/parking.png b/extras/emotes/public/default/parking.png new file mode 100755 index 0000000..c24af81 Binary files /dev/null and b/extras/emotes/public/default/parking.png differ diff --git a/extras/emotes/public/default/part_alternation_mark.png b/extras/emotes/public/default/part_alternation_mark.png new file mode 100755 index 0000000..1e5855f Binary files /dev/null and b/extras/emotes/public/default/part_alternation_mark.png differ diff --git a/extras/emotes/public/default/partly_sunny.png b/extras/emotes/public/default/partly_sunny.png new file mode 100755 index 0000000..b3f5bcf Binary files /dev/null and b/extras/emotes/public/default/partly_sunny.png differ diff --git a/extras/emotes/public/default/passport_control.png b/extras/emotes/public/default/passport_control.png new file mode 100755 index 0000000..675b76d Binary files /dev/null and b/extras/emotes/public/default/passport_control.png differ diff --git a/extras/emotes/public/default/paw_prints.png b/extras/emotes/public/default/paw_prints.png new file mode 100755 index 0000000..89b9fec Binary files /dev/null and b/extras/emotes/public/default/paw_prints.png differ diff --git a/extras/emotes/public/default/peach.png b/extras/emotes/public/default/peach.png new file mode 100755 index 0000000..ee2139e Binary files /dev/null and b/extras/emotes/public/default/peach.png differ diff --git a/extras/emotes/public/default/pear.png b/extras/emotes/public/default/pear.png new file mode 100755 index 0000000..f24aca8 Binary files /dev/null and b/extras/emotes/public/default/pear.png differ diff --git a/extras/emotes/public/default/pencil.png b/extras/emotes/public/default/pencil.png new file mode 100755 index 0000000..fc97ddb Binary files /dev/null and b/extras/emotes/public/default/pencil.png differ diff --git a/extras/emotes/public/default/pencil2.png b/extras/emotes/public/default/pencil2.png new file mode 100755 index 0000000..64c2d9b Binary files /dev/null and b/extras/emotes/public/default/pencil2.png differ diff --git a/extras/emotes/public/default/penguin.png b/extras/emotes/public/default/penguin.png new file mode 100755 index 0000000..d8edbcb Binary files /dev/null and b/extras/emotes/public/default/penguin.png differ diff --git a/extras/emotes/public/default/pensive.png b/extras/emotes/public/default/pensive.png new file mode 100755 index 0000000..4159f3c Binary files /dev/null and b/extras/emotes/public/default/pensive.png differ diff --git a/extras/emotes/public/default/performing_arts.png b/extras/emotes/public/default/performing_arts.png new file mode 100755 index 0000000..899fbe5 Binary files /dev/null and b/extras/emotes/public/default/performing_arts.png differ diff --git a/extras/emotes/public/default/persevere.png b/extras/emotes/public/default/persevere.png new file mode 100755 index 0000000..f99f6da Binary files /dev/null and b/extras/emotes/public/default/persevere.png differ diff --git a/extras/emotes/public/default/person_frowning.png b/extras/emotes/public/default/person_frowning.png new file mode 100755 index 0000000..6f34d5e Binary files /dev/null and b/extras/emotes/public/default/person_frowning.png differ diff --git a/extras/emotes/public/default/person_with_blond_hair.png b/extras/emotes/public/default/person_with_blond_hair.png new file mode 100755 index 0000000..c144301 Binary files /dev/null and b/extras/emotes/public/default/person_with_blond_hair.png differ diff --git a/extras/emotes/public/default/person_with_pouting_face.png b/extras/emotes/public/default/person_with_pouting_face.png new file mode 100755 index 0000000..c4a95c3 Binary files /dev/null and b/extras/emotes/public/default/person_with_pouting_face.png differ diff --git a/extras/emotes/public/default/phone.png b/extras/emotes/public/default/phone.png new file mode 100755 index 0000000..87d2559 Binary files /dev/null and b/extras/emotes/public/default/phone.png differ diff --git a/extras/emotes/public/default/pig.png b/extras/emotes/public/default/pig.png new file mode 100755 index 0000000..f7f273c Binary files /dev/null and b/extras/emotes/public/default/pig.png differ diff --git a/extras/emotes/public/default/pig2.png b/extras/emotes/public/default/pig2.png new file mode 100755 index 0000000..fec3374 Binary files /dev/null and b/extras/emotes/public/default/pig2.png differ diff --git a/extras/emotes/public/default/pig_nose.png b/extras/emotes/public/default/pig_nose.png new file mode 100755 index 0000000..38d6124 Binary files /dev/null and b/extras/emotes/public/default/pig_nose.png differ diff --git a/extras/emotes/public/default/pill.png b/extras/emotes/public/default/pill.png new file mode 100755 index 0000000..cd84a78 Binary files /dev/null and b/extras/emotes/public/default/pill.png differ diff --git a/extras/emotes/public/default/pineapple.png b/extras/emotes/public/default/pineapple.png new file mode 100755 index 0000000..d6f8e28 Binary files /dev/null and b/extras/emotes/public/default/pineapple.png differ diff --git a/extras/emotes/public/default/pisces.png b/extras/emotes/public/default/pisces.png new file mode 100755 index 0000000..6db2c3d Binary files /dev/null and b/extras/emotes/public/default/pisces.png differ diff --git a/extras/emotes/public/default/pizza.png b/extras/emotes/public/default/pizza.png new file mode 100755 index 0000000..460367d Binary files /dev/null and b/extras/emotes/public/default/pizza.png differ diff --git a/extras/emotes/public/default/plus1.png b/extras/emotes/public/default/plus1.png new file mode 100755 index 0000000..81786c1 Binary files /dev/null and b/extras/emotes/public/default/plus1.png differ diff --git a/extras/emotes/public/default/point_down.png b/extras/emotes/public/default/point_down.png new file mode 100755 index 0000000..658c6d9 Binary files /dev/null and b/extras/emotes/public/default/point_down.png differ diff --git a/extras/emotes/public/default/point_left.png b/extras/emotes/public/default/point_left.png new file mode 100755 index 0000000..38a99b4 Binary files /dev/null and b/extras/emotes/public/default/point_left.png differ diff --git a/extras/emotes/public/default/point_right.png b/extras/emotes/public/default/point_right.png new file mode 100755 index 0000000..6f9f029 Binary files /dev/null and b/extras/emotes/public/default/point_right.png differ diff --git a/extras/emotes/public/default/point_up.png b/extras/emotes/public/default/point_up.png new file mode 100755 index 0000000..01896e2 Binary files /dev/null and b/extras/emotes/public/default/point_up.png differ diff --git a/extras/emotes/public/default/point_up_2.png b/extras/emotes/public/default/point_up_2.png new file mode 100755 index 0000000..1cfe736 Binary files /dev/null and b/extras/emotes/public/default/point_up_2.png differ diff --git a/extras/emotes/public/default/police_car.png b/extras/emotes/public/default/police_car.png new file mode 100755 index 0000000..b8f1727 Binary files /dev/null and b/extras/emotes/public/default/police_car.png differ diff --git a/extras/emotes/public/default/poodle.png b/extras/emotes/public/default/poodle.png new file mode 100755 index 0000000..adac80b Binary files /dev/null and b/extras/emotes/public/default/poodle.png differ diff --git a/extras/emotes/public/default/poop.png b/extras/emotes/public/default/poop.png new file mode 100755 index 0000000..73a4dc8 Binary files /dev/null and b/extras/emotes/public/default/poop.png differ diff --git a/extras/emotes/public/default/post_office.png b/extras/emotes/public/default/post_office.png new file mode 100755 index 0000000..43b59e3 Binary files /dev/null and b/extras/emotes/public/default/post_office.png differ diff --git a/extras/emotes/public/default/postal_horn.png b/extras/emotes/public/default/postal_horn.png new file mode 100755 index 0000000..13a1514 Binary files /dev/null and b/extras/emotes/public/default/postal_horn.png differ diff --git a/extras/emotes/public/default/postbox.png b/extras/emotes/public/default/postbox.png new file mode 100755 index 0000000..ce04b70 Binary files /dev/null and b/extras/emotes/public/default/postbox.png differ diff --git a/extras/emotes/public/default/potable_water.png b/extras/emotes/public/default/potable_water.png new file mode 100755 index 0000000..e9fd560 Binary files /dev/null and b/extras/emotes/public/default/potable_water.png differ diff --git a/extras/emotes/public/default/pouch.png b/extras/emotes/public/default/pouch.png new file mode 100755 index 0000000..dc35ae8 Binary files /dev/null and b/extras/emotes/public/default/pouch.png differ diff --git a/extras/emotes/public/default/poultry_leg.png b/extras/emotes/public/default/poultry_leg.png new file mode 100755 index 0000000..43ad859 Binary files /dev/null and b/extras/emotes/public/default/poultry_leg.png differ diff --git a/extras/emotes/public/default/pound.png b/extras/emotes/public/default/pound.png new file mode 100755 index 0000000..f8be91d Binary files /dev/null and b/extras/emotes/public/default/pound.png differ diff --git a/extras/emotes/public/default/pouting_cat.png b/extras/emotes/public/default/pouting_cat.png new file mode 100755 index 0000000..4325fd4 Binary files /dev/null and b/extras/emotes/public/default/pouting_cat.png differ diff --git a/extras/emotes/public/default/pray.png b/extras/emotes/public/default/pray.png new file mode 100755 index 0000000..f86c992 Binary files /dev/null and b/extras/emotes/public/default/pray.png differ diff --git a/extras/emotes/public/default/princess.png b/extras/emotes/public/default/princess.png new file mode 100755 index 0000000..1ebb2ce Binary files /dev/null and b/extras/emotes/public/default/princess.png differ diff --git a/extras/emotes/public/default/punch.png b/extras/emotes/public/default/punch.png new file mode 100755 index 0000000..277047b Binary files /dev/null and b/extras/emotes/public/default/punch.png differ diff --git a/extras/emotes/public/default/purple_heart.png b/extras/emotes/public/default/purple_heart.png new file mode 100755 index 0000000..d5f8750 Binary files /dev/null and b/extras/emotes/public/default/purple_heart.png differ diff --git a/extras/emotes/public/default/purse.png b/extras/emotes/public/default/purse.png new file mode 100755 index 0000000..8f06a2b Binary files /dev/null and b/extras/emotes/public/default/purse.png differ diff --git a/extras/emotes/public/default/pushpin.png b/extras/emotes/public/default/pushpin.png new file mode 100755 index 0000000..540c4ec Binary files /dev/null and b/extras/emotes/public/default/pushpin.png differ diff --git a/extras/emotes/public/default/put_litter_in_its_place.png b/extras/emotes/public/default/put_litter_in_its_place.png new file mode 100755 index 0000000..c2e350c Binary files /dev/null and b/extras/emotes/public/default/put_litter_in_its_place.png differ diff --git a/extras/emotes/public/default/question.png b/extras/emotes/public/default/question.png new file mode 100755 index 0000000..38cedf5 Binary files /dev/null and b/extras/emotes/public/default/question.png differ diff --git a/extras/emotes/public/default/rabbit.png b/extras/emotes/public/default/rabbit.png new file mode 100755 index 0000000..5cb3ef6 Binary files /dev/null and b/extras/emotes/public/default/rabbit.png differ diff --git a/extras/emotes/public/default/rabbit2.png b/extras/emotes/public/default/rabbit2.png new file mode 100755 index 0000000..a9fd24d Binary files /dev/null and b/extras/emotes/public/default/rabbit2.png differ diff --git a/extras/emotes/public/default/racehorse.png b/extras/emotes/public/default/racehorse.png new file mode 100755 index 0000000..4d09c64 Binary files /dev/null and b/extras/emotes/public/default/racehorse.png differ diff --git a/extras/emotes/public/default/radio.png b/extras/emotes/public/default/radio.png new file mode 100755 index 0000000..ea589ef Binary files /dev/null and b/extras/emotes/public/default/radio.png differ diff --git a/extras/emotes/public/default/radio_button.png b/extras/emotes/public/default/radio_button.png new file mode 100755 index 0000000..63755ee Binary files /dev/null and b/extras/emotes/public/default/radio_button.png differ diff --git a/extras/emotes/public/default/rage.png b/extras/emotes/public/default/rage.png new file mode 100755 index 0000000..c65ddff Binary files /dev/null and b/extras/emotes/public/default/rage.png differ diff --git a/extras/emotes/public/default/rage1.png b/extras/emotes/public/default/rage1.png new file mode 100755 index 0000000..1506ba4 Binary files /dev/null and b/extras/emotes/public/default/rage1.png differ diff --git a/extras/emotes/public/default/rage2.png b/extras/emotes/public/default/rage2.png new file mode 100755 index 0000000..f792e06 Binary files /dev/null and b/extras/emotes/public/default/rage2.png differ diff --git a/extras/emotes/public/default/rage3.png b/extras/emotes/public/default/rage3.png new file mode 100755 index 0000000..58764cb Binary files /dev/null and b/extras/emotes/public/default/rage3.png differ diff --git a/extras/emotes/public/default/rage4.png b/extras/emotes/public/default/rage4.png new file mode 100755 index 0000000..c726c94 Binary files /dev/null and b/extras/emotes/public/default/rage4.png differ diff --git a/extras/emotes/public/default/railway_car.png b/extras/emotes/public/default/railway_car.png new file mode 100755 index 0000000..2236115 Binary files /dev/null and b/extras/emotes/public/default/railway_car.png differ diff --git a/extras/emotes/public/default/rainbow.png b/extras/emotes/public/default/rainbow.png new file mode 100755 index 0000000..6b1faa0 Binary files /dev/null and b/extras/emotes/public/default/rainbow.png differ diff --git a/extras/emotes/public/default/raised_hand.png b/extras/emotes/public/default/raised_hand.png new file mode 100755 index 0000000..5e45c25 Binary files /dev/null and b/extras/emotes/public/default/raised_hand.png differ diff --git a/extras/emotes/public/default/raised_hands.png b/extras/emotes/public/default/raised_hands.png new file mode 100755 index 0000000..e03142b Binary files /dev/null and b/extras/emotes/public/default/raised_hands.png differ diff --git a/extras/emotes/public/default/raising_hand.png b/extras/emotes/public/default/raising_hand.png new file mode 100755 index 0000000..e1741a4 Binary files /dev/null and b/extras/emotes/public/default/raising_hand.png differ diff --git a/extras/emotes/public/default/ram.png b/extras/emotes/public/default/ram.png new file mode 100755 index 0000000..5ea7bfb Binary files /dev/null and b/extras/emotes/public/default/ram.png differ diff --git a/extras/emotes/public/default/ramen.png b/extras/emotes/public/default/ramen.png new file mode 100755 index 0000000..78dc7d5 Binary files /dev/null and b/extras/emotes/public/default/ramen.png differ diff --git a/extras/emotes/public/default/rat.png b/extras/emotes/public/default/rat.png new file mode 100755 index 0000000..fa7dd40 Binary files /dev/null and b/extras/emotes/public/default/rat.png differ diff --git a/extras/emotes/public/default/recycle.png b/extras/emotes/public/default/recycle.png new file mode 100755 index 0000000..99104c0 Binary files /dev/null and b/extras/emotes/public/default/recycle.png differ diff --git a/extras/emotes/public/default/red_car.png b/extras/emotes/public/default/red_car.png new file mode 100755 index 0000000..d70a2f0 Binary files /dev/null and b/extras/emotes/public/default/red_car.png differ diff --git a/extras/emotes/public/default/red_circle.png b/extras/emotes/public/default/red_circle.png new file mode 100755 index 0000000..b391289 Binary files /dev/null and b/extras/emotes/public/default/red_circle.png differ diff --git a/extras/emotes/public/default/registered.png b/extras/emotes/public/default/registered.png new file mode 100755 index 0000000..31c68a8 Binary files /dev/null and b/extras/emotes/public/default/registered.png differ diff --git a/extras/emotes/public/default/relaxed.png b/extras/emotes/public/default/relaxed.png new file mode 100755 index 0000000..bbab82d Binary files /dev/null and b/extras/emotes/public/default/relaxed.png differ diff --git a/extras/emotes/public/default/relieved.png b/extras/emotes/public/default/relieved.png new file mode 100755 index 0000000..fe5629f Binary files /dev/null and b/extras/emotes/public/default/relieved.png differ diff --git a/extras/emotes/public/default/repeat.png b/extras/emotes/public/default/repeat.png new file mode 100755 index 0000000..80113b6 Binary files /dev/null and b/extras/emotes/public/default/repeat.png differ diff --git a/extras/emotes/public/default/repeat_one.png b/extras/emotes/public/default/repeat_one.png new file mode 100755 index 0000000..3c47bcc Binary files /dev/null and b/extras/emotes/public/default/repeat_one.png differ diff --git a/extras/emotes/public/default/restroom.png b/extras/emotes/public/default/restroom.png new file mode 100755 index 0000000..d6c111b Binary files /dev/null and b/extras/emotes/public/default/restroom.png differ diff --git a/extras/emotes/public/default/revolving_hearts.png b/extras/emotes/public/default/revolving_hearts.png new file mode 100755 index 0000000..ea3317c Binary files /dev/null and b/extras/emotes/public/default/revolving_hearts.png differ diff --git a/extras/emotes/public/default/rewind.png b/extras/emotes/public/default/rewind.png new file mode 100755 index 0000000..26289dc Binary files /dev/null and b/extras/emotes/public/default/rewind.png differ diff --git a/extras/emotes/public/default/ribbon.png b/extras/emotes/public/default/ribbon.png new file mode 100755 index 0000000..63ee5ba Binary files /dev/null and b/extras/emotes/public/default/ribbon.png differ diff --git a/extras/emotes/public/default/rice.png b/extras/emotes/public/default/rice.png new file mode 100755 index 0000000..1fd2202 Binary files /dev/null and b/extras/emotes/public/default/rice.png differ diff --git a/extras/emotes/public/default/rice_ball.png b/extras/emotes/public/default/rice_ball.png new file mode 100755 index 0000000..ade7c45 Binary files /dev/null and b/extras/emotes/public/default/rice_ball.png differ diff --git a/extras/emotes/public/default/rice_cracker.png b/extras/emotes/public/default/rice_cracker.png new file mode 100755 index 0000000..954c901 Binary files /dev/null and b/extras/emotes/public/default/rice_cracker.png differ diff --git a/extras/emotes/public/default/rice_scene.png b/extras/emotes/public/default/rice_scene.png new file mode 100755 index 0000000..1436198 Binary files /dev/null and b/extras/emotes/public/default/rice_scene.png differ diff --git a/extras/emotes/public/default/ring.png b/extras/emotes/public/default/ring.png new file mode 100755 index 0000000..8a57fd6 Binary files /dev/null and b/extras/emotes/public/default/ring.png differ diff --git a/extras/emotes/public/default/rocket.png b/extras/emotes/public/default/rocket.png new file mode 100755 index 0000000..783078d Binary files /dev/null and b/extras/emotes/public/default/rocket.png differ diff --git a/extras/emotes/public/default/roller_coaster.png b/extras/emotes/public/default/roller_coaster.png new file mode 100755 index 0000000..9180b98 Binary files /dev/null and b/extras/emotes/public/default/roller_coaster.png differ diff --git a/extras/emotes/public/default/rooster.png b/extras/emotes/public/default/rooster.png new file mode 100755 index 0000000..fab23ad Binary files /dev/null and b/extras/emotes/public/default/rooster.png differ diff --git a/extras/emotes/public/default/rose.png b/extras/emotes/public/default/rose.png new file mode 100755 index 0000000..3479fbc Binary files /dev/null and b/extras/emotes/public/default/rose.png differ diff --git a/extras/emotes/public/default/rotating_light.png b/extras/emotes/public/default/rotating_light.png new file mode 100755 index 0000000..6cf4a77 Binary files /dev/null and b/extras/emotes/public/default/rotating_light.png differ diff --git a/extras/emotes/public/default/round_pushpin.png b/extras/emotes/public/default/round_pushpin.png new file mode 100755 index 0000000..e498e92 Binary files /dev/null and b/extras/emotes/public/default/round_pushpin.png differ diff --git a/extras/emotes/public/default/rowboat.png b/extras/emotes/public/default/rowboat.png new file mode 100755 index 0000000..e370d0f Binary files /dev/null and b/extras/emotes/public/default/rowboat.png differ diff --git a/extras/emotes/public/default/ru.png b/extras/emotes/public/default/ru.png new file mode 100755 index 0000000..55fcf35 Binary files /dev/null and b/extras/emotes/public/default/ru.png differ diff --git a/extras/emotes/public/default/rugby_football.png b/extras/emotes/public/default/rugby_football.png new file mode 100755 index 0000000..f8db67d Binary files /dev/null and b/extras/emotes/public/default/rugby_football.png differ diff --git a/extras/emotes/public/default/runner.png b/extras/emotes/public/default/runner.png new file mode 100755 index 0000000..cb00429 Binary files /dev/null and b/extras/emotes/public/default/runner.png differ diff --git a/extras/emotes/public/default/running.png b/extras/emotes/public/default/running.png new file mode 100755 index 0000000..cb00429 Binary files /dev/null and b/extras/emotes/public/default/running.png differ diff --git a/extras/emotes/public/default/running_shirt_with_sash.png b/extras/emotes/public/default/running_shirt_with_sash.png new file mode 100755 index 0000000..0d68bba Binary files /dev/null and b/extras/emotes/public/default/running_shirt_with_sash.png differ diff --git a/extras/emotes/public/default/sa.png b/extras/emotes/public/default/sa.png new file mode 100755 index 0000000..387f098 Binary files /dev/null and b/extras/emotes/public/default/sa.png differ diff --git a/extras/emotes/public/default/sagittarius.png b/extras/emotes/public/default/sagittarius.png new file mode 100755 index 0000000..8b5435b Binary files /dev/null and b/extras/emotes/public/default/sagittarius.png differ diff --git a/extras/emotes/public/default/sailboat.png b/extras/emotes/public/default/sailboat.png new file mode 100755 index 0000000..ff656dc Binary files /dev/null and b/extras/emotes/public/default/sailboat.png differ diff --git a/extras/emotes/public/default/sake.png b/extras/emotes/public/default/sake.png new file mode 100755 index 0000000..1f69907 Binary files /dev/null and b/extras/emotes/public/default/sake.png differ diff --git a/extras/emotes/public/default/sandal.png b/extras/emotes/public/default/sandal.png new file mode 100755 index 0000000..0bb3f66 Binary files /dev/null and b/extras/emotes/public/default/sandal.png differ diff --git a/extras/emotes/public/default/santa.png b/extras/emotes/public/default/santa.png new file mode 100755 index 0000000..a2240c0 Binary files /dev/null and b/extras/emotes/public/default/santa.png differ diff --git a/extras/emotes/public/default/satellite.png b/extras/emotes/public/default/satellite.png new file mode 100755 index 0000000..3481cc2 Binary files /dev/null and b/extras/emotes/public/default/satellite.png differ diff --git a/extras/emotes/public/default/satisfied.png b/extras/emotes/public/default/satisfied.png new file mode 100755 index 0000000..11c91eb Binary files /dev/null and b/extras/emotes/public/default/satisfied.png differ diff --git a/extras/emotes/public/default/saxophone.png b/extras/emotes/public/default/saxophone.png new file mode 100755 index 0000000..011559a Binary files /dev/null and b/extras/emotes/public/default/saxophone.png differ diff --git a/extras/emotes/public/default/school.png b/extras/emotes/public/default/school.png new file mode 100755 index 0000000..afd922b Binary files /dev/null and b/extras/emotes/public/default/school.png differ diff --git a/extras/emotes/public/default/school_satchel.png b/extras/emotes/public/default/school_satchel.png new file mode 100755 index 0000000..edfb19a Binary files /dev/null and b/extras/emotes/public/default/school_satchel.png differ diff --git a/extras/emotes/public/default/scissors.png b/extras/emotes/public/default/scissors.png new file mode 100755 index 0000000..d99b8ae Binary files /dev/null and b/extras/emotes/public/default/scissors.png differ diff --git a/extras/emotes/public/default/scorpius.png b/extras/emotes/public/default/scorpius.png new file mode 100755 index 0000000..67fcea1 Binary files /dev/null and b/extras/emotes/public/default/scorpius.png differ diff --git a/extras/emotes/public/default/scream.png b/extras/emotes/public/default/scream.png new file mode 100755 index 0000000..9e93c88 Binary files /dev/null and b/extras/emotes/public/default/scream.png differ diff --git a/extras/emotes/public/default/scream_cat.png b/extras/emotes/public/default/scream_cat.png new file mode 100755 index 0000000..d94cd34 Binary files /dev/null and b/extras/emotes/public/default/scream_cat.png differ diff --git a/extras/emotes/public/default/scroll.png b/extras/emotes/public/default/scroll.png new file mode 100755 index 0000000..c5a10e6 Binary files /dev/null and b/extras/emotes/public/default/scroll.png differ diff --git a/extras/emotes/public/default/seat.png b/extras/emotes/public/default/seat.png new file mode 100755 index 0000000..d1cb864 Binary files /dev/null and b/extras/emotes/public/default/seat.png differ diff --git a/extras/emotes/public/default/secret.png b/extras/emotes/public/default/secret.png new file mode 100755 index 0000000..82e383a Binary files /dev/null and b/extras/emotes/public/default/secret.png differ diff --git a/extras/emotes/public/default/see_no_evil.png b/extras/emotes/public/default/see_no_evil.png new file mode 100755 index 0000000..0890a62 Binary files /dev/null and b/extras/emotes/public/default/see_no_evil.png differ diff --git a/extras/emotes/public/default/seedling.png b/extras/emotes/public/default/seedling.png new file mode 100755 index 0000000..2ab0793 Binary files /dev/null and b/extras/emotes/public/default/seedling.png differ diff --git a/extras/emotes/public/default/seven.png b/extras/emotes/public/default/seven.png new file mode 100755 index 0000000..354e89a Binary files /dev/null and b/extras/emotes/public/default/seven.png differ diff --git a/extras/emotes/public/default/shaved_ice.png b/extras/emotes/public/default/shaved_ice.png new file mode 100755 index 0000000..0d0b382 Binary files /dev/null and b/extras/emotes/public/default/shaved_ice.png differ diff --git a/extras/emotes/public/default/sheep.png b/extras/emotes/public/default/sheep.png new file mode 100755 index 0000000..c7277d2 Binary files /dev/null and b/extras/emotes/public/default/sheep.png differ diff --git a/extras/emotes/public/default/shell.png b/extras/emotes/public/default/shell.png new file mode 100755 index 0000000..3145b56 Binary files /dev/null and b/extras/emotes/public/default/shell.png differ diff --git a/extras/emotes/public/default/ship.png b/extras/emotes/public/default/ship.png new file mode 100755 index 0000000..5d2d8b6 Binary files /dev/null and b/extras/emotes/public/default/ship.png differ diff --git a/extras/emotes/public/default/shipit.png b/extras/emotes/public/default/shipit.png new file mode 100755 index 0000000..a58a47f Binary files /dev/null and b/extras/emotes/public/default/shipit.png differ diff --git a/extras/emotes/public/default/shirt.png b/extras/emotes/public/default/shirt.png new file mode 100755 index 0000000..297a6d6 Binary files /dev/null and b/extras/emotes/public/default/shirt.png differ diff --git a/extras/emotes/public/default/shit.png b/extras/emotes/public/default/shit.png new file mode 100755 index 0000000..73a4dc8 Binary files /dev/null and b/extras/emotes/public/default/shit.png differ diff --git a/extras/emotes/public/default/shoe.png b/extras/emotes/public/default/shoe.png new file mode 100755 index 0000000..45b82e6 Binary files /dev/null and b/extras/emotes/public/default/shoe.png differ diff --git a/extras/emotes/public/default/shower.png b/extras/emotes/public/default/shower.png new file mode 100755 index 0000000..0d72ab8 Binary files /dev/null and b/extras/emotes/public/default/shower.png differ diff --git a/extras/emotes/public/default/signal_strength.png b/extras/emotes/public/default/signal_strength.png new file mode 100755 index 0000000..a4bd23e Binary files /dev/null and b/extras/emotes/public/default/signal_strength.png differ diff --git a/extras/emotes/public/default/six.png b/extras/emotes/public/default/six.png new file mode 100755 index 0000000..5688055 Binary files /dev/null and b/extras/emotes/public/default/six.png differ diff --git a/extras/emotes/public/default/six_pointed_star.png b/extras/emotes/public/default/six_pointed_star.png new file mode 100755 index 0000000..c11af14 Binary files /dev/null and b/extras/emotes/public/default/six_pointed_star.png differ diff --git a/extras/emotes/public/default/ski.png b/extras/emotes/public/default/ski.png new file mode 100755 index 0000000..98f5cb0 Binary files /dev/null and b/extras/emotes/public/default/ski.png differ diff --git a/extras/emotes/public/default/skull.png b/extras/emotes/public/default/skull.png new file mode 100755 index 0000000..bd4ee38 Binary files /dev/null and b/extras/emotes/public/default/skull.png differ diff --git a/extras/emotes/public/default/sleeping.png b/extras/emotes/public/default/sleeping.png new file mode 100755 index 0000000..093b852 Binary files /dev/null and b/extras/emotes/public/default/sleeping.png differ diff --git a/extras/emotes/public/default/sleepy.png b/extras/emotes/public/default/sleepy.png new file mode 100755 index 0000000..df4f55e Binary files /dev/null and b/extras/emotes/public/default/sleepy.png differ diff --git a/extras/emotes/public/default/slot_machine.png b/extras/emotes/public/default/slot_machine.png new file mode 100755 index 0000000..26f1148 Binary files /dev/null and b/extras/emotes/public/default/slot_machine.png differ diff --git a/extras/emotes/public/default/small_blue_diamond.png b/extras/emotes/public/default/small_blue_diamond.png new file mode 100755 index 0000000..8cd4920 Binary files /dev/null and b/extras/emotes/public/default/small_blue_diamond.png differ diff --git a/extras/emotes/public/default/small_orange_diamond.png b/extras/emotes/public/default/small_orange_diamond.png new file mode 100755 index 0000000..04941d3 Binary files /dev/null and b/extras/emotes/public/default/small_orange_diamond.png differ diff --git a/extras/emotes/public/default/small_red_triangle.png b/extras/emotes/public/default/small_red_triangle.png new file mode 100755 index 0000000..8c4428d Binary files /dev/null and b/extras/emotes/public/default/small_red_triangle.png differ diff --git a/extras/emotes/public/default/small_red_triangle_down.png b/extras/emotes/public/default/small_red_triangle_down.png new file mode 100755 index 0000000..94832f0 Binary files /dev/null and b/extras/emotes/public/default/small_red_triangle_down.png differ diff --git a/extras/emotes/public/default/smile.png b/extras/emotes/public/default/smile.png new file mode 100755 index 0000000..81a8396 Binary files /dev/null and b/extras/emotes/public/default/smile.png differ diff --git a/extras/emotes/public/default/smile_cat.png b/extras/emotes/public/default/smile_cat.png new file mode 100755 index 0000000..ad333ba Binary files /dev/null and b/extras/emotes/public/default/smile_cat.png differ diff --git a/extras/emotes/public/default/smiley.png b/extras/emotes/public/default/smiley.png new file mode 100755 index 0000000..77b581d Binary files /dev/null and b/extras/emotes/public/default/smiley.png differ diff --git a/extras/emotes/public/default/smiley_cat.png b/extras/emotes/public/default/smiley_cat.png new file mode 100755 index 0000000..dbf1b02 Binary files /dev/null and b/extras/emotes/public/default/smiley_cat.png differ diff --git a/extras/emotes/public/default/smiling_imp.png b/extras/emotes/public/default/smiling_imp.png new file mode 100755 index 0000000..d904049 Binary files /dev/null and b/extras/emotes/public/default/smiling_imp.png differ diff --git a/extras/emotes/public/default/smirk.png b/extras/emotes/public/default/smirk.png new file mode 100755 index 0000000..bc6e508 Binary files /dev/null and b/extras/emotes/public/default/smirk.png differ diff --git a/extras/emotes/public/default/smirk_cat.png b/extras/emotes/public/default/smirk_cat.png new file mode 100755 index 0000000..351565e Binary files /dev/null and b/extras/emotes/public/default/smirk_cat.png differ diff --git a/extras/emotes/public/default/smoking.png b/extras/emotes/public/default/smoking.png new file mode 100755 index 0000000..4aad6cb Binary files /dev/null and b/extras/emotes/public/default/smoking.png differ diff --git a/extras/emotes/public/default/snail.png b/extras/emotes/public/default/snail.png new file mode 100755 index 0000000..e75e69a Binary files /dev/null and b/extras/emotes/public/default/snail.png differ diff --git a/extras/emotes/public/default/snake.png b/extras/emotes/public/default/snake.png new file mode 100755 index 0000000..ef58933 Binary files /dev/null and b/extras/emotes/public/default/snake.png differ diff --git a/extras/emotes/public/default/snowboarder.png b/extras/emotes/public/default/snowboarder.png new file mode 100755 index 0000000..aeda5c8 Binary files /dev/null and b/extras/emotes/public/default/snowboarder.png differ diff --git a/extras/emotes/public/default/snowflake.png b/extras/emotes/public/default/snowflake.png new file mode 100755 index 0000000..54b68ff Binary files /dev/null and b/extras/emotes/public/default/snowflake.png differ diff --git a/extras/emotes/public/default/snowman.png b/extras/emotes/public/default/snowman.png new file mode 100755 index 0000000..a97902e Binary files /dev/null and b/extras/emotes/public/default/snowman.png differ diff --git a/extras/emotes/public/default/sob.png b/extras/emotes/public/default/sob.png new file mode 100755 index 0000000..1561df9 Binary files /dev/null and b/extras/emotes/public/default/sob.png differ diff --git a/extras/emotes/public/default/soccer.png b/extras/emotes/public/default/soccer.png new file mode 100755 index 0000000..1e118b5 Binary files /dev/null and b/extras/emotes/public/default/soccer.png differ diff --git a/extras/emotes/public/default/soon.png b/extras/emotes/public/default/soon.png new file mode 100755 index 0000000..2cf46df Binary files /dev/null and b/extras/emotes/public/default/soon.png differ diff --git a/extras/emotes/public/default/sos.png b/extras/emotes/public/default/sos.png new file mode 100755 index 0000000..e3e16ef Binary files /dev/null and b/extras/emotes/public/default/sos.png differ diff --git a/extras/emotes/public/default/sound.png b/extras/emotes/public/default/sound.png new file mode 100755 index 0000000..6aa4dbf Binary files /dev/null and b/extras/emotes/public/default/sound.png differ diff --git a/extras/emotes/public/default/space_invader.png b/extras/emotes/public/default/space_invader.png new file mode 100755 index 0000000..3840491 Binary files /dev/null and b/extras/emotes/public/default/space_invader.png differ diff --git a/extras/emotes/public/default/spades.png b/extras/emotes/public/default/spades.png new file mode 100755 index 0000000..133a1ab Binary files /dev/null and b/extras/emotes/public/default/spades.png differ diff --git a/extras/emotes/public/default/spaghetti.png b/extras/emotes/public/default/spaghetti.png new file mode 100755 index 0000000..08de243 Binary files /dev/null and b/extras/emotes/public/default/spaghetti.png differ diff --git a/extras/emotes/public/default/sparkle.png b/extras/emotes/public/default/sparkle.png new file mode 100755 index 0000000..23a68ce Binary files /dev/null and b/extras/emotes/public/default/sparkle.png differ diff --git a/extras/emotes/public/default/sparkler.png b/extras/emotes/public/default/sparkler.png new file mode 100755 index 0000000..4aabd7e Binary files /dev/null and b/extras/emotes/public/default/sparkler.png differ diff --git a/extras/emotes/public/default/sparkles.png b/extras/emotes/public/default/sparkles.png new file mode 100755 index 0000000..51307bc Binary files /dev/null and b/extras/emotes/public/default/sparkles.png differ diff --git a/extras/emotes/public/default/sparkling_heart.png b/extras/emotes/public/default/sparkling_heart.png new file mode 100755 index 0000000..64ac066 Binary files /dev/null and b/extras/emotes/public/default/sparkling_heart.png differ diff --git a/extras/emotes/public/default/speak_no_evil.png b/extras/emotes/public/default/speak_no_evil.png new file mode 100755 index 0000000..87944c4 Binary files /dev/null and b/extras/emotes/public/default/speak_no_evil.png differ diff --git a/extras/emotes/public/default/speaker.png b/extras/emotes/public/default/speaker.png new file mode 100755 index 0000000..470476e Binary files /dev/null and b/extras/emotes/public/default/speaker.png differ diff --git a/extras/emotes/public/default/speech_balloon.png b/extras/emotes/public/default/speech_balloon.png new file mode 100755 index 0000000..2896c27 Binary files /dev/null and b/extras/emotes/public/default/speech_balloon.png differ diff --git a/extras/emotes/public/default/speedboat.png b/extras/emotes/public/default/speedboat.png new file mode 100755 index 0000000..da6689b Binary files /dev/null and b/extras/emotes/public/default/speedboat.png differ diff --git a/extras/emotes/public/default/squirrel.png b/extras/emotes/public/default/squirrel.png new file mode 100755 index 0000000..a58a47f Binary files /dev/null and b/extras/emotes/public/default/squirrel.png differ diff --git a/extras/emotes/public/default/star.png b/extras/emotes/public/default/star.png new file mode 100755 index 0000000..1bfddc8 Binary files /dev/null and b/extras/emotes/public/default/star.png differ diff --git a/extras/emotes/public/default/star2.png b/extras/emotes/public/default/star2.png new file mode 100755 index 0000000..8b40ff4 Binary files /dev/null and b/extras/emotes/public/default/star2.png differ diff --git a/extras/emotes/public/default/stars.png b/extras/emotes/public/default/stars.png new file mode 100755 index 0000000..097a842 Binary files /dev/null and b/extras/emotes/public/default/stars.png differ diff --git a/extras/emotes/public/default/station.png b/extras/emotes/public/default/station.png new file mode 100755 index 0000000..e77daa8 Binary files /dev/null and b/extras/emotes/public/default/station.png differ diff --git a/extras/emotes/public/default/statue_of_liberty.png b/extras/emotes/public/default/statue_of_liberty.png new file mode 100755 index 0000000..9ad9028 Binary files /dev/null and b/extras/emotes/public/default/statue_of_liberty.png differ diff --git a/extras/emotes/public/default/steam_locomotive.png b/extras/emotes/public/default/steam_locomotive.png new file mode 100755 index 0000000..5495077 Binary files /dev/null and b/extras/emotes/public/default/steam_locomotive.png differ diff --git a/extras/emotes/public/default/stew.png b/extras/emotes/public/default/stew.png new file mode 100755 index 0000000..e9687f9 Binary files /dev/null and b/extras/emotes/public/default/stew.png differ diff --git a/extras/emotes/public/default/straight_ruler.png b/extras/emotes/public/default/straight_ruler.png new file mode 100755 index 0000000..d96658e Binary files /dev/null and b/extras/emotes/public/default/straight_ruler.png differ diff --git a/extras/emotes/public/default/strawberry.png b/extras/emotes/public/default/strawberry.png new file mode 100755 index 0000000..13eb827 Binary files /dev/null and b/extras/emotes/public/default/strawberry.png differ diff --git a/extras/emotes/public/default/stuck_out_tongue.png b/extras/emotes/public/default/stuck_out_tongue.png new file mode 100755 index 0000000..fa7b58e Binary files /dev/null and b/extras/emotes/public/default/stuck_out_tongue.png differ diff --git a/extras/emotes/public/default/stuck_out_tongue_closed_eyes.png b/extras/emotes/public/default/stuck_out_tongue_closed_eyes.png new file mode 100755 index 0000000..333716e Binary files /dev/null and b/extras/emotes/public/default/stuck_out_tongue_closed_eyes.png differ diff --git a/extras/emotes/public/default/stuck_out_tongue_winking_eye.png b/extras/emotes/public/default/stuck_out_tongue_winking_eye.png new file mode 100755 index 0000000..6ae9d49 Binary files /dev/null and b/extras/emotes/public/default/stuck_out_tongue_winking_eye.png differ diff --git a/extras/emotes/public/default/sun_with_face.png b/extras/emotes/public/default/sun_with_face.png new file mode 100755 index 0000000..ee27663 Binary files /dev/null and b/extras/emotes/public/default/sun_with_face.png differ diff --git a/extras/emotes/public/default/sunflower.png b/extras/emotes/public/default/sunflower.png new file mode 100755 index 0000000..d9bad19 Binary files /dev/null and b/extras/emotes/public/default/sunflower.png differ diff --git a/extras/emotes/public/default/sunglasses.png b/extras/emotes/public/default/sunglasses.png new file mode 100755 index 0000000..f2e5247 Binary files /dev/null and b/extras/emotes/public/default/sunglasses.png differ diff --git a/extras/emotes/public/default/sunny.png b/extras/emotes/public/default/sunny.png new file mode 100755 index 0000000..d23c095 Binary files /dev/null and b/extras/emotes/public/default/sunny.png differ diff --git a/extras/emotes/public/default/sunrise.png b/extras/emotes/public/default/sunrise.png new file mode 100755 index 0000000..ec58dcc Binary files /dev/null and b/extras/emotes/public/default/sunrise.png differ diff --git a/extras/emotes/public/default/sunrise_over_mountains.png b/extras/emotes/public/default/sunrise_over_mountains.png new file mode 100755 index 0000000..ebc3db1 Binary files /dev/null and b/extras/emotes/public/default/sunrise_over_mountains.png differ diff --git a/extras/emotes/public/default/surfer.png b/extras/emotes/public/default/surfer.png new file mode 100755 index 0000000..b067e8c Binary files /dev/null and b/extras/emotes/public/default/surfer.png differ diff --git a/extras/emotes/public/default/sushi.png b/extras/emotes/public/default/sushi.png new file mode 100755 index 0000000..0d179bd Binary files /dev/null and b/extras/emotes/public/default/sushi.png differ diff --git a/extras/emotes/public/default/suspect.png b/extras/emotes/public/default/suspect.png new file mode 100755 index 0000000..58e8921 Binary files /dev/null and b/extras/emotes/public/default/suspect.png differ diff --git a/extras/emotes/public/default/suspension_railway.png b/extras/emotes/public/default/suspension_railway.png new file mode 100755 index 0000000..aaa45f6 Binary files /dev/null and b/extras/emotes/public/default/suspension_railway.png differ diff --git a/extras/emotes/public/default/sweat.png b/extras/emotes/public/default/sweat.png new file mode 100755 index 0000000..e894b76 Binary files /dev/null and b/extras/emotes/public/default/sweat.png differ diff --git a/extras/emotes/public/default/sweat_drops.png b/extras/emotes/public/default/sweat_drops.png new file mode 100755 index 0000000..a83b3e9 Binary files /dev/null and b/extras/emotes/public/default/sweat_drops.png differ diff --git a/extras/emotes/public/default/sweat_smile.png b/extras/emotes/public/default/sweat_smile.png new file mode 100755 index 0000000..3903f71 Binary files /dev/null and b/extras/emotes/public/default/sweat_smile.png differ diff --git a/extras/emotes/public/default/sweet_potato.png b/extras/emotes/public/default/sweet_potato.png new file mode 100755 index 0000000..cde7880 Binary files /dev/null and b/extras/emotes/public/default/sweet_potato.png differ diff --git a/extras/emotes/public/default/swimmer.png b/extras/emotes/public/default/swimmer.png new file mode 100755 index 0000000..d3878a0 Binary files /dev/null and b/extras/emotes/public/default/swimmer.png differ diff --git a/extras/emotes/public/default/symbols.png b/extras/emotes/public/default/symbols.png new file mode 100755 index 0000000..16bc1da Binary files /dev/null and b/extras/emotes/public/default/symbols.png differ diff --git a/extras/emotes/public/default/syringe.png b/extras/emotes/public/default/syringe.png new file mode 100755 index 0000000..36aa8fe Binary files /dev/null and b/extras/emotes/public/default/syringe.png differ diff --git a/extras/emotes/public/default/tada.png b/extras/emotes/public/default/tada.png new file mode 100755 index 0000000..7411b52 Binary files /dev/null and b/extras/emotes/public/default/tada.png differ diff --git a/extras/emotes/public/default/tanabata_tree.png b/extras/emotes/public/default/tanabata_tree.png new file mode 100755 index 0000000..6dea4b2 Binary files /dev/null and b/extras/emotes/public/default/tanabata_tree.png differ diff --git a/extras/emotes/public/default/tangerine.png b/extras/emotes/public/default/tangerine.png new file mode 100755 index 0000000..fc9d4f8 Binary files /dev/null and b/extras/emotes/public/default/tangerine.png differ diff --git a/extras/emotes/public/default/taurus.png b/extras/emotes/public/default/taurus.png new file mode 100755 index 0000000..6af582f Binary files /dev/null and b/extras/emotes/public/default/taurus.png differ diff --git a/extras/emotes/public/default/taxi.png b/extras/emotes/public/default/taxi.png new file mode 100755 index 0000000..60a50d3 Binary files /dev/null and b/extras/emotes/public/default/taxi.png differ diff --git a/extras/emotes/public/default/tea.png b/extras/emotes/public/default/tea.png new file mode 100755 index 0000000..3ece0b7 Binary files /dev/null and b/extras/emotes/public/default/tea.png differ diff --git a/extras/emotes/public/default/telephone.png b/extras/emotes/public/default/telephone.png new file mode 100755 index 0000000..87d2559 Binary files /dev/null and b/extras/emotes/public/default/telephone.png differ diff --git a/extras/emotes/public/default/telephone_receiver.png b/extras/emotes/public/default/telephone_receiver.png new file mode 100755 index 0000000..36e21e0 Binary files /dev/null and b/extras/emotes/public/default/telephone_receiver.png differ diff --git a/extras/emotes/public/default/telescope.png b/extras/emotes/public/default/telescope.png new file mode 100755 index 0000000..98e5755 Binary files /dev/null and b/extras/emotes/public/default/telescope.png differ diff --git a/extras/emotes/public/default/tennis.png b/extras/emotes/public/default/tennis.png new file mode 100755 index 0000000..278d904 Binary files /dev/null and b/extras/emotes/public/default/tennis.png differ diff --git a/extras/emotes/public/default/tent.png b/extras/emotes/public/default/tent.png new file mode 100755 index 0000000..5c0d20e Binary files /dev/null and b/extras/emotes/public/default/tent.png differ diff --git a/extras/emotes/public/default/thought_balloon.png b/extras/emotes/public/default/thought_balloon.png new file mode 100755 index 0000000..febe30d Binary files /dev/null and b/extras/emotes/public/default/thought_balloon.png differ diff --git a/extras/emotes/public/default/three.png b/extras/emotes/public/default/three.png new file mode 100755 index 0000000..55644c9 Binary files /dev/null and b/extras/emotes/public/default/three.png differ diff --git a/extras/emotes/public/default/thumbsdown.png b/extras/emotes/public/default/thumbsdown.png new file mode 100755 index 0000000..41c6b82 Binary files /dev/null and b/extras/emotes/public/default/thumbsdown.png differ diff --git a/extras/emotes/public/default/thumbsup.png b/extras/emotes/public/default/thumbsup.png new file mode 100755 index 0000000..81786c1 Binary files /dev/null and b/extras/emotes/public/default/thumbsup.png differ diff --git a/extras/emotes/public/default/ticket.png b/extras/emotes/public/default/ticket.png new file mode 100755 index 0000000..cdacf1a Binary files /dev/null and b/extras/emotes/public/default/ticket.png differ diff --git a/extras/emotes/public/default/tiger.png b/extras/emotes/public/default/tiger.png new file mode 100755 index 0000000..d6cc84a Binary files /dev/null and b/extras/emotes/public/default/tiger.png differ diff --git a/extras/emotes/public/default/tiger2.png b/extras/emotes/public/default/tiger2.png new file mode 100755 index 0000000..b0c7d8d Binary files /dev/null and b/extras/emotes/public/default/tiger2.png differ diff --git a/extras/emotes/public/default/tired_face.png b/extras/emotes/public/default/tired_face.png new file mode 100755 index 0000000..77b7834 Binary files /dev/null and b/extras/emotes/public/default/tired_face.png differ diff --git a/extras/emotes/public/default/tm.png b/extras/emotes/public/default/tm.png new file mode 100755 index 0000000..c7dec75 Binary files /dev/null and b/extras/emotes/public/default/tm.png differ diff --git a/extras/emotes/public/default/toilet.png b/extras/emotes/public/default/toilet.png new file mode 100755 index 0000000..e5cc411 Binary files /dev/null and b/extras/emotes/public/default/toilet.png differ diff --git a/extras/emotes/public/default/tokyo_tower.png b/extras/emotes/public/default/tokyo_tower.png new file mode 100755 index 0000000..e1cbd7a Binary files /dev/null and b/extras/emotes/public/default/tokyo_tower.png differ diff --git a/extras/emotes/public/default/tomato.png b/extras/emotes/public/default/tomato.png new file mode 100755 index 0000000..a129700 Binary files /dev/null and b/extras/emotes/public/default/tomato.png differ diff --git a/extras/emotes/public/default/tongue.png b/extras/emotes/public/default/tongue.png new file mode 100755 index 0000000..b0bab12 Binary files /dev/null and b/extras/emotes/public/default/tongue.png differ diff --git a/extras/emotes/public/default/top.png b/extras/emotes/public/default/top.png new file mode 100755 index 0000000..5aa4dd4 Binary files /dev/null and b/extras/emotes/public/default/top.png differ diff --git a/extras/emotes/public/default/tophat.png b/extras/emotes/public/default/tophat.png new file mode 100755 index 0000000..7d27134 Binary files /dev/null and b/extras/emotes/public/default/tophat.png differ diff --git a/extras/emotes/public/default/tractor.png b/extras/emotes/public/default/tractor.png new file mode 100755 index 0000000..058fd3e Binary files /dev/null and b/extras/emotes/public/default/tractor.png differ diff --git a/extras/emotes/public/default/traffic_light.png b/extras/emotes/public/default/traffic_light.png new file mode 100755 index 0000000..1facb27 Binary files /dev/null and b/extras/emotes/public/default/traffic_light.png differ diff --git a/extras/emotes/public/default/train.png b/extras/emotes/public/default/train.png new file mode 100755 index 0000000..3202d80 Binary files /dev/null and b/extras/emotes/public/default/train.png differ diff --git a/extras/emotes/public/default/train2.png b/extras/emotes/public/default/train2.png new file mode 100755 index 0000000..9c0d3ab Binary files /dev/null and b/extras/emotes/public/default/train2.png differ diff --git a/extras/emotes/public/default/tram.png b/extras/emotes/public/default/tram.png new file mode 100755 index 0000000..5eb29fb Binary files /dev/null and b/extras/emotes/public/default/tram.png differ diff --git a/extras/emotes/public/default/triangular_flag_on_post.png b/extras/emotes/public/default/triangular_flag_on_post.png new file mode 100755 index 0000000..f9a3f32 Binary files /dev/null and b/extras/emotes/public/default/triangular_flag_on_post.png differ diff --git a/extras/emotes/public/default/triangular_ruler.png b/extras/emotes/public/default/triangular_ruler.png new file mode 100755 index 0000000..383677c Binary files /dev/null and b/extras/emotes/public/default/triangular_ruler.png differ diff --git a/extras/emotes/public/default/trident.png b/extras/emotes/public/default/trident.png new file mode 100755 index 0000000..d79a7b4 Binary files /dev/null and b/extras/emotes/public/default/trident.png differ diff --git a/extras/emotes/public/default/triumph.png b/extras/emotes/public/default/triumph.png new file mode 100755 index 0000000..92f93bd Binary files /dev/null and b/extras/emotes/public/default/triumph.png differ diff --git a/extras/emotes/public/default/trolleybus.png b/extras/emotes/public/default/trolleybus.png new file mode 100755 index 0000000..b9740a5 Binary files /dev/null and b/extras/emotes/public/default/trolleybus.png differ diff --git a/extras/emotes/public/default/trollface.png b/extras/emotes/public/default/trollface.png new file mode 100755 index 0000000..119d77e Binary files /dev/null and b/extras/emotes/public/default/trollface.png differ diff --git a/extras/emotes/public/default/trophy.png b/extras/emotes/public/default/trophy.png new file mode 100755 index 0000000..95d3b63 Binary files /dev/null and b/extras/emotes/public/default/trophy.png differ diff --git a/extras/emotes/public/default/tropical_drink.png b/extras/emotes/public/default/tropical_drink.png new file mode 100755 index 0000000..55ca9ee Binary files /dev/null and b/extras/emotes/public/default/tropical_drink.png differ diff --git a/extras/emotes/public/default/tropical_fish.png b/extras/emotes/public/default/tropical_fish.png new file mode 100755 index 0000000..a6d7349 Binary files /dev/null and b/extras/emotes/public/default/tropical_fish.png differ diff --git a/extras/emotes/public/default/truck.png b/extras/emotes/public/default/truck.png new file mode 100755 index 0000000..3f25ba1 Binary files /dev/null and b/extras/emotes/public/default/truck.png differ diff --git a/extras/emotes/public/default/trumpet.png b/extras/emotes/public/default/trumpet.png new file mode 100755 index 0000000..c84cfb1 Binary files /dev/null and b/extras/emotes/public/default/trumpet.png differ diff --git a/extras/emotes/public/default/tshirt.png b/extras/emotes/public/default/tshirt.png new file mode 100755 index 0000000..297a6d6 Binary files /dev/null and b/extras/emotes/public/default/tshirt.png differ diff --git a/extras/emotes/public/default/tulip.png b/extras/emotes/public/default/tulip.png new file mode 100755 index 0000000..b3ee110 Binary files /dev/null and b/extras/emotes/public/default/tulip.png differ diff --git a/extras/emotes/public/default/turtle.png b/extras/emotes/public/default/turtle.png new file mode 100755 index 0000000..04d1d96 Binary files /dev/null and b/extras/emotes/public/default/turtle.png differ diff --git a/extras/emotes/public/default/tv.png b/extras/emotes/public/default/tv.png new file mode 100755 index 0000000..803dc3d Binary files /dev/null and b/extras/emotes/public/default/tv.png differ diff --git a/extras/emotes/public/default/twisted_rightwards_arrows.png b/extras/emotes/public/default/twisted_rightwards_arrows.png new file mode 100755 index 0000000..25cde18 Binary files /dev/null and b/extras/emotes/public/default/twisted_rightwards_arrows.png differ diff --git a/extras/emotes/public/default/two.png b/extras/emotes/public/default/two.png new file mode 100755 index 0000000..c191f8a Binary files /dev/null and b/extras/emotes/public/default/two.png differ diff --git a/extras/emotes/public/default/two_hearts.png b/extras/emotes/public/default/two_hearts.png new file mode 100755 index 0000000..b189e9a Binary files /dev/null and b/extras/emotes/public/default/two_hearts.png differ diff --git a/extras/emotes/public/default/two_men_holding_hands.png b/extras/emotes/public/default/two_men_holding_hands.png new file mode 100755 index 0000000..d1099f2 Binary files /dev/null and b/extras/emotes/public/default/two_men_holding_hands.png differ diff --git a/extras/emotes/public/default/two_women_holding_hands.png b/extras/emotes/public/default/two_women_holding_hands.png new file mode 100755 index 0000000..619646c Binary files /dev/null and b/extras/emotes/public/default/two_women_holding_hands.png differ diff --git a/extras/emotes/public/default/u5272.png b/extras/emotes/public/default/u5272.png new file mode 100755 index 0000000..2148253 Binary files /dev/null and b/extras/emotes/public/default/u5272.png differ diff --git a/extras/emotes/public/default/u5408.png b/extras/emotes/public/default/u5408.png new file mode 100755 index 0000000..03ab0d8 Binary files /dev/null and b/extras/emotes/public/default/u5408.png differ diff --git a/extras/emotes/public/default/u55b6.png b/extras/emotes/public/default/u55b6.png new file mode 100755 index 0000000..ba946d3 Binary files /dev/null and b/extras/emotes/public/default/u55b6.png differ diff --git a/extras/emotes/public/default/u6307.png b/extras/emotes/public/default/u6307.png new file mode 100755 index 0000000..6557f56 Binary files /dev/null and b/extras/emotes/public/default/u6307.png differ diff --git a/extras/emotes/public/default/u6708.png b/extras/emotes/public/default/u6708.png new file mode 100755 index 0000000..e4dfe5a Binary files /dev/null and b/extras/emotes/public/default/u6708.png differ diff --git a/extras/emotes/public/default/u6709.png b/extras/emotes/public/default/u6709.png new file mode 100755 index 0000000..cd8fb3f Binary files /dev/null and b/extras/emotes/public/default/u6709.png differ diff --git a/extras/emotes/public/default/u6e80.png b/extras/emotes/public/default/u6e80.png new file mode 100755 index 0000000..5df1cb8 Binary files /dev/null and b/extras/emotes/public/default/u6e80.png differ diff --git a/extras/emotes/public/default/u7121.png b/extras/emotes/public/default/u7121.png new file mode 100755 index 0000000..25f694e Binary files /dev/null and b/extras/emotes/public/default/u7121.png differ diff --git a/extras/emotes/public/default/u7533.png b/extras/emotes/public/default/u7533.png new file mode 100755 index 0000000..fc4a990 Binary files /dev/null and b/extras/emotes/public/default/u7533.png differ diff --git a/extras/emotes/public/default/u7981.png b/extras/emotes/public/default/u7981.png new file mode 100755 index 0000000..f550a57 Binary files /dev/null and b/extras/emotes/public/default/u7981.png differ diff --git a/extras/emotes/public/default/u7a7a.png b/extras/emotes/public/default/u7a7a.png new file mode 100755 index 0000000..c05f5cf Binary files /dev/null and b/extras/emotes/public/default/u7a7a.png differ diff --git a/extras/emotes/public/default/uk.png b/extras/emotes/public/default/uk.png new file mode 100755 index 0000000..2a62c7a Binary files /dev/null and b/extras/emotes/public/default/uk.png differ diff --git a/extras/emotes/public/default/umbrella.png b/extras/emotes/public/default/umbrella.png new file mode 100755 index 0000000..1db722f Binary files /dev/null and b/extras/emotes/public/default/umbrella.png differ diff --git a/extras/emotes/public/default/unamused.png b/extras/emotes/public/default/unamused.png new file mode 100755 index 0000000..3722e6f Binary files /dev/null and b/extras/emotes/public/default/unamused.png differ diff --git a/extras/emotes/public/default/underage.png b/extras/emotes/public/default/underage.png new file mode 100755 index 0000000..a789b3c Binary files /dev/null and b/extras/emotes/public/default/underage.png differ diff --git a/extras/emotes/public/default/unlock.png b/extras/emotes/public/default/unlock.png new file mode 100755 index 0000000..22b429c Binary files /dev/null and b/extras/emotes/public/default/unlock.png differ diff --git a/extras/emotes/public/default/up.png b/extras/emotes/public/default/up.png new file mode 100755 index 0000000..829219a Binary files /dev/null and b/extras/emotes/public/default/up.png differ diff --git a/extras/emotes/public/default/us.png b/extras/emotes/public/default/us.png new file mode 100755 index 0000000..3813766 Binary files /dev/null and b/extras/emotes/public/default/us.png differ diff --git a/extras/emotes/public/default/v.png b/extras/emotes/public/default/v.png new file mode 100755 index 0000000..f61267c Binary files /dev/null and b/extras/emotes/public/default/v.png differ diff --git a/extras/emotes/public/default/vertical_traffic_light.png b/extras/emotes/public/default/vertical_traffic_light.png new file mode 100755 index 0000000..7a5ba35 Binary files /dev/null and b/extras/emotes/public/default/vertical_traffic_light.png differ diff --git a/extras/emotes/public/default/vhs.png b/extras/emotes/public/default/vhs.png new file mode 100755 index 0000000..881081c Binary files /dev/null and b/extras/emotes/public/default/vhs.png differ diff --git a/extras/emotes/public/default/vibration_mode.png b/extras/emotes/public/default/vibration_mode.png new file mode 100755 index 0000000..a716e96 Binary files /dev/null and b/extras/emotes/public/default/vibration_mode.png differ diff --git a/extras/emotes/public/default/video_camera.png b/extras/emotes/public/default/video_camera.png new file mode 100755 index 0000000..274cecd Binary files /dev/null and b/extras/emotes/public/default/video_camera.png differ diff --git a/extras/emotes/public/default/video_game.png b/extras/emotes/public/default/video_game.png new file mode 100755 index 0000000..e265a3b Binary files /dev/null and b/extras/emotes/public/default/video_game.png differ diff --git a/extras/emotes/public/default/violin.png b/extras/emotes/public/default/violin.png new file mode 100755 index 0000000..69347b5 Binary files /dev/null and b/extras/emotes/public/default/violin.png differ diff --git a/extras/emotes/public/default/virgo.png b/extras/emotes/public/default/virgo.png new file mode 100755 index 0000000..72e1763 Binary files /dev/null and b/extras/emotes/public/default/virgo.png differ diff --git a/extras/emotes/public/default/volcano.png b/extras/emotes/public/default/volcano.png new file mode 100755 index 0000000..9b43453 Binary files /dev/null and b/extras/emotes/public/default/volcano.png differ diff --git a/extras/emotes/public/default/vs.png b/extras/emotes/public/default/vs.png new file mode 100755 index 0000000..8636388 Binary files /dev/null and b/extras/emotes/public/default/vs.png differ diff --git a/extras/emotes/public/default/walking.png b/extras/emotes/public/default/walking.png new file mode 100755 index 0000000..52bc038 Binary files /dev/null and b/extras/emotes/public/default/walking.png differ diff --git a/extras/emotes/public/default/waning_crescent_moon.png b/extras/emotes/public/default/waning_crescent_moon.png new file mode 100755 index 0000000..3038778 Binary files /dev/null and b/extras/emotes/public/default/waning_crescent_moon.png differ diff --git a/extras/emotes/public/default/waning_gibbous_moon.png b/extras/emotes/public/default/waning_gibbous_moon.png new file mode 100755 index 0000000..5100990 Binary files /dev/null and b/extras/emotes/public/default/waning_gibbous_moon.png differ diff --git a/extras/emotes/public/default/warning.png b/extras/emotes/public/default/warning.png new file mode 100755 index 0000000..db6f96f Binary files /dev/null and b/extras/emotes/public/default/warning.png differ diff --git a/extras/emotes/public/default/watch.png b/extras/emotes/public/default/watch.png new file mode 100755 index 0000000..d503bb8 Binary files /dev/null and b/extras/emotes/public/default/watch.png differ diff --git a/extras/emotes/public/default/water_buffalo.png b/extras/emotes/public/default/water_buffalo.png new file mode 100755 index 0000000..3bcde3e Binary files /dev/null and b/extras/emotes/public/default/water_buffalo.png differ diff --git a/extras/emotes/public/default/watermelon.png b/extras/emotes/public/default/watermelon.png new file mode 100755 index 0000000..fc212be Binary files /dev/null and b/extras/emotes/public/default/watermelon.png differ diff --git a/extras/emotes/public/default/wave.png b/extras/emotes/public/default/wave.png new file mode 100755 index 0000000..56e6e82 Binary files /dev/null and b/extras/emotes/public/default/wave.png differ diff --git a/extras/emotes/public/default/wavy_dash.png b/extras/emotes/public/default/wavy_dash.png new file mode 100755 index 0000000..5a74e5c Binary files /dev/null and b/extras/emotes/public/default/wavy_dash.png differ diff --git a/extras/emotes/public/default/waxing_crescent_moon.png b/extras/emotes/public/default/waxing_crescent_moon.png new file mode 100755 index 0000000..c8f13dd Binary files /dev/null and b/extras/emotes/public/default/waxing_crescent_moon.png differ diff --git a/extras/emotes/public/default/waxing_gibbous_moon.png b/extras/emotes/public/default/waxing_gibbous_moon.png new file mode 100755 index 0000000..54e7ec6 Binary files /dev/null and b/extras/emotes/public/default/waxing_gibbous_moon.png differ diff --git a/extras/emotes/public/default/wc.png b/extras/emotes/public/default/wc.png new file mode 100755 index 0000000..dfe84d2 Binary files /dev/null and b/extras/emotes/public/default/wc.png differ diff --git a/extras/emotes/public/default/weary.png b/extras/emotes/public/default/weary.png new file mode 100755 index 0000000..0c54754 Binary files /dev/null and b/extras/emotes/public/default/weary.png differ diff --git a/extras/emotes/public/default/wedding.png b/extras/emotes/public/default/wedding.png new file mode 100755 index 0000000..ead19d5 Binary files /dev/null and b/extras/emotes/public/default/wedding.png differ diff --git a/extras/emotes/public/default/whale.png b/extras/emotes/public/default/whale.png new file mode 100755 index 0000000..5bb113e Binary files /dev/null and b/extras/emotes/public/default/whale.png differ diff --git a/extras/emotes/public/default/whale2.png b/extras/emotes/public/default/whale2.png new file mode 100755 index 0000000..0ef4ea9 Binary files /dev/null and b/extras/emotes/public/default/whale2.png differ diff --git a/extras/emotes/public/default/wheelchair.png b/extras/emotes/public/default/wheelchair.png new file mode 100755 index 0000000..eddcdd7 Binary files /dev/null and b/extras/emotes/public/default/wheelchair.png differ diff --git a/extras/emotes/public/default/white_check_mark.png b/extras/emotes/public/default/white_check_mark.png new file mode 100755 index 0000000..61dc058 Binary files /dev/null and b/extras/emotes/public/default/white_check_mark.png differ diff --git a/extras/emotes/public/default/white_circle.png b/extras/emotes/public/default/white_circle.png new file mode 100755 index 0000000..3f648d1 Binary files /dev/null and b/extras/emotes/public/default/white_circle.png differ diff --git a/extras/emotes/public/default/white_flower.png b/extras/emotes/public/default/white_flower.png new file mode 100755 index 0000000..c0929d0 Binary files /dev/null and b/extras/emotes/public/default/white_flower.png differ diff --git a/extras/emotes/public/default/white_large_square.png b/extras/emotes/public/default/white_large_square.png new file mode 100755 index 0000000..60cb19a Binary files /dev/null and b/extras/emotes/public/default/white_large_square.png differ diff --git a/extras/emotes/public/default/white_medium_small_square.png b/extras/emotes/public/default/white_medium_small_square.png new file mode 100755 index 0000000..a115cdc Binary files /dev/null and b/extras/emotes/public/default/white_medium_small_square.png differ diff --git a/extras/emotes/public/default/white_medium_square.png b/extras/emotes/public/default/white_medium_square.png new file mode 100755 index 0000000..199808b Binary files /dev/null and b/extras/emotes/public/default/white_medium_square.png differ diff --git a/extras/emotes/public/default/white_small_square.png b/extras/emotes/public/default/white_small_square.png new file mode 100755 index 0000000..24ba879 Binary files /dev/null and b/extras/emotes/public/default/white_small_square.png differ diff --git a/extras/emotes/public/default/white_square_button.png b/extras/emotes/public/default/white_square_button.png new file mode 100755 index 0000000..ad54d55 Binary files /dev/null and b/extras/emotes/public/default/white_square_button.png differ diff --git a/extras/emotes/public/default/wind_chime.png b/extras/emotes/public/default/wind_chime.png new file mode 100755 index 0000000..efacf5d Binary files /dev/null and b/extras/emotes/public/default/wind_chime.png differ diff --git a/extras/emotes/public/default/wine_glass.png b/extras/emotes/public/default/wine_glass.png new file mode 100755 index 0000000..82b0f00 Binary files /dev/null and b/extras/emotes/public/default/wine_glass.png differ diff --git a/extras/emotes/public/default/wink.png b/extras/emotes/public/default/wink.png new file mode 100755 index 0000000..756766d Binary files /dev/null and b/extras/emotes/public/default/wink.png differ diff --git a/extras/emotes/public/default/wolf.png b/extras/emotes/public/default/wolf.png new file mode 100755 index 0000000..c60c968 Binary files /dev/null and b/extras/emotes/public/default/wolf.png differ diff --git a/extras/emotes/public/default/woman.png b/extras/emotes/public/default/woman.png new file mode 100755 index 0000000..6bf0d2b Binary files /dev/null and b/extras/emotes/public/default/woman.png differ diff --git a/extras/emotes/public/default/womans_clothes.png b/extras/emotes/public/default/womans_clothes.png new file mode 100755 index 0000000..aa297c7 Binary files /dev/null and b/extras/emotes/public/default/womans_clothes.png differ diff --git a/extras/emotes/public/default/womans_hat.png b/extras/emotes/public/default/womans_hat.png new file mode 100755 index 0000000..4cb2e6a Binary files /dev/null and b/extras/emotes/public/default/womans_hat.png differ diff --git a/extras/emotes/public/default/womens.png b/extras/emotes/public/default/womens.png new file mode 100755 index 0000000..2fab296 Binary files /dev/null and b/extras/emotes/public/default/womens.png differ diff --git a/extras/emotes/public/default/worried.png b/extras/emotes/public/default/worried.png new file mode 100755 index 0000000..bfa1856 Binary files /dev/null and b/extras/emotes/public/default/worried.png differ diff --git a/extras/emotes/public/default/wrench.png b/extras/emotes/public/default/wrench.png new file mode 100755 index 0000000..a87072a Binary files /dev/null and b/extras/emotes/public/default/wrench.png differ diff --git a/extras/emotes/public/default/x.png b/extras/emotes/public/default/x.png new file mode 100755 index 0000000..dff9efa Binary files /dev/null and b/extras/emotes/public/default/x.png differ diff --git a/extras/emotes/public/default/yellow_heart.png b/extras/emotes/public/default/yellow_heart.png new file mode 100755 index 0000000..fa41ce7 Binary files /dev/null and b/extras/emotes/public/default/yellow_heart.png differ diff --git a/extras/emotes/public/default/yen.png b/extras/emotes/public/default/yen.png new file mode 100755 index 0000000..139bc93 Binary files /dev/null and b/extras/emotes/public/default/yen.png differ diff --git a/extras/emotes/public/default/yum.png b/extras/emotes/public/default/yum.png new file mode 100755 index 0000000..fc39637 Binary files /dev/null and b/extras/emotes/public/default/yum.png differ diff --git a/extras/emotes/public/default/zap.png b/extras/emotes/public/default/zap.png new file mode 100755 index 0000000..260c531 Binary files /dev/null and b/extras/emotes/public/default/zap.png differ diff --git a/extras/emotes/public/default/zero.png b/extras/emotes/public/default/zero.png new file mode 100755 index 0000000..6e57b33 Binary files /dev/null and b/extras/emotes/public/default/zero.png differ diff --git a/extras/emotes/public/default/zzz.png b/extras/emotes/public/default/zzz.png new file mode 100755 index 0000000..30be046 Binary files /dev/null and b/extras/emotes/public/default/zzz.png differ diff --git a/extras/emotes/public/lets-chat/houssam.gif b/extras/emotes/public/lets-chat/houssam.gif new file mode 100644 index 0000000..588948b Binary files /dev/null and b/extras/emotes/public/lets-chat/houssam.gif differ diff --git a/extras/emotes/public/lets-chat/pistol.png b/extras/emotes/public/lets-chat/pistol.png new file mode 100644 index 0000000..be0d337 Binary files /dev/null and b/extras/emotes/public/lets-chat/pistol.png differ diff --git a/extras/emotes/public/lets-chat/simon.gif b/extras/emotes/public/lets-chat/simon.gif new file mode 100644 index 0000000..e61ff68 Binary files /dev/null and b/extras/emotes/public/lets-chat/simon.gif differ diff --git a/extras/replacements/local.yml.sample b/extras/replacements/local.yml.sample new file mode 100644 index 0000000..9f44258 --- /dev/null +++ b/extras/replacements/local.yml.sample @@ -0,0 +1,6 @@ +# +# Replacements +# + +- regex: \B#(\d{2,8})\b + template: #$1 \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..11dc14f --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,24 @@ +var gulp = require('gulp'); +var istanbul = require('gulp-istanbul'); +// We'll use mocha in this example, but any test framework will work +var mocha = require('gulp-mocha'); +var debug = require('gulp-debug'); + +gulp.task('pre-test', function () { + return gulp.src(['app/**/*.js','!app/tests/**/*.spec.js']) + // Covering files + .pipe(istanbul()) + // Force `require` to return covered files + .pipe(istanbul.hookRequire()); +}); + +gulp.task('test', ['pre-test'], function () { + console.log('test task') + return gulp.src(['app/tests/*.js']) + .pipe(debug({title: 'unicorn:'})) + .pipe(mocha({timeout:10000})) + // Creating the reports after tests ran + .pipe(istanbul.writeReports()) + // Enforce a coverage of at least 90% + .pipe(istanbul.enforceThresholds({ thresholds: { global: 90 } })); +}); diff --git a/locales/de.json b/locales/de.json new file mode 100644 index 0000000..2166d09 --- /dev/null +++ b/locales/de.json @@ -0,0 +1,78 @@ +{ + "Sign In": "Anmelden", + "Sign in": "Anmelden", + "Username or Email": "Nutzername oder E-Mail", + "Password": "Passwort", + "I need an account": "Ich brauche einen Account", + "Register": "Registrieren", + "Username": "Nutzername", + "Email": "E-Mail", + "Display Name": "Anzeigename", + "First Name": "Vorname", + "Last Name": "Nachname", + "Confirm Password": "Passwort bestätigen", + "I already have an account": "Ich habe bereits einen Account", + "From Toronto with Love": "Aus Toronto mit Liebe", + "Photos by %s and Friends": "Fotos von %s und Freunden", + "Fork me on GitHub": "Fork me on GitHub", + "Edit Profile": "Profil bearbeiten", + "Account Settings": "Accounteinstellungen", + "Notifications": "Benachrichtigungen", + "Auth Tokens": "Authentifizierungs-Tokens", + "Logout": "Logout", + "Disconnected": "Getrennt", + "Connected": "Verbunden", + "All Rooms": "Alle Räume", + "Loading": "Laden", + "New Password": "Neues Passwort", + "Confirm New Password": "Neues Passwort bestätigen", + "Current Password": "Derzeitiges Passwort", + "Required": "Pflichtfeld", + "Save": "Speichern", + "This room requires password to enter": "Dieser Raum benötigt ein Passwort zum Betreten", + "Edit Room": "Raum bearbeiten", + "Chat History": "Chat-Verlauf", + "Upload Files": "Dateien hochladen", + "Giphy": "Giphy", + "Got something to say?": "Hast du was zu sagen?", + "Send": "Senden", + "Who's Here": "Eingeloggte Personen", + "Files": "Dateien", + "Name": "Name", + "Description": "Beschreibung", + "Participants": "Teilnehmer", + "Archive Room": "Raum archivieren", + "Password required": "Passwort benötigt", + "Room %s requires a password.": "Der Raum %s verlangt ein Passwort.", + "Cancel": "Abbrechen", + "Enter": "Eintreten", + "Desktop Notifications are": "Desktopbenachrichtigungen sind", + "enabled": "aktiviert", + "Use your browser settings to disable them": "Benutzen Sie die Browsereinstellungen um sie zu deaktivieren", + "Enable Desktop Notifications": "Desktopbenachrichtigungen aktivieren", + "blocked": "blockiert", + "Please check your browser settings": "Bitte überprüfen Sie Ihre Browsereinstellungen", + "Profile Settings": "Profileinstellungen", + "XMPP/Jabber Connection Details": "XMPP/Jabber Verbindungsdetails", + "Connection Details": "Verbindungsdetails", + "Host": "Host", + "Port": "Port", + "Conference Host": "Gastgeber", + "Supported Clients": "Unterstützte Clients", + "Desktop": "Desktop", + "Search": "Suche", + "Upload": "Upload", + "Select Files": "Dateien auswählen", + "Room": "Raum", + "Post in room?": "Als Nachricht anzeigen?", + "Authentication tokens": "Authentifizierungs-Tokens", + "Auth tokens are used to access the Let's Chat API.": "Authentifizierungs-Tokens werden für Zugriffe auf die Let's Chat API benötigt.", + "Generate token": "Generiere Token", + "Revoke token": "Token löschen", + "Your generated token is below. It will not be shown again.": "Ihr generierter Token steht unten. Er wird nicht nocheinmal angezeigt.", + "Add Room": "Raum hinzufügen", + "Slug": "Referenz", + "XMPP/Jabber": "XMPP/Jabber", + "Private?": "Privat?", + "Empty for public room": "Leer lassen für einen öffentlichen Raum" +} \ No newline at end of file diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..05ab161 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,83 @@ +{ + "Sign In": "Sign In", + "Sign in": "Sign in", + "Username or Email": "Username or Email", + "Password": "Password", + "I need an account": "I need an account", + "Register": "Register", + "Username": "Username", + "Email": "Email", + "Display Name": "Display Name", + "First Name": "First Name", + "Last Name": "Last Name", + "Confirm Password": "Confirm Password", + "I already have an account": "I already have an account", + "From Toronto with Love": "From Toronto with Love", + "Photos by %s and Friends": "Photos by %s and Friends", + "Fork me on GitHub": "Fork me on GitHub", + "Edit Profile": "Edit Profile", + "Account Settings": "Account Settings", + "Notifications": "Notifications", + "Auth Tokens": "Auth Tokens", + "Logout": "Logout", + "Disconnected": "Disconnected", + "Connected": "Connected", + "All Rooms": "All Rooms", + "Loading": "Loading", + "New Password": "New Password", + "Confirm New Password": "Confirm New Password", + "Current Password": "Current Password", + "Required": "Required", + "Save": "Save", + "This room requires password to enter": "This room requires password to enter", + "Edit Room": "Edit Room", + "Chat History": "Chat History", + "Upload Files": "Upload Files", + "Giphy": "Giphy", + "Got something to say?": "Got something to say?", + "Send": "Send", + "Who's Here": "Who's Here", + "Files": "Files", + "Name": "Name", + "Description": "Description", + "Participants": "Participants", + "Archive Room": "Archive Room", + "Password required": "Password required", + "Room %s requires a password.": "Room %s requires a password.", + "Cancel": "Cancel", + "Enter": "Enter", + "Desktop Notifications are": "Desktop Notifications are", + "enabled": "enabled", + "Use your browser settings to disable them": "Use your browser settings to disable them", + "Enable Desktop Notifications": "Enable Desktop Notifications", + "blocked": "blocked", + "Please check your browser settings": "Please check your browser settings", + "Profile Settings": "Profile Settings", + "XMPP/Jabber Connection Details": "XMPP/Jabber Connection Details", + "Connection Details": "Connection Details", + "Host": "Host", + "Port": "Port", + "Conference Host": "Conference Host", + "Supported Clients": "Supported Clients", + "Desktop": "Desktop", + "Search": "Search", + "Upload": "Upload", + "Select Files": "Select Files", + "Room": "Room", + "Post in room?": "Post in room?", + "Authentication tokens": "Authentication tokens", + "Auth tokens are used to access the Let's Chat API.": "Auth tokens are used to access the Let's Chat API.", + "Generate token": "Generate token", + "Revoke token": "Revoke token", + "Your generated token is below. It will not be shown again.": "Your generated token is below. It will not be shown again.", + "Add Room": "Add Room", + "Slug": "Slug", + "XMPP/Jabber": "XMPP/Jabber", + "Private?": "Private?", + "Empty for public room": "Empty for public room", + "Toggle Navigation": "Toggle Navigation", + "Home": "Home", + "Toggle Sidebar": "Toggle Sidebar", + "Close": "Close", + "Transcript for": "Transcript for" +} \ No newline at end of file diff --git a/locales/es.json b/locales/es.json new file mode 100644 index 0000000..77529ce --- /dev/null +++ b/locales/es.json @@ -0,0 +1,83 @@ +{ + "Sign In": "Iniciar Sesión", + "Sign in": "Iniciar sesión", + "Username or Email": "Nombre de Usuario o Correo Electrónico", + "Password": "Contraseña", + "I need an account": "Necesito una cuenta", + "Register": "Registro", + "Username": "Nombre de Usuario", + "Email": "Correo Electrónico", + "Display Name": "Nombre a Mostrar", + "First Name": "Primer Nombre", + "Last Name": "Apellido", + "Confirm Password": "Confirmar Contraseña", + "I already have an account": "Ya tengo una cuenta", + "From Toronto with Love": "Desde Toronto con Amor", + "Photos by %s and Friends": "Fotos por %s y Amigos", + "Fork me on GitHub": "Hazme un Fork en GitHub", + "Edit Profile": "Editar Perfil", + "Account Settings": "Configuración de Cuenta", + "Notifications": "Notificaciones", + "Auth Tokens": "Auth Tokens", + "Logout": "Cerrar Sesión", + "Disconnected": "Desconectado", + "Connected": "Conectado", + "All Rooms": "Todas las Salas", + "Loading": "Cargando", + "New Password": "Nueva Contraseña", + "Confirm New Password": "Confirme Nueva Contraseña", + "Current Password": "Contraseña Actual", + "Required": "Indispensable", + "Save": "Guardar", + "This room requires password to enter": "Esta sala requiere contraseña para ingresar", + "Edit Room": "Editar Sala", + "Chat History": "Historial Chat", + "Upload Files": "Cargar Archivos", + "Giphy": "Giphy", + "Got something to say?": "¿Quieres decir algo?", + "Send": "Enviar", + "Who's Here": "Quien está aquí", + "Files": "Archivos", + "Name": "Nombre", + "Description": "Descripción", + "Participants": "Participantes", + "Archive Room": "Archivar Sala", + "Password required": "Se requiere contraseña", + "Room %s requires a password.": "Sala %s require una contraseña.", + "Cancel": "Cancelar", + "Enter": "Ingresar", + "Desktop Notifications are": "Notificaciones de Escritorio son", + "enabled": "habilitado", + "Use your browser settings to disable them": "Utilice la configuración de tu navegador para deshabilitarlos", + "Enable Desktop Notifications": "Habilitar Notificaciones de Escritorio", + "blocked": "bloqueado", + "Please check your browser settings": "Por favor, compruebe la configuración del navegador", + "Profile Settings": "Configuración de Perfil", + "XMPP/Jabber Connection Details": "Detalles de Conexión XMPP/Jabber", + "Connection Details": "Detalles de Conexión", + "Host": "Host", + "Port": "Puerto", + "Conference Host": "Host de Conferencia", + "Supported Clients": "Clientes con Soporte", + "Desktop": "Escritorio", + "Search": "Buscar", + "Upload": "Cargar", + "Select Files": "Selecciona Archivos", + "Room": "Sala", + "Post in room?": "¿Publicar en la sala?", + "Authentication tokens": "Tokens de autenticación", + "Auth tokens are used to access the Let's Chat API.": "Auth tokens son utilizados para acceder al API de Let's Chat.", + "Generate token": "Generar token", + "Revoke token": "Revocar token", + "Your generated token is below. It will not be shown again.": "Su token es generada en el campo inferior. No se mostrará de nuevo.", + "Add Room": "Añadir Sala", + "Slug": "Etiqueta", + "XMPP/Jabber": "XMPP/Jabber", + "Private?": "¿Privada?", + "Empty for public room": "Vacío para sala pública", + "Toggle Navigation": "Cambiar Navegación", + "Home": "Inicio", + "Toggle Sidebar": "Cambiar Barra Lateral", + "Close": "Cerrar", + "Transcript for": "Transcripción de" +} diff --git a/locales/fi.json b/locales/fi.json new file mode 100644 index 0000000..7ff8dc7 --- /dev/null +++ b/locales/fi.json @@ -0,0 +1,78 @@ +{ + "Sign In": "Sisäänkirjautuminen", + "Sign in": "Kirjaudu sisään", + "Username or Email": "Käyttäjätunnus tai sähköposti", + "Password": "Salasana", + "I need an account": "Tarvitsen käyttäjätilin", + "Register": "Rekisteröidy", + "Username": "Käyttäjätunnus", + "Email": "Sähköposti", + "Display Name": "Näyttönimi", + "First Name": "Etunimi", + "Last Name": "Sukunimi", + "Confirm Password": "Vahvista salasana", + "I already have an account": "Minulla on jo käyttäjätili", + "From Toronto with Love": "Rakkaudella Torontosta", + "Photos by %s and Friends": "Valokuvat: %s ja ystävät", + "Fork me on GitHub": "Forkkaa GitHubissa", + "Edit Profile": "Muokkaa profiilia", + "Account Settings": "Tilin asetukset", + "Notifications": "Ilmoitukset", + "Auth Tokens": "Todennustunnisteet", + "Logout": "Kirjaudu ulos", + "Disconnected": "Yhteys katkennut", + "Connected": "Yhdistetty", + "All Rooms": "Kaikki huoneet", + "Loading": "Ladataan", + "New Password": "Uusi salasana", + "Confirm New Password": "Vahvista uusi salasana", + "Current Password": "Nykyinen salasana", + "Required": "Pakollinen", + "Save": "Tallenna", + "This room requires password to enter": "Tähän huoneeseen pääsy vaatii salasanan", + "Edit Room": "Muokkaa huonetta", + "Chat History": "Keskusteluhistoria", + "Upload Files": "Lähetä tiedostoja", + "Giphy": "Giphy", + "Got something to say?": "Onko sanottavaa?", + "Send": "Lähetä", + "Who's Here": "Keitä täällä on", + "Files": "Tiedostot", + "Name": "Nimi", + "Description": "Kuvaus", + "Participants": "Osallistujat", + "Archive Room": "Arkistoi huone", + "Password required": "Salasana vaaditaan", + "Room %s requires a password.": "Huone %s vaatii salasanan.", + "Cancel": "Peruuta", + "Enter": "Käy sisään", + "Desktop Notifications are": "Työpöytäilmoitukset ovat", + "enabled": "päällä", + "Use your browser settings to disable them": "Käytä selaimesi asetuksia säätääksesi ne pois päältä", + "Enable Desktop Notifications": "Ota työpöytäilmoitukset käyttöön", + "blocked": "estetty", + "Please check your browser settings": "Tarkista selaimesi asetukset", + "Profile Settings": "Profiilin asetukset", + "XMPP/Jabber Connection Details": "XMPP/Jabber-yhteystiedot", + "Connection Details": "Yhteystiedot", + "Host": "Palvelin", + "Port": "Portti", + "Conference Host": "Neuvottelupalvelin", + "Supported Clients": "Tuetut asiakasohjelmat", + "Desktop": "Työpöytä", + "Search": "Hae", + "Upload": "Lähetä", + "Select Files": "Valitse tiedostot", + "Room": "Huone", + "Post in room?": "Lähetä huoneeseen?", + "Authentication tokens": "Todennustunnisteet", + "Auth tokens are used to access the Let's Chat API.": "Todennustunnisteita käytetään Let's Chatin APIssa.", + "Generate token": "Luo tunniste", + "Revoke token": "Kumoa tunniste", + "Your generated token is below. It will not be shown again.": "Luomasti tunniste näkyy alapuolella. Sitä ei näytetä uudestaan.", + "Add Room": "Lisää huone", + "Slug": "Tunniste", + "XMPP/Jabber": "XMPP/Jabber", + "Private?": "Yksityinen?", + "Empty for public room": "Tyhjä julkiselle huoneelle" +} diff --git a/locales/fr.json b/locales/fr.json new file mode 100644 index 0000000..82ad123 --- /dev/null +++ b/locales/fr.json @@ -0,0 +1,83 @@ +{ + "Sign In": "S'identifier", + "Sign in": "S'identifier", + "Username or Email": "Nom d'utilisateur ou Email", + "Password": "Mot de passe", + "I need an account": "J'ai besoin d'un compte", + "Register": "S'enregistrer", + "Username": "Nom d'utilisateur", + "Email": "Email", + "Display Name": "Nom affiché", + "First Name": "Prénom", + "Last Name": "Nom", + "Confirm Password": "Confirmer le mot de passe", + "I already have an account": "Je dispose déjà d'un compte", + "From Toronto with Love": "De Toronto avec amour", + "Photos by %s and Friends": "Photos prises par %s et ses amis", + "Fork me on GitHub": "Copiez-moi avec GitHub", + "Edit Profile": "Édition du profil", + "Account Settings": "Paramètres du compte", + "Notifications": "Notifications", + "Auth Tokens": "Jetons d'authentification", + "Logout": "Déconnexion", + "Disconnected": "Déconnecté", + "Connected": "Connecté", + "All Rooms": "Tous les salons", + "Loading": "Chargement", + "New Password": "Nouveau mot de passe", + "Confirm New Password": "Confirmer votre nouveau mot de passe", + "Current Password": "Mot de passe courant", + "Required": "Requis", + "Save": "Enregistrer", + "This room requires password to enter": "Ce salon requiert un mot de passe d'accès", + "Edit Room": "Édition du Salon", + "Chat History": "Historique de la discussion", + "Upload Files": "Transférer des fichiers", + "Giphy": "Giphy", + "Got something to say?": "Quelque chose à dire ?", + "Send": "Envoi", + "Who's Here": "Qui est là", + "Files": "Fichiers", + "Name": "Nom", + "Description": "Description", + "Participants": "Participants", + "Archive Room": "Archiver le salon", + "Password required": "Mot de passe requis", + "Room %s requires a password.": "Le salon %s requiert un mot de passe.", + "Cancel": "Annuler", + "Enter": "Entrer", + "Desktop Notifications are": "Les notifications de bureau sont", + "enabled": "activées", + "Use your browser settings to disable them": "Utilisez les paramètres du navigateur pour les désactiver", + "Enable Desktop Notifications": "Activer les notifications de bureau", + "blocked": "bloqué", + "Please check your browser settings": "Vérifiez vos paramètres de navigateur s'il vous plaît", + "Profile Settings": "Paramètres du profil", + "XMPP/Jabber Connection Details": "Détails de connexion de XMPP/Jabber", + "Connection Details": "Détails de connexion", + "Host": "Hôte", + "Port": "Port", + "Conference Host": "Hôte pour les salons de conférence", + "Supported Clients": "Clients supportés", + "Desktop": "Bureau", + "Search": "Rechercher", + "Upload": "Transférer", + "Select Files": "Choix des fichiers", + "Room": "Salon", + "Post in room?": "Publier dans le salon ?", + "Authentication tokens": "Jetons d'authentification", + "Auth tokens are used to access the Let's Chat API.": "Les jetons d'authentifications sont utilisés pour accéder à l'API de Let's Chat.", + "Generate token": "Générer un jeton", + "Revoke token": "Révoquer un jeton", + "Your generated token is below. It will not be shown again.": "Votre nouveau jeton est visible ci-dessous. Il ne sera plus accessible à la fermeture de cette fenêtre.", + "Add Room": "Ajouter un salon", + "Slug": "Slug", + "XMPP/Jabber": "XMPP/Jabber", + "Private?": "Privé ?", + "Empty for public room": "Vide pour un salon public", + "Toggle Navigation": "Basculer l'affichage de la navigation", + "Home": "Accueil", + "Toggle Sidebar": "Basculer l'affichage de la barre latérale", + "Close": "Fermer", + "Transcript for": "Verbatim pour" +} diff --git a/locales/is.json b/locales/is.json new file mode 100644 index 0000000..0bc2e5b --- /dev/null +++ b/locales/is.json @@ -0,0 +1,78 @@ +{ + "Sign In": "Innskráning", + "Sign in": "Innskráning", + "Username or Email": "Notendanafn eða tölvupóstfang", + "Password": "Lykilorð", + "I need an account": "Mig vantar aðgang", + "Register": "Skrá nýjan reikning", + "Username": "Notendanafn", + "Email": "Tölvupóstfang", + "Display Name": "Sýnilegt Nafn", + "First Name": "Eiginnafn", + "Last Name": "Kenninafn", + "Confirm Password": "Lykilorð til staðfestingar", + "I already have an account": "Ég hef nú þegar aðgang", + "From Toronto with Love": "Frá Toronto með kveðju", + "Photos by %s and Friends": "Myndir frá %s og vinum", + "Fork me on GitHub": "Afritaðu(fork) mig á GitHub", + "Edit Profile": "Breyta persónu stillingum", + "Account Settings": "Reikningsstillingar", + "Notifications": "Tilkynningar", + "Auth Tokens": "Auðkenni", + "Logout": "Útskráning", + "Disconnected": "Aftengd", + "Connected": "Tengd", + "All Rooms": "Öll herbergi", + "Loading": "Er að hlaða", + "New Password": "Nýtt lykilorð", + "Confirm New Password": "Staðfestu nýtt lykilorð", + "Current Password": "Núverandi lykilorð", + "Required": "Nauðsynlegt", + "Save": "Vista", + "This room requires password to enter": "Þetta herbergi er læst með lykilorði", + "Edit Room": "Breyta herbergi", + "Chat History": "Spjallsaga", + "Upload Files": "Hlaða upp skrám", + "Giphy": "Giphy", + "Got something to say?": "Liggur þér eitthvað á hjarta?", + "Send": "Senda", + "Who's Here": "Hver er hér", + "Files": "Skrár", + "Name": "Nafn", + "Description": "Lýsing", + "Participants": "þáttakendur", + "Archive Room": "Virkt Herbergi", + "Password required": "Lykilorð nauðsynlegt", + "Room %s requires a password.": "Til að fá aðgang að herberginu %s þarf lykilorð.", + "Cancel": "Hætta við", + "Enter": "Ganga inn", + "Desktop Notifications are": "Tilkynningar á skjáborði eru", + "enabled": "virkt", + "Use your browser settings to disable them": "Notið stillingar í vafra til að afvirkja", + "Enable Desktop Notifications": "Virkja skjáborðstilkynningar", + "blocked": "bannað", + "Please check your browser settings": "Vinsamlegast skoðid stillingar í vafra", + "Profile Settings": "Persónu stillingar", + "XMPP/Jabber Connection Details": "XMPP/Jabber tengingar", + "Connection Details": "Tengingar", + "Host": "Miðlari", + "Port": "Port", + "Conference Host": "Samskiptamiðlari", + "Supported Clients": "Studd notendaforrit", + "Desktop": "Skjáborð", + "Search": "Leita", + "Upload": "Hlaða upp", + "Select Files": "Velja skrár", + "Room": "Herbergi", + "Post in room?": "Birta í herbergi?", + "Authentication tokens": "Auðkenni", + "Auth tokens are used to access the Let's Chat API.": "Auðkenni eru notuð til að gefa aðgang að tölvuviðmóti(API).", + "Generate token": "Búa til auðkenni", + "Revoke token": "Afvirkja auðkenni", + "Your generated token is below. It will not be shown again.": "Nýtt auðkenni birtist að neðan, auðkennið verður ekki birt aftur.", + "Add Room": "Nýtt herbergi", + "Slug": "Stutt lýsing", + "XMPP/Jabber": "XMPP/Jabber", + "Private?": "Private?", + "Empty for public room": "Empty for public room" +} \ No newline at end of file diff --git a/locales/ja.json b/locales/ja.json new file mode 100644 index 0000000..eb8e23e --- /dev/null +++ b/locales/ja.json @@ -0,0 +1,78 @@ +{ + "Sign In": "ログイン", + "Sign in": "ログイン", + "Username or Email": "ユーザー名もしくはメールアドレス", + "Password": "パスワード", + "I need an account": "アカウント作成", + "Register": "登録", + "Username": "ユーザー名", + "Email": "メールアドレス", + "Display Name": "表示名", + "First Name": "名", + "Last Name": "姓", + "Confirm Password": "パスワードを確認", + "I already have an account": "ログイン画面へ戻る", + "From Toronto with Love": "トロントから愛をこめて", + "Photos by %s and Friends": "%s と友人の写真", + "Fork me on GitHub": "GitHubでforkしてください", + "Edit Profile": "プロフィール編集", + "Account Settings": "アカウント設定", + "Notifications": "通知", + "Auth Tokens": "認証トークン", + "Logout": "ログアウト", + "Disconnected": "切断されました", + "Connected": "接続中", + "All Rooms": "すべてのルーム", + "Loading": "ロード中", + "New Password": "新しいパスワード", + "Confirm New Password": "新しいパスワードを確認", + "Current Password": "現在のパスワード", + "Required": "必須", + "Save": "保存", + "This room requires password to enter": "このルームに入るにはパスワードが必要です", + "Edit Room": "ルームを編集", + "Chat History": "チャット履歴", + "Upload Files": "ファイルをアップロード", + "Giphy": "Giphy", + "Got something to say?": "言いたいことはありますか?", + "Send": "送信", + "Who's Here": "オンライン", + "Files": "ファイル", + "Name": "名前", + "Description": "説明", + "Participants": "参加者", + "Archive Room": "アーカイブルーム", + "Password required": "パスワードが必要", + "Room %s requires a password.": "ルーム %s は、パスワードが必要です。", + "Cancel": "キャンセル", + "Enter": "入力", + "Desktop Notifications are": "ディスクトップ通知", + "enabled": "有効", + "Use your browser settings to disable them": "設定を無効にするには、ブラウザの設定を変更してください", + "Enable Desktop Notifications": "ディスクトップ通知を有効にする", + "blocked": "ブロック", + "Please check your browser settings": "ブラウザの設定を確認してください", + "Profile Settings": "プロフィール設定", + "XMPP/Jabber Connection Details": "XMPP/Jabber 接続設定", + "Connection Details": "接続設定", + "Host": "ホスト", + "Port": "ポート", + "Conference Host": "会議ホスト", + "Supported Clients": "対応クライアント", + "Desktop": "デスクトップ", + "Search": "検索", + "Upload": "アップロード", + "Select Files": "ファイルを選択", + "Room": "ルーム", + "Post in room?": "ルームに投稿しますか?", + "Authentication tokens": "認証トークン", + "Auth tokens are used to access the Let's Chat API.": "認証トークンは Let's Chat API へアクセスするために使用されています。", + "Generate token": "認証トークン生成", + "Revoke token": "認証トークン無効化", + "Your generated token is below. It will not be shown again.": "生成したトークンは以下の通りです。再表示されないので注意してください。", + "Add Room": "ルームを追加", + "Slug": "短縮名", + "XMPP/Jabber": "XMPP/Jabber", + "Private?": "非公開ルームにしますか?", + "Empty for public room": "公開ルームがありません" +} diff --git a/locales/nl.json b/locales/nl.json new file mode 100644 index 0000000..4758e51 --- /dev/null +++ b/locales/nl.json @@ -0,0 +1,78 @@ +{ + "Sign In": "Aanmelden", + "Sign in": "Aanmelden", + "Username or Email": "Gebruikersnaam of e-mail", + "Password": "Paswoord", + "I need an account": "Ik heb nog geen account", + "Register": "Registreren", + "Username": "Gebruikersnaam", + "Email": "E-mail", + "Display Name": "Schermnaam", + "First Name": "Voornaam", + "Last Name": "Achternaam", + "Confirm Password": "Bevestig wachtwoord", + "I already have an account": "Ik heb al een account", + "From Toronto with Love": "From Toronto with Love", + "Photos by %s and Friends": "Foto's van %s en vrienden", + "Fork me on GitHub": "Fork me on GitHub", + "Edit Profile": "Bewerk Profiel", + "Account Settings": "Account instellingen", + "Notifications": "Notificaties", + "Auth Tokens": "Auth Tokens", + "Logout": "Afmelden", + "Disconnected": "Verbinding verbroken", + "Connected": "Verbonden", + "All Rooms": "Chatrooms", + "Loading": "Laden", + "New Password": "Nieuw Wachtwoord", + "Confirm New Password": "Bevestig nieuw wachtwoord", + "Current Password": "Huidig wachtwoord", + "Required": "Verplicht", + "Save": "Opslaan", + "This room requires password to enter": "Deze chatroom vereist een wachtwoord", + "Edit Room": "Bewerk chatroom", + "Chat History": "Chat geschiedenis", + "Upload Files": "Upload bestanden", + "Giphy": "Giphy", + "Got something to say?": "Heb je iets te zeggen?", + "Send": "Verstuur", + "Who's Here": "Wie is hier", + "Files": "Bestanden", + "Name": "Naam", + "Description": "Beschrijving", + "Participants": "Deelnemers", + "Archive Room": "Archieveer chatroom", + "Password required": "Wachtwoord vereist", + "Room %s requires a password.": "Chatroom %s vereist een wachtwoord.", + "Cancel": "Annuleren", + "Enter": "Enter", + "Desktop Notifications are": "Bureaublad notificaties staan", + "enabled": "aan", + "Use your browser settings to disable them": "Gebruik de instellingen van je browser om ze uit te zetten", + "Enable Desktop Notifications": "Bureaublad notificaties aanzetten", + "blocked": "geblokkeerd", + "Please check your browser settings": "Controleer uw browser instellingen", + "Profile Settings": "Profiel instellingen", + "XMPP/Jabber Connection Details": "XMPP/Jabber verbinding details", + "Connection Details": "Verbinding details", + "Host": "Host", + "Port": "Poort", + "Conference Host": "Conferentie host", + "Supported Clients": "Ondersteunde clients", + "Desktop": "Bureaublad", + "Search": "Zoeken", + "Upload": "Upload", + "Select Files": "Selecteer bestanden", + "Room": "Chatroom", + "Post in room?": "Verzenden naar chatroom?", + "Authentication tokens": "Authenticatie tokens", + "Auth tokens are used to access the Let's Chat API.": "Auth tokens worden gebruikt om de Let's Chat API te gebruiken.", + "Generate token": "Genereer token", + "Revoke token": "Herroep token", + "Your generated token is below. It will not be shown again.": "Uw gegenereerd token staat hieronder. Het zal niet opnieuw getoond worden.", + "Add Room": "Chatroom toevoegen", + "Slug": "Referentie", + "XMPP/Jabber": "XMPP/Jabber", + "Private?": "Privaat?", + "Empty for public room": "Leegmaken voor publieke chatroom" +} diff --git a/locales/pl.json b/locales/pl.json new file mode 100644 index 0000000..e49373e --- /dev/null +++ b/locales/pl.json @@ -0,0 +1,83 @@ +{ + "Sign In": "Zaloguj", + "Sign in": "Zaloguj", + "Username or Email": "Nazwa użytkownika lub email", + "Password": "Hasło", + "I need an account": "Potrzebuję konta", + "Register": "Zarejestruj", + "Username": "Nazwa użytkownika", + "Email": "Email", + "Display Name": "Wyświetlana nazwa", + "First Name": "Imię", + "Last Name": "Nazwisko", + "Confirm Password": "Potwierdź hasło", + "I already have an account": "Mam już konto", + "From Toronto with Love": "Z Toronto z miłością", + "Photos by %s and Friends": "Zdjęcia %s i przyjaciele", + "Fork me on GitHub": "Fork me on GitHub", + "Edit Profile": "Edytuj Profil", + "Account Settings": "Ustawienia konta", + "Notifications": "Powiadomienia", + "Auth Tokens": "Tokeny uwierzytelniania", + "Logout": "Wyloguj", + "Disconnected": "Rozłączony", + "Connected": "Połączony", + "All Rooms": "Wszystkie pokoje", + "Loading": "Wczytywanie", + "New Password": "Nowe hasło", + "Confirm New Password": "Potwierdź nowe hasło", + "Current Password": "Aktualne hasło", + "Required": "Wymagane", + "Save": "Zapisz", + "This room requires password to enter": "Ten pokój wymaga wpisania hasła", + "Edit Room": "Edytuj pokój", + "Chat History": "Historia czatu", + "Upload Files": "Wyślij pliki", + "Giphy": "Giphy", + "Got something to say?": "Masz coś do powiedzenia?", + "Send": "Wyślij", + "Who's Here": "Kto tutaj jest", + "Files": "Pliki", + "Name": "Nazwa", + "Description": "Opis", + "Participants": "Uczestnicy", + "Archive Room": "Pokój archiwalny", + "Password required": "Wymagane hasło", + "Room %s requires a password.": "Pokój %s wymaga hasła.", + "Cancel": "Anuluj", + "Enter": "Wejdź", + "Desktop Notifications are": "Powiadomienia pulpitu są", + "enabled": "włączone", + "Use your browser settings to disable them": "Użyj ustawień przeglądarki aby je zablokować", + "Enable Desktop Notifications": "Włącz powiadomienia pulpitu", + "blocked": "zablokowane", + "Please check your browser settings": "Proszę sprawdzić ustawienia przeglądarki", + "Profile Settings": "Ustawienia profilu", + "XMPP/Jabber Connection Details": "Szczegóły połączenia XMPP/Jabber", + "Connection Details": "Szczegóły połączenia", + "Host": "Host", + "Port": "Port", + "Conference Host": "Gospodarz konferencji", + "Supported Clients": "Obsługiwani klienci", + "Desktop": "Pulpit", + "Search": "Znajdź", + "Upload": "Wyslij", + "Select Files": "Wybierz pliki", + "Room": "Pokój", + "Post in room?": "Wysłać do pokoju?", + "Authentication tokens": "Tokeny uwierzytelniania", + "Auth tokens are used to access the Let's Chat API.": "Tokeny uwierzytelniania pozwalają na dostęp do API Let's Chat", + "Generate token": "Wygeneruj token", + "Revoke token": "Odrzuć token", + "Your generated token is below. It will not be shown again.": "Twój wygenerowany token znajduje się poniżej. Nie będzie już ponownie pokazany.", + "Add Room": "Dodaj pokój", + "Slug": "Slug", + "XMPP/Jabber": "XMPP/Jabber", + "Private?": "Prywatny?", + "Empty for public room": "Pusty dla pokoju publicznego", + "Toggle Navigation": "Przełącz nawigację", + "Home": "Home", + "Toggle Sidebar": "Przełącz panel boczny", + "Close": "Zamknij", + "Transcript for": "Zapis dla" +} \ No newline at end of file diff --git a/locales/pt.json b/locales/pt.json new file mode 100644 index 0000000..9a51b97 --- /dev/null +++ b/locales/pt.json @@ -0,0 +1,78 @@ +{ + "Sign In": "Entrar", + "Sign in": "Entrar", + "Username or Email": "Nome de Usuário ou Email", + "Password": "Senha", + "I need an account": "Preciso de uma conta", + "Register": "Cadastrar", + "Username": "Nome de Usuário", + "Email": "Email", + "Display Name": "Nome Mostrado no Aplicativo", + "First Name": "Primeiro Nome", + "Last Name": "Último Nome", + "Confirm Password": "Confirmar Senha", + "I already have an account": "Já tenho uma conta", + "From Toronto with Love": "De Toronto com Amor", + "Photos by %s and Friends": "Fotos por %s e Amigos", + "Fork me on GitHub": "Me copie no GitHub", + "Edit Profile": "Editar Perfil", + "Account Settings": "Configurações da Conta", + "Notifications": "Notificações", + "Auth Tokens": "Tokens de Autenticação", + "Logout": "Sair", + "Disconnected": "Desconectado", + "Connected": "Conectado", + "All Rooms": "Todas as Salas", + "Loading": "Carregando", + "New Password": "Nova Senha", + "Confirm New Password": "Confirmar Nova Senha", + "Current Password": "Senha Atual", + "Required": "Necessária", + "Save": "Salvar", + "This room requires password to enter": "Esta sala requer senha para entrar", + "Edit Room": "Editar Sala", + "Chat History": "Histórico do Chat", + "Upload Files": "Upload de Arquivos", + "Giphy": "Giphy", + "Got something to say?": "Tem algo a dizer?", + "Send": "Enviar", + "Who's Here": "Quem está Aqui", + "Files": "Arquivos", + "Name": "Nome", + "Description": "Descrição", + "Participants": "Participantes", + "Archive Room": "Arquivar Sala", + "Password required": "Senha necessária", + "Room %s requires a password.": "Sala %s requer uma senha.", + "Cancel": "Cancelar", + "Enter": "Entrar", + "Desktop Notifications are": "Notificações de Desktop estão", + "enabled": "habilitado", + "Use your browser settings to disable them": "Use suas configurações do navegador para desabilitá-los", + "Enable Desktop Notifications": "Habilitar Notificações de Desktop", + "blocked": "bloqueadas", + "Please check your browser settings": "Por favor cheque suas configurações do navegador", + "Profile Settings": "Configurações de Perfil", + "XMPP/Jabber Connection Details": "Detalhes da Conexão XMPP/Jabber", + "Connection Details": "Detalhes da Conexão", + "Host": "Host", + "Port": "Porta", + "Conference Host": "Host da Conferência", + "Supported Clients": "Clientes Suportados", + "Desktop": "Desktop", + "Search": "Busca", + "Upload": "Upload", + "Select Files": "Selecionar Arquivos", + "Room": "Sala", + "Post in room?": "Postar na sala?", + "Authentication tokens": "Tokens de authenticação", + "Auth tokens are used to access the Let's Chat API.": "Tokens de authenticação são usadas para acessar o API do Let's Chat.", + "Generate token": "Gerar token", + "Revoke token": "Revogar token", + "Your generated token is below. It will not be shown again.": "Sua token gerada encontra-se abaixo. Ela não será mostrada novamente.", + "Add Room": "Criar Sala", + "Slug": "Slug", + "XMPP/Jabber": "XMPP/Jabber", + "Private?": "Particular?", + "Empty for public room": "Vazio para sala pública" +} \ No newline at end of file diff --git a/locales/ru.json b/locales/ru.json new file mode 100644 index 0000000..cfdacfb --- /dev/null +++ b/locales/ru.json @@ -0,0 +1,82 @@ +{ + "Sign In": "Авторизация", + "Sign in": "Войти", + "Username or Email": "Логин или Email", + "Password": "Пароль", + "I need an account": "Мне нужен аккаунт", + "Register": "Регистрация", + "Username": "Логин", + "Email": "Email", + "Display Name": "Отображаемое имя", + "First Name": "Имя", + "Last Name": "Фамилия", + "Confirm Password": "Повторите пароль", + "I already have an account": "У меня уже есть аккаунт", + "From Toronto with Love": "Из Торонто с любовью", + "Photos by %s and Friends": "Фотографии %s и друзей", + "Fork me on GitHub": "Форкни меня на GitHub", + "Edit Profile": "Редактировать профиль", + "Account Settings": "Настройки аккаунта", + "Notifications": "Оповещения", + "Auth Tokens": "Auth Tokens", + "Logout": "Выйти", + "Disconnected": "Отключен", + "Connected": "Подключен", + "All Rooms": "Все комнаты", + "Loading": "Загрузка", + "New Password": "Новый пароль", + "Confirm New Password": "Повторите новый пароль", + "Current Password": "Нынешний пароль", + "Required": "Обязательно", + "Save": "Сохранить", + "This room requires password to enter": "Для входа в эту комнату нужно ввести пароль", + "Edit Room": "Настройки комнаты", + "Chat History": "История чата", + "Upload Files": "Загрузить файлы", + "Giphy": "Giphy", + "Got something to say?": "Есть что сказать?", + "Send": "Отправить", + "Who's Here": "Кто здесь", + "Files": "Файлы", + "Name": "Название", + "Description": "Описание", + "Participants": "Участники", + "Archive Room": "Отправить в архив", + "Password required": "Нужен пароль", + "Room %s requires a password.": "Комната %s запрашивает пароль.", + "Cancel": "Отменить", + "Enter": "Войти", + "Desktop Notifications are": "Оповещения", + "enabled": "включены", + "Use your browser settings to disable them": "Отключите их в настройках браузера", + "Enable Desktop Notifications": "Включить оповещения", + "blocked": "заблокированы", + "Please check your browser settings": "Пожалуйста, проверьте настройки вашего браузера", + "Profile Settings": "Настройки профиля", + "XMPP/Jabber Connection Details": "Детали соединения XMPP/Jabber", + "Connection Details": "Информация о подключении", + "Host": "Хост", + "Port": "Порт", + "Conference Host": "Хост конференции", + "Supported Clients": "Поддерживаемые клиенты", + "Desktop": "Desktop", + "Search": "Поиск", + "Upload": "Загрузить", + "Select Files": "Выбрать файлы", + "Room": "Комната", + "Post in room?": "Разместить в комнате?", + "Authentication tokens": "Токены аутентификации", + "Auth tokens are used to access the Let's Chat API.": "Токены аутентификации используются для предоставления доступа к API Let's Chat.", + "Generate token": "Сгенерировать токен", + "Revoke token": "Отозвать токен", + "Your generated token is below. It will not be shown again.": "Ваш сгенерированный токен. Он более не будет показан.", + "Add Room": "Создать комнату", + "Slug": "Slug", + "XMPP/Jabber": "XMPP/Jabber", + "Private?": "Приватно?", + "Empty for public room": "Empty for public room", + "Toggle Navigation": "Переключить навигацию", + "Home": "Домой", + "Toggle Sidebar": "Переключить боковую панель", + "Close": "Закрыть" +} diff --git a/locales/zh.json b/locales/zh.json new file mode 100644 index 0000000..0616611 --- /dev/null +++ b/locales/zh.json @@ -0,0 +1,75 @@ +{ + "Sign In": "登入", + "Sign in": "登入", + "Username or Email": "账号或电邮", + "Password": "密码", + "I need an account": "我需要个账号", + "Register": "注册", + "Username": "用户名", + "Email": "电邮", + "Display Name": "显示名字", + "First Name": "名", + "Last Name": "姓", + "Confirm Password": "确认密码", + "I already have an account": "已有账号", + "From Toronto with Love": "从多伦多发来的爱意", + "Photos by %s and Friends": "照片来源自: %s", + "Fork me on GitHub": "Fork me on GitHub", + "Edit Profile": "修改档案", + "Account Settings": "账号设置", + "Notifications": "通知", + "Auth Tokens": "认证令牌", + "Logout": "登出", + "Disconnected": "离线", + "Connected": "在线", + "All Rooms": "所有房间", + "Loading": "加载中", + "New Password": "新密码", + "Confirm New Password": "确认新密码", + "Current Password": "当前密码", + "Required": "必须的", + "Save": "保存", + "This room requires password to enter": "此房间需密码才能进入", + "Edit Room": "编辑房间", + "Chat History": "聊天历史", + "Upload Files": "上传文件", + "Giphy": "Giphy", + "Got something to say?": "有话想说?", + "Send": "发送", + "Who's Here": "在线", + "Files": "文件", + "Name": "名字", + "Description": "简介", + "Participants": "参与者", + "Archive Room": "存档房间", + "Password required": "需要密码", + "Room %s requires a password.": "房间 %s 需要密码", + "Cancel": "取消", + "Enter": "进入", + "Desktop Notifications are": "桌面通知已", + "enabled": "开通", + "Use your browser settings to disable them": "请用您的浏览器设置把桌面通知关闭。", + "Enable Desktop Notifications": "开通桌面通知", + "blocked": "封锁", + "Please check your browser settings": "请检验您的浏览器设置", + "Profile Settings": "档案设置", + "XMPP/Jabber Connection Details": "XMPP/Jabber连接细节", + "Connection Details": "连接细节", + "Host": "服务器", + "Port": "接口", + "Conference Host": "会议服务器", + "Supported Clients": "支持的客户端", + "Desktop": "桌面", + "Search": "搜索", + "Upload": "上传", + "Select Files": "选择文件", + "Room": "房间", + "Post in room?": "在本房发送?", + "Authentication tokens": "认证令牌", + "Auth tokens are used to access the Let's Chat API.": "Let's Chat API需靠认证令牌验证", + "Generate token": "生成令牌", + "Revoke token": "撤销令牌", + "Your generated token is below. It will not be shown again.": "您的认证令牌如下,之后将不再显示。", + "Add Room": "添加房间", + "Slug": "Slug" +} diff --git a/media/favicon.ico b/media/favicon.ico new file mode 100644 index 0000000..3d6b809 Binary files /dev/null and b/media/favicon.ico differ diff --git a/media/font/pacifico.woff b/media/font/pacifico.woff new file mode 100644 index 0000000..fd0cdee Binary files /dev/null and b/media/font/pacifico.woff differ diff --git a/media/font/vendor/font-awesome/FontAwesome.otf b/media/font/vendor/font-awesome/FontAwesome.otf new file mode 100644 index 0000000..81c9ad9 Binary files /dev/null and b/media/font/vendor/font-awesome/FontAwesome.otf differ diff --git a/media/font/vendor/font-awesome/fontawesome-webfont.eot b/media/font/vendor/font-awesome/fontawesome-webfont.eot new file mode 100644 index 0000000..84677bc Binary files /dev/null and b/media/font/vendor/font-awesome/fontawesome-webfont.eot differ diff --git a/media/font/vendor/font-awesome/fontawesome-webfont.svg b/media/font/vendor/font-awesome/fontawesome-webfont.svg new file mode 100644 index 0000000..d907b25 --- /dev/null +++ b/media/font/vendor/font-awesome/fontawesome-webfont.svg @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/media/font/vendor/font-awesome/fontawesome-webfont.ttf b/media/font/vendor/font-awesome/fontawesome-webfont.ttf new file mode 100644 index 0000000..96a3639 Binary files /dev/null and b/media/font/vendor/font-awesome/fontawesome-webfont.ttf differ diff --git a/media/font/vendor/font-awesome/fontawesome-webfont.woff b/media/font/vendor/font-awesome/fontawesome-webfont.woff new file mode 100644 index 0000000..628b6a5 Binary files /dev/null and b/media/font/vendor/font-awesome/fontawesome-webfont.woff differ diff --git a/media/img/dark-noise.png b/media/img/dark-noise.png new file mode 100644 index 0000000..bc220b3 Binary files /dev/null and b/media/img/dark-noise.png differ diff --git a/media/img/loading.svg b/media/img/loading.svg new file mode 100644 index 0000000..4020b4d --- /dev/null +++ b/media/img/loading.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/media/img/photo-overlay.png b/media/img/photo-overlay.png new file mode 100644 index 0000000..3b75106 Binary files /dev/null and b/media/img/photo-overlay.png differ diff --git a/media/img/photos/alley.jpg b/media/img/photos/alley.jpg new file mode 100644 index 0000000..0d9ffb7 Binary files /dev/null and b/media/img/photos/alley.jpg differ diff --git a/media/img/photos/city.jpg b/media/img/photos/city.jpg new file mode 100644 index 0000000..9d9fa01 Binary files /dev/null and b/media/img/photos/city.jpg differ diff --git a/media/img/photos/fishing.jpg b/media/img/photos/fishing.jpg new file mode 100644 index 0000000..4db4a36 Binary files /dev/null and b/media/img/photos/fishing.jpg differ diff --git a/media/img/photos/houses.jpg b/media/img/photos/houses.jpg new file mode 100644 index 0000000..94a1926 Binary files /dev/null and b/media/img/photos/houses.jpg differ diff --git a/media/img/photos/lake.jpg b/media/img/photos/lake.jpg new file mode 100644 index 0000000..a4544b5 Binary files /dev/null and b/media/img/photos/lake.jpg differ diff --git a/media/img/photos/plane.jpg b/media/img/photos/plane.jpg new file mode 100644 index 0000000..eb64e70 Binary files /dev/null and b/media/img/photos/plane.jpg differ diff --git a/media/img/photos/reflection.jpg b/media/img/photos/reflection.jpg new file mode 100644 index 0000000..4e24ddc Binary files /dev/null and b/media/img/photos/reflection.jpg differ diff --git a/media/img/photos/rosedale.jpg b/media/img/photos/rosedale.jpg new file mode 100644 index 0000000..c6670a1 Binary files /dev/null and b/media/img/photos/rosedale.jpg differ diff --git a/media/img/photos/skyscraper.jpg b/media/img/photos/skyscraper.jpg new file mode 100644 index 0000000..559af9b Binary files /dev/null and b/media/img/photos/skyscraper.jpg differ diff --git a/media/img/photos/storm-city.jpg b/media/img/photos/storm-city.jpg new file mode 100644 index 0000000..b88b85c Binary files /dev/null and b/media/img/photos/storm-city.jpg differ diff --git a/media/img/photos/storm.jpg b/media/img/photos/storm.jpg new file mode 100644 index 0000000..78dba13 Binary files /dev/null and b/media/img/photos/storm.jpg differ diff --git a/media/img/photos/streak.jpg b/media/img/photos/streak.jpg new file mode 100644 index 0000000..02c395a Binary files /dev/null and b/media/img/photos/streak.jpg differ diff --git a/media/js/chat.js b/media/js/chat.js new file mode 100644 index 0000000..0c83e17 --- /dev/null +++ b/media/js/chat.js @@ -0,0 +1,19 @@ +//= require util/message.js +//= require models.js +//= require views/browser.js +//= require views/room.js +//= require views/status.js +//= require views/window.js +//= require views/panes.js +//= require views/modals.js +//= require views/upload.js +//= require views/client.js +//= require client.js + +$(function() { + window.client = new window.LCB.Client({ + filesEnabled: $('#lcb-upload').length > 0, + giphyEnabled: $('#lcb-giphy').length > 0 + }); + window.client.start(); +}); diff --git a/media/js/client.js b/media/js/client.js new file mode 100644 index 0000000..61063b4 --- /dev/null +++ b/media/js/client.js @@ -0,0 +1,562 @@ +// +// LCB Client +// + +(function(window, $, _) { + + var RoomStore = { + add: function(id) { + var rooms = store.get('openrooms') || []; + if (!_.contains(rooms, id)) { + rooms.push(id); + store.set('openrooms', rooms); + } + }, + remove: function(id) { + var rooms = store.get('openrooms') || []; + if (_.contains(rooms, id)) { + store.set('openrooms', _.without(rooms, id)); + } + }, + get: function() { + var rooms = store.get('openrooms') || []; + rooms = _.uniq(rooms); + store.set('openrooms', rooms); + return rooms; + } + }; + + // + // Base + // + var Client = function(options) { + this.options = options; + this.status = new Backbone.Model(); + this.user = new UserModel(); + this.users = new UsersCollection(); + this.rooms = new RoomsCollection(); + this.events = _.extend({}, Backbone.Events); + return this; + }; + // + // Account + // + Client.prototype.getUser = function() { + var that = this; + this.socket.emit('account:whoami', function(user) { + that.user.set(user); + }); + }; + Client.prototype.updateProfile = function(profile) { + var that = this; + this.socket.emit('account:profile', profile, function(user) { + that.user.set(user); + }); + }; + + // + // Rooms + // + Client.prototype.createRoom = function(data) { + var that = this; + var room = { + name: data.name, + slug: data.slug, + description: data.description, + password: data.password, + participants: data.participants, + private: data.private + }; + var callback = data.callback; + this.socket.emit('rooms:create', room, function(room) { + if (room && room.errors) { + swal("Unable to create room", + "Room slugs can only contain lower case letters, numbers or underscores!", + "error"); + } else if (room && room.id) { + that.addRoom(room); + that.switchRoom(room.id); + } + callback && callback(room); + }); + }; + Client.prototype.getRooms = function(cb) { + var that = this; + this.socket.emit('rooms:list', { users: true }, function(rooms) { + that.rooms.set(rooms); + // Get users for each room! + // We do it here for the room browser + _.each(rooms, function(room) { + if (room.users) { + that.setUsers(room.id, room.users); + } + }); + + if (cb) { + cb(rooms); + } + }); + }; + Client.prototype.switchRoom = function(id) { + // Make sure we have a last known room ID + this.rooms.last.set('id', this.rooms.current.get('id')); + if (!id || id === 'list') { + this.rooms.current.set('id', 'list'); + this.router.navigate('!/', { + replace: true + }); + return; + } + var room = this.rooms.get(id); + if (room && room.get('joined')) { + this.rooms.current.set('id', id); + this.router.navigate('!/room/' + room.id, { + replace: true + }); + return; + } else if(room) { + this.joinRoom(room, true); + } else { + this.joinRoom({id: id}, true); + } + }; + Client.prototype.updateRoom = function(room) { + this.socket.emit('rooms:update', room); + }; + Client.prototype.roomUpdate = function(resRoom) { + var room = this.rooms.get(resRoom.id); + if (!room) { + this.addRoom(resRoom); + return; + } + room.set(resRoom); + }; + Client.prototype.addRoom = function(room) { + var r = this.rooms.get(room.id); + if (r) { + return r; + } + return this.rooms.add(room); + }; + Client.prototype.archiveRoom = function(options) { + this.socket.emit('rooms:archive', options, function(data) { + if (data !== 'No Content') { + swal('Unable to Archive!', + 'Unable to archive this room!', + 'error'); + } + }); + }; + Client.prototype.roomArchive = function(room) { + this.leaveRoom(room.id); + this.rooms.remove(room.id); + }; + Client.prototype.rejoinRoom = function(room) { + this.joinRoom(room, undefined, true); + }; + Client.prototype.lockJoin = function(id) { + if (_.contains(this.joining, id)) { + return false; + } + + this.joining = this.joining || []; + this.joining.push(id); + return true; + }; + Client.prototype.unlockJoin = function(id) { + var that = this; + _.defer(function() { + that.joining = _.without(that.joining, id); + }); + }; + Client.prototype.joinRoom = function(room, switchRoom, rejoin) { + if (!room || !room.id) { + return; + } + + var that = this; + var id = room.id; + var password = room.password; + + if (!rejoin) { + // Must not have already joined + var room1 = that.rooms.get(id); + if (room1 && room1.get('joined')) { + return; + } + } + + if (!this.lockJoin(id)) { + return; + } + + var passwordCB = function(password) { + room.password = password; + that.joinRoom(room, switchRoom, rejoin); + }; + + this.socket.emit('rooms:join', {roomId: id, password: password}, function(resRoom) { + // Room was likely archived if this returns + if (!resRoom) { + return; + } + + if (resRoom && resRoom.errors && + resRoom.errors === 'password required') { + + that.passwordModal.show({ + roomName: resRoom.roomName, + callback: passwordCB + }); + + that.unlockJoin(id); + return; + } + + if (resRoom && resRoom.errors) { + that.unlockJoin(id); + return; + } + + var room = that.addRoom(resRoom); + room.set('joined', true); + + if (room.get('hasPassword')) { + that.getRoomUsers(room.id, _.bind(function(users) { + this.setUsers(room.id, users); + }, that)); + } + + // Get room history + that.getMessages({ + room: room.id, + since_id: room.lastMessage.get('id'), + take: 200, + expand: 'owner, room', + reverse: true + }, function(messages) { + messages.reverse(); + that.addMessages(messages, !rejoin && !room.lastMessage.get('id')); + !rejoin && room.lastMessage.set(messages[messages.length - 1]); + }); + + if (that.options.filesEnabled) { + that.getFiles({ + room: room.id, + take: 15 + }, function(files) { + files.reverse(); + that.setFiles(room.id, files); + }); + } + // Do we want to switch? + if (switchRoom) { + that.switchRoom(id); + } + // + // Add room id to localstorage so we can reopen it on refresh + // + RoomStore.add(id); + + that.unlockJoin(id); + }); + }; + Client.prototype.leaveRoom = function(id) { + var room = this.rooms.get(id); + if (room) { + room.set('joined', false); + room.lastMessage.clear(); + if (room.get('hasPassword')) { + room.users.set([]); + } + } + this.socket.emit('rooms:leave', id); + if (id === this.rooms.current.get('id')) { + var room = this.rooms.get(this.rooms.last.get('id')); + this.switchRoom(room && room.get('joined') ? room.id : ''); + } + // Remove room id from localstorage + RoomStore.remove(id); + }; + Client.prototype.getRoomUsers = function(id, callback) { + this.socket.emit('rooms:users', { + room: id + }, callback); + }; + // + // Messages + // + Client.prototype.addMessage = function(message) { + var room = this.rooms.get(message.room); + if (!room || !message) { + // Unknown room, nothing to do! + return; + } + room.set('lastActive', message.posted); + if (!message.historical) { + room.lastMessage.set(message); + } + room.trigger('messages:new', message); + }; + Client.prototype.addMessages = function(messages, historical) { + _.each(messages, function(message) { + if (historical) { + message.historical = true; + } + this.addMessage(message); + }, this); + }; + Client.prototype.sendMessage = function(message) { + this.socket.emit('messages:create', message); + }; + Client.prototype.getMessages = function(query, callback) { + this.socket.emit('messages:list', query, callback); + }; + // + // Files + // + Client.prototype.getFiles = function(query, callback) { + this.socket.emit('files:list', { + room: query.room || '', + take: query.take || 40, + expand: query.expand || 'owner' + }, callback); + }; + Client.prototype.setFiles = function(roomId, files) { + if (!roomId || !files || !files.length) { + // Nothing to do here... + return; + } + var room = this.rooms.get(roomId); + if (!room) { + // No room + return; + } + room.files.set(files); + }; + Client.prototype.addFile = function(file) { + var room = this.rooms.get(file.room); + if (!room) { + // No room + return; + } + room.files.add(file); + }; + // + // Users + // + Client.prototype.setUsers = function(roomId, users) { + if (!roomId || !users || !users.length) { + // Data is not valid + return; + } + var room = this.rooms.get(roomId); + if (!room) { + // No room + return; + } + room.users.set(users); + }; + Client.prototype.addUser = function(user) { + var room = this.rooms.get(user.room); + if (!room) { + // No room + return; + } + room.users.add(user); + }; + Client.prototype.removeUser = function(user) { + var room = this.rooms.get(user.room); + if (!room) { + // No room + return; + } + room.users.remove(user.id); + }; + Client.prototype.updateUser = function(user) { + // Update if current user + if (user.id == this.user.id) { + this.user.set(user); + } + // Update all rooms + this.rooms.each(function(room) { + var target = room.users.findWhere({ + id: user.id + }); + target && target.set(user); + }, this); + }; + Client.prototype.getUsersSync = function() { + if (this.users.length) { + return this.users; + } + + var that = this; + + function success(users) { + that.users.set(users); + } + + $.ajax({url:'./users', async: false, success: success}); + + return this.users; + }; + // + // Extras + // + Client.prototype.getEmotes = function(callback) { + this.extras = this.extras || {}; + if (!this.extras.emotes) { + // Use AJAX, so we can take advantage of HTTP caching + // Also, it's a promise - which ensures we only load emotes once + this.extras.emotes = $.get('./extras/emotes'); + } + if (callback) { + this.extras.emotes.done(callback); + } + }; + Client.prototype.getReplacements = function(callback) { + this.extras = this.extras || {}; + if (!this.extras.replacements) { + // Use AJAX, so we can take advantage of HTTP caching + // Also, it's a promise - which ensures we only load emotes once + this.extras.replacements = $.get('./extras/replacements'); + } + if (callback) { + this.extras.replacements.done(callback); + } + }; + + // + // Router Setup + // + Client.prototype.route = function() { + var that = this; + var Router = Backbone.Router.extend({ + routes: { + '!/room/': 'list', + '!/room/:id': 'join', + '*path': 'list' + }, + join: function(id) { + that.switchRoom(id); + }, + list: function() { + that.switchRoom('list'); + } + }); + this.router = new Router(); + Backbone.history.start(); + }; + // + // Listen + // + Client.prototype.listen = function() { + var that = this; + + function joinRooms(rooms) { + // + // Join rooms from localstorage + // We need to check each room is available before trying to join + // + var roomIds = _.map(rooms, function(room) { + return room.id; + }); + + var openRooms = RoomStore.get(); + // Let's open some rooms! + _.defer(function() { + //slow down because router can start a join with no password + _.each(openRooms, function(id) { + if (_.contains(roomIds, id)) { + that.joinRoom({ id: id }); + } + }); + }.bind(this)); + } + + var path = '/' + _.compact( + window.location.pathname.split('/').concat(['socket.io']) + ).join('/'); + + // + // Socket + // + this.socket = io.connect({ + path: path, + reconnection: true, + reconnectionDelay: 500, + reconnectionDelayMax: 1000, + timeout: 3000 + }); + this.socket.on('connect', function() { + that.getUser(); + that.getRooms(joinRooms); + that.status.set('connected', true); + }); + this.socket.on('reconnect', function() { + _.each(that.rooms.where({ joined: true }), function(room) { + that.rejoinRoom(room); + }); + }); + this.socket.on('messages:new', function(message) { + that.addMessage(message); + }); + this.socket.on('rooms:new', function(data) { + that.addRoom(data); + }); + this.socket.on('rooms:update', function(room) { + that.roomUpdate(room); + }); + this.socket.on('rooms:archive', function(room) { + that.roomArchive(room); + }); + this.socket.on('users:join', function(user) { + that.addUser(user); + }); + this.socket.on('users:leave', function(user) { + that.removeUser(user); + }); + this.socket.on('users:update', function(user) { + that.updateUser(user); + }); + this.socket.on('files:new', function(file) { + that.addFile(file); + }); + this.socket.on('disconnect', function() { + that.status.set('connected', false); + }); + // + // GUI + // + this.events.on('messages:send', this.sendMessage, this); + this.events.on('rooms:update', this.updateRoom, this); + this.events.on('rooms:leave', this.leaveRoom, this); + this.events.on('rooms:create', this.createRoom, this); + this.events.on('rooms:switch', this.switchRoom, this); + this.events.on('rooms:archive', this.archiveRoom, this); + this.events.on('profile:update', this.updateProfile, this); + this.events.on('rooms:join', this.joinRoom, this); + }; + // + // Start + // + Client.prototype.start = function() { + this.getEmotes(); + this.getReplacements(); + this.listen(); + this.route(); + this.view = new window.LCB.ClientView({ + client: this + }); + this.passwordModal = new window.LCB.RoomPasswordModalView({ + el: $('#lcb-password') + }); + return this; + }; + // + // Add to window + // + window.LCB = window.LCB || {}; + window.LCB.Client = Client; +})(window, $, _); diff --git a/media/js/common.js b/media/js/common.js new file mode 100644 index 0000000..ccdd8ba --- /dev/null +++ b/media/js/common.js @@ -0,0 +1,53 @@ + +// Validator defaults +$.validator.setDefaults({ + highlight: function(element) { + $(element).closest('.form-group').addClass('has-error'); + }, + unhighlight: function(element) { + $(element).closest('.form-group').removeClass('has-error'); + }, + errorElement: 'span', + errorClass: 'help-block', + errorPlacement: function(error, element) { + if(element.parent('.input-group').length) { + error.insertAfter(element.parent()); + } else if(element.parent().parent('.input-group').length) { + error.insertAfter(element.parent().parent()); + } else { + error.insertAfter(element); + } + } +}); + +$.validator.addMethod('alphanum', function(value, element) { + return this.optional(element) || /^[a-z0-9]+$/i.test(value); +}, 'Only letters and numbers are allowed'); + +$.fn.updateTimeStamp = function() { + var $this = $(this); + var time = $this.attr('title'); + time = moment(time).calendar(); + $this.text(time); +}; + +$(function() { + + // Refresh timestamps just after midnight + + var oneMinutePastMidnight = moment() + .endOf('day') + .add(1, 'minutes') + .diff(moment()); + + var interval = moment.duration(24, 'hours').asMilliseconds(); + + setTimeout(function() { + setInterval(function() { + $('time').each(function() { + $(this).updateTimeStamp(); + }); + }, interval); + }, oneMinutePastMidnight); + +}); diff --git a/media/js/login.js b/media/js/login.js new file mode 100644 index 0000000..bd4b6a1 --- /dev/null +++ b/media/js/login.js @@ -0,0 +1,93 @@ +//= require vendor/md5/md5.js + ++function() { + + function onSubmit(form, callbacks) { + var $form = $(form); + $.ajax({ + type: 'POST', + url: $form.attr('action'), + data: $form.serialize(), + dataType: 'json', + complete: getLoginCallback($form) + }); + } + + function getLoginCallback($form) { + return function(res, text) { + switch(res.status) { + case 200: + case 201: + swal('Success', res.responseJSON.message, 'success'); + $form.hasClass('lcb-login-box-login') && $('.sweet-alert').each(function() { + $(this).find('.confirm').hide(); + $(this).find('p').css('margin-bottom', '20px'); + }); + if ($form.data('refresh')) { + setTimeout(function() { + window.location = + './' + (window.location.hash || ''); + }, 1000); + return; + } + $form[0].reset(); + $('.lcb-show-box:visible').click(); + break; + case 400: + swal('Woops', + res.responseJSON && res.responseJSON.message, + 'error'); + break; + case 401: + swal('Woops.', + 'Your username or password is not correct', + 'warning'); + break; + case 403: + swal('Woops.', + 'Your account is locked', + 'warning'); + break; + default: + swal('Woops.', + 'A server error has occured', + 'error'); + break; + } + // $indicator.removeClass('loading'); + // Clear some inputs + $form.find('[data-clear="true"]').val(''); + }; + } + + $(function() { + // JVFloat + $('input[placeholder]').jvFloat(); + // Switch between login boxes + $('.lcb-show-box').on('click', function() { + var $target = $('html').find($(this).data('target')); + if ($target.length > 0) { + $target.siblings('.lcb-login-box').hide(); + $target.show(); + } + }); + // Show avatar + $('[action="./account/login"] [name="username"]').on('blur', function(e) { + var email = $(this).val(); + var valid = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(email); + if (valid) { + $('.lcb-login-avatar') + .attr('src', 'https://www.gravatar.com/avatar/' + md5(email) + '?s=100?d=mm') + .addClass('show'); + } else { + $('.lcb-login-avatar').removeClass('show'); + } + }); + // Validation + $('form.validate').each(function() { + $(this).validate({ + submitHandler: onSubmit + }); + }); + }); +}(); diff --git a/media/js/models.js b/media/js/models.js new file mode 100644 index 0000000..8cdcc7a --- /dev/null +++ b/media/js/models.js @@ -0,0 +1,48 @@ +// +// LCB Models +// + +var UserModel = Backbone.Model.extend(); + +var UsersCollection = Backbone.Collection.extend({ + model: UserModel +}); + +var MessageModel = Backbone.Model.extend(); + +var MessagesCollection = Backbone.Collection.extend({ + model: MessageModel +}); + +var FileModel = Backbone.Model.extend(); + +var FilesCollection = Backbone.Collection.extend({ + model: FileModel +}); + +var RoomModel = Backbone.Model.extend({ + initialize: function() { + this.messages = new MessagesCollection(); + this.users = new UsersCollection(); + this.files = new FilesCollection(); + this.lastMessage = new Backbone.Model(); + // + // Child events + // + this.users.on('add', _.bind(function(user) { + this.trigger('users:add', user, this); + }, this)); + this.users.on('remove', function(user) { + this.trigger('users:remove', user, this); + }, this); + }, + loaded: false +}); + +var RoomsCollection = Backbone.Collection.extend({ + model: RoomModel, + initialize: function() { + this.current = new Backbone.Model(); + this.last = new Backbone.Model(); + } +}); diff --git a/media/js/transcript.js b/media/js/transcript.js new file mode 100644 index 0000000..7fbb116 --- /dev/null +++ b/media/js/transcript.js @@ -0,0 +1,13 @@ +//= require vendor/bootstrap-daterangepicker/daterangepicker.js +//= require util/message.js +//= require views/transcript.js + +$(function() { + var transcript = new window.LCB.TranscriptView({ + el: '.lcb-transcript', + room: { + id: $('[name="room-id"]').val(), + name: $('[name="room-name"]').val() + } + }); +}); diff --git a/media/js/util/message.js b/media/js/util/message.js new file mode 100644 index 0000000..8ed4d8d --- /dev/null +++ b/media/js/util/message.js @@ -0,0 +1,160 @@ +'use strict'; + +if (typeof window !== 'undefined' && typeof exports === 'undefined') { + if (typeof window.utils !== 'object') { + window.utils = {}; + } +} + +if (typeof exports !== 'undefined') { + var _ = require('underscore'); +} + +(function(exports) { + // + // Message Text Formatting + // + + + function encodeEntities(value) { + return value. + replace(/&/g, '&'). + replace(surrogatePairRegexp, function(value) { + var hi = value.charCodeAt(0), + low = value.charCodeAt(1); + return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';'; + }). + replace(nonAlphanumericRegexp, function(value) { + return '&#' + value.charCodeAt(0) + ';'; + }). + replace(//g, '>'); + } + + function getBaseUrl() { + var parts = window.location.pathname.split('/'); + + parts = _.filter(parts, function(part) { + return part.length; + }); + + if (parts.length) { + parts.splice(parts.length - 1, 1); + } + + var path = window.location.origin; + + if (parts.length) { + path = path + '/' + parts.join('/'); + } + + return path + '/'; + } + + function trim(text) { + return text.trim(); + } + + function mentions(text) { + var mentionPattern = /\B@([\w\.]+)(?!@)\b/g; + return text.replace(mentionPattern, '@$1'); + } + + function roomLinks(text, data) { + if (!data.rooms) { + return text; + } + + var slugPattern = /\B(\#[a-z0-9_]+)\b/g; + + return text.replace(slugPattern, function(slug) { + var s = slug.substring(1); + var room = data.rooms.find(function(room) { + return room.attributes.slug === s; + }); + + if (!room) { + return slug; + } + + return '#' + s + ''; + }); + } + + function uploads(text) { + var pattern = /^\s*(upload:\/\/[-A-Z0-9+&*@#\/%?=~_|!:,.;'"!()]*)\s*$/i; + + return text.replace(pattern, function(url) { + return getBaseUrl() + url.substring(9); + }); + } + + function links(text) { + if (imagePattern.test(text)) { + return text.replace(imagePattern, function(url) { + var uri = encodeEntities(_.unescape(url)); + return 'Pasted Image'; + }); + } else { + return text.replace(linkPattern, function(url) { + var uri = encodeEntities(_.unescape(url)); + return '' + url + ''; + }); + } + } + + function emotes(text, data) { + var regex = new RegExp('\\B(:[a-z0-9_\\+\\-]+:)[\\b]?', 'ig'); + + return text.replace(regex, function(group) { + var key = group.split(':')[1]; + var emote = _.find(data.emotes, function(emote) { + return emote.emote === key; + }); + + if (!emote) { + return group; + } + + var image = _.escape(emote.image), + emo = _.escape(':' + emote.emote + ':'), + size = _.escape(emote.size || 20); + + return '' + emo + ''; + }); + } + + function replacements(text, data) { + _.each(data.replacements, function(replacement) { + text = text.replace(new RegExp(replacement.regex, 'ig'), replacement.template); + }); + return text; + } + + var surrogatePairRegexp = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + // Match everything outside of normal chars and " (quote character) + nonAlphanumericRegexp = /([^\#-~| |!])/g, + imagePattern = /^\s*((https?|ftp):\/\/[-A-Z0-9\u00a1-\uffff+&@#\/%?=~_|!:,.;'"!()]*[-A-Z0-9\u00a1-\uffff+&@#\/%=~_|][.](jpe?g|png|gif))\s*$/i, + linkPattern = /((https?|ftp):\/\/[-A-Z0-9\u00a1-\uffff+&*@#\/%?=~_|!:,.;'"!()]*[-A-Z0-9\u00a1-\uffff+&@#\/%=~_|])/ig; + + exports.format = function(text, data) { + var pipeline = [ + trim, + mentions, + roomLinks, + uploads, + links, + emotes, + replacements + ]; + + _.each(pipeline, function(func) { + text = func(text, data); + }); + + return text; + }; + +})(typeof exports === 'undefined' ? window.utils.message = {} : exports); diff --git a/media/js/vendor.js b/media/js/vendor.js new file mode 100644 index 0000000..7086808 --- /dev/null +++ b/media/js/vendor.js @@ -0,0 +1,19 @@ +//= require vendor/socket.io/socket.io.js +//= require vendor/jquery/jquery.js +//= require vendor/sweetalert/sweet-alert.js +//= require vendor/jquery-validate/jquery.validate.js +//= require vendor/lodash/lodash.js +//= require vendor/backbone/backbone.js +//= require vendor/moment/moment.js +//= require vendor/handlebars/handlebars.js +//= require vendor/bootstrap/bootstrap.js +//= require vendor/store.js/store.js +//= require vendor/JVFloat/jvfloat.js +//= require vendor/dropzone/dropzone.js +//= require vendor/selectize/selectize.js +//= require vendor/notifications/desktop-notifications.js +//= require vendor/favico.js/favico.js +//= require vendor/at/jquery.caret.js +//= require vendor/at/jquery.atwho.js +//= require vendor/backbone.keys/backbone.keys.js +//= require common.js diff --git a/media/js/vendor/JVFloat/.bower.json b/media/js/vendor/JVFloat/.bower.json new file mode 100644 index 0000000..f7c972a --- /dev/null +++ b/media/js/vendor/JVFloat/.bower.json @@ -0,0 +1,42 @@ +{ + "name": "JVFloat", + "main": "jvfloat.min.js, jvfloat.css", + "version": "1.1.0", + "homepage": "http://maman.github.io/JVFloat.js", + "authors": [ + "Achmad Mahardi " + ], + "description": "a jQuery plugin to transform input's placeholder into a floated placeholder", + "keywords": [ + "javascript", + "jquery", + "input", + "placeholder", + "ui", + "float", + "jvfloat" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "app/bower_components", + "test", + "tests" + ], + "dependencies": { + "jquery": ">= 1.7.0" + }, + "devDependencies": {}, + "_release": "1.1.0", + "_resolution": { + "type": "version", + "tag": "v1.1.0", + "commit": "07fc80f3e911b5e5b95958615a1483f9c4c06d36" + }, + "_source": "git://github.com/maman/JVFloat.js.git", + "_target": "~1.1.0", + "_originalSource": "JVFloat", + "_direct": true +} \ No newline at end of file diff --git a/media/js/vendor/JVFloat/jvfloat.css b/media/js/vendor/JVFloat/jvfloat.css new file mode 100644 index 0000000..f664518 --- /dev/null +++ b/media/js/vendor/JVFloat/jvfloat.css @@ -0,0 +1,65 @@ +/* + * Default jvFloat theme. + * modify it as you wish! + */ + +.jvFloat { + position: relative; + display: inline; + margin-top: 1em; +} + +.jvFloat .placeHolder.required { + color: red; +} + +/* Start CSS3 Animations on supported browser */ +.jvFloat .placeHolder { + position: absolute; + top: 0; + left: 0; + width: auto; + color: #0c61fc; + font-size: .8em; + font-weight: bold; + -webkit-transform: translate(0, 0); + -moz-transform: translate(0, 0); + -o-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); + -webkit-transition: -webkit-transform 150ms, opacity 100ms, visibility 100ms; + transition: transform 150ms, opacity 100ms, visibility 100ms; + opacity: 0; + visibility: hidden; +} +.jvFloat .placeHolder.active { + display: block; + visibility: visible; + -webkit-transform: translate(0, -1em); + -moz-transform: translate(0, -1em); + -o-transform: translate(0, -1em); + -ms-transform: translate(0, -1em); + transform: translate(0, -1em); + -webkit-transition: -webkit-transform 100ms, opacity 120ms, visibility 120ms; + transition: transform 100ms, opacity 120ms, visibility 120ms; + opacity: 1; +} +/* End CSS3 */ + +/* Legacy browser */ +/*.jvFloat .placeHolder { + position: absolute; + top: -1em; + left: 0; + color: #0c61fc; + font-size: .85em; + font-weight: bold; + opacity: 0; + visibility: hidden; +} +.jvFloat .placeHolder.active { + display: block; + visibility: visible; + opacity: 1; +}*/ +/* End Legacy */ diff --git a/media/js/vendor/JVFloat/jvfloat.js b/media/js/vendor/JVFloat/jvfloat.js new file mode 100644 index 0000000..f36245b --- /dev/null +++ b/media/js/vendor/JVFloat/jvfloat.js @@ -0,0 +1,75 @@ +/* + * JVFloat.js + * modified on: 18/09/2014 + */ + +(function($) { + 'use strict'; + + // Init Plugin Functions + $.fn.jvFloat = function () { + // Check input type - filter submit buttons. + return this.filter('input:not([type=submit]), textarea, select').each(function() { + function getPlaceholderText($el) { + var text = $el.attr('placeholder'); + + if (typeof text == 'undefined') { + text = $el.attr('title'); + } + + return text; + } + function setState () { + // change span.placeHolder to span.placeHolder.active + var currentValue = $el.val(); + + if (currentValue == null) { + currentValue = ''; + } + else if ($el.is('select')) { + var placeholderValue = getPlaceholderText($el); + + if (placeholderValue == currentValue) { + currentValue = ''; + } + } + + placeholder.toggleClass('active', currentValue !== ''); + } + function generateUIDNotMoreThan1million () { + var id = ''; + do { + id = ('0000' + (Math.random()*Math.pow(36,4) << 0).toString(36)).substr(-4); + } while (!!$('#' + id).length); + return id; + } + function createIdOnElement($el) { + var id = generateUIDNotMoreThan1million(); + $el.prop('id', id); + return id; + } + // Wrap the input in div.jvFloat + var $el = $(this).wrap('
'); + var forId = $el.attr('id'); + if (!forId) { forId = createIdOnElement($el);} + // Store the placeholder text in span.placeHolder + // added `required` input detection and state + var required = $el.attr('required') || ''; + + // adds a different class tag for text areas (.jvFloat .placeHolder.textarea) + // to allow better positioning of the element for multiline text area inputs + var placeholder = ''; + var placeholderText = getPlaceholderText($el); + + if ($(this).is('textarea')) { + placeholder = $('').insertBefore($el); + } else { + placeholder = $('').insertBefore($el); + } + // checks to see if inputs are pre-populated and adds active to span.placeholder + setState(); + $el.bind('keyup blur', setState); + }); + }; +// Make Zeptojs & jQuery Compatible +})(window.jQuery || window.Zepto || window.$); diff --git a/media/js/vendor/at/jquery.atwho.css b/media/js/vendor/at/jquery.atwho.css new file mode 100644 index 0000000..a073908 --- /dev/null +++ b/media/js/vendor/at/jquery.atwho.css @@ -0,0 +1,49 @@ +.atwho-view { + position:absolute; + top: 0; + left: 0; + display: none; + margin-top: 18px; + background: white; + color: black; + border: 1px solid #DDD; + border-radius: 3px; + box-shadow: 0 0 5px rgba(0,0,0,0.1); + min-width: 120px; + max-height: 200px; + overflow: auto; + z-index: 11110 !important; +} + +.atwho-view .cur { + background: #3366FF; + color: white; +} +.atwho-view .cur small { + color: white; +} +.atwho-view strong { + color: #3366FF; +} +.atwho-view .cur strong { + color: white; + font:bold; +} +.atwho-view ul { + /* width: 100px; */ + list-style:none; + padding:0; + margin:auto; +} +.atwho-view ul li { + display: block; + padding: 5px 10px; + border-bottom: 1px solid #DDD; + cursor: pointer; + /* border-top: 1px solid #C8C8C8; */ +} +.atwho-view small { + font-size: smaller; + color: #777; + font-weight: normal; +} diff --git a/media/js/vendor/at/jquery.atwho.js b/media/js/vendor/at/jquery.atwho.js new file mode 100644 index 0000000..1bceb86 --- /dev/null +++ b/media/js/vendor/at/jquery.atwho.js @@ -0,0 +1,875 @@ +/*! jquery.atwho - v0.5.2 %> +* Copyright (c) 2014 chord.luo ; +* homepage: http://ichord.github.com/At.js +* Licensed MIT +*/ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(["jquery"], function ($) { + return (root.returnExportsGlobal = factory($)); + }); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like enviroments that support module.exports, + // like Node. + module.exports = factory(require("jquery")); + } else { + factory(jQuery); + } +}(this, function ($) { + +var Api, App, Controller, DEFAULT_CALLBACKS, KEY_CODE, Model, View, + __slice = [].slice; + +App = (function() { + function App(inputor) { + this.current_flag = null; + this.controllers = {}; + this.alias_maps = {}; + this.$inputor = $(inputor); + this.setIframe(); + this.listen(); + } + + App.prototype.createContainer = function(doc) { + if ((this.$el = $("#atwho-container", doc)).length === 0) { + return $(doc.body).append(this.$el = $("
")); + } + }; + + App.prototype.setIframe = function(iframe, standalone) { + var _ref; + if (standalone == null) { + standalone = false; + } + if (iframe) { + this.window = iframe.contentWindow; + this.document = iframe.contentDocument || this.window.document; + this.iframe = iframe; + } else { + this.document = document; + this.window = window; + this.iframe = null; + } + if (this.iframeStandalone = standalone) { + if ((_ref = this.$el) != null) { + _ref.remove(); + } + return this.createContainer(this.document); + } else { + return this.createContainer(document); + } + }; + + App.prototype.controller = function(at) { + var c, current, current_flag, _ref; + if (this.alias_maps[at]) { + current = this.controllers[this.alias_maps[at]]; + } else { + _ref = this.controllers; + for (current_flag in _ref) { + c = _ref[current_flag]; + if (current_flag === at) { + current = c; + break; + } + } + } + if (current) { + return current; + } else { + return this.controllers[this.current_flag]; + } + }; + + App.prototype.set_context_for = function(at) { + this.current_flag = at; + return this; + }; + + App.prototype.reg = function(flag, setting) { + var controller, _base; + controller = (_base = this.controllers)[flag] || (_base[flag] = new Controller(this, flag)); + if (setting.alias) { + this.alias_maps[setting.alias] = flag; + } + controller.init(setting); + return this; + }; + + App.prototype.listen = function() { + return this.$inputor.on('keyup.atwhoInner', (function(_this) { + return function(e) { + return _this.on_keyup(e); + }; + })(this)).on('keydown.atwhoInner', (function(_this) { + return function(e) { + return _this.on_keydown(e); + }; + })(this)).on('scroll.atwhoInner', (function(_this) { + return function(e) { + var _ref; + return (_ref = _this.controller()) != null ? _ref.view.hide(e) : void 0; + }; + })(this)).on('blur.atwhoInner', (function(_this) { + return function(e) { + var c; + if (c = _this.controller()) { + return c.view.hide(e, c.get_opt("display_timeout")); + } + }; + })(this)).on('click.atwhoInner', (function(_this) { + return function(e) { + return _this.dispatch(); + }; + })(this)); + }; + + App.prototype.shutdown = function() { + var c, _, _ref; + _ref = this.controllers; + for (_ in _ref) { + c = _ref[_]; + c.destroy(); + delete this.controllers[_]; + } + this.$inputor.off('.atwhoInner'); + return this.$el.remove(); + }; + + App.prototype.dispatch = function() { + return $.map(this.controllers, (function(_this) { + return function(c) { + var delay; + if (delay = c.get_opt('delay')) { + clearTimeout(_this.delayedCallback); + return _this.delayedCallback = setTimeout(function() { + if (c.look_up()) { + return _this.set_context_for(c.at); + } + }, delay); + } else { + if (c.look_up()) { + return _this.set_context_for(c.at); + } + } + }; + })(this)); + }; + + App.prototype.on_keyup = function(e) { + var _ref; + switch (e.keyCode) { + case KEY_CODE.ESC: + e.preventDefault(); + if ((_ref = this.controller()) != null) { + _ref.view.hide(); + } + break; + case KEY_CODE.DOWN: + case KEY_CODE.UP: + case KEY_CODE.CTRL: + $.noop(); + break; + case KEY_CODE.P: + case KEY_CODE.N: + if (!e.ctrlKey) { + this.dispatch(); + } + break; + default: + this.dispatch(); + } + }; + + App.prototype.on_keydown = function(e) { + var view, _ref; + view = (_ref = this.controller()) != null ? _ref.view : void 0; + if (!(view && view.visible())) { + return; + } + switch (e.keyCode) { + case KEY_CODE.ESC: + e.preventDefault(); + view.hide(e); + break; + case KEY_CODE.UP: + e.preventDefault(); + view.prev(); + break; + case KEY_CODE.DOWN: + e.preventDefault(); + view.next(); + break; + case KEY_CODE.P: + if (!e.ctrlKey) { + return; + } + e.preventDefault(); + view.prev(); + break; + case KEY_CODE.N: + if (!e.ctrlKey) { + return; + } + e.preventDefault(); + view.next(); + break; + case KEY_CODE.TAB: + case KEY_CODE.ENTER: + if (!view.visible()) { + return; + } + e.preventDefault(); + view.choose(e); + break; + default: + $.noop(); + } + }; + + return App; + +})(); + +Controller = (function() { + Controller.prototype.uid = function() { + return (Math.random().toString(16) + "000000000").substr(2, 8) + (new Date().getTime()); + }; + + function Controller(app, at) { + this.app = app; + this.at = at; + this.$inputor = this.app.$inputor; + this.id = this.$inputor[0].id || this.uid(); + this.setting = null; + this.query = null; + this.pos = 0; + this.cur_rect = null; + this.range = null; + if ((this.$el = $("#atwho-ground-" + this.id, this.app.$el)).length === 0) { + this.app.$el.append(this.$el = $("
")); + } + this.model = new Model(this); + this.view = new View(this); + } + + Controller.prototype.init = function(setting) { + this.setting = $.extend({}, this.setting || $.fn.atwho["default"], setting); + this.view.init(); + return this.model.reload(this.setting.data); + }; + + Controller.prototype.destroy = function() { + this.trigger('beforeDestroy'); + this.model.destroy(); + this.view.destroy(); + return this.$el.remove(); + }; + + Controller.prototype.call_default = function() { + var args, error, func_name; + func_name = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + try { + return DEFAULT_CALLBACKS[func_name].apply(this, args); + } catch (_error) { + error = _error; + return $.error("" + error + " Or maybe At.js doesn't have function " + func_name); + } + }; + + Controller.prototype.trigger = function(name, data) { + var alias, event_name; + if (data == null) { + data = []; + } + data.push(this); + alias = this.get_opt('alias'); + event_name = alias ? "" + name + "-" + alias + ".atwho" : "" + name + ".atwho"; + return this.$inputor.trigger(event_name, data); + }; + + Controller.prototype.callbacks = function(func_name) { + return this.get_opt("callbacks")[func_name] || DEFAULT_CALLBACKS[func_name]; + }; + + Controller.prototype.get_opt = function(at, default_value) { + var e; + try { + return this.setting[at]; + } catch (_error) { + e = _error; + return null; + } + }; + + Controller.prototype.content = function() { + var range; + if (this.$inputor.is('textarea, input')) { + return this.$inputor.val(); + } else { + if (!(range = this.mark_range())) { + return; + } + return range.startContainer.parentNode.textContent || ""; + } + }; + + Controller.prototype.catch_query = function() { + var caret_pos, content, end, query, start, subtext; + content = this.content(); + caret_pos = this.$inputor.caret('pos', { + iframe: this.app.iframe + }); + subtext = content.slice(0, caret_pos); + query = this.callbacks("matcher").call(this, this.at, subtext, this.get_opt('start_with_space')); + if (typeof query === "string" && query.length <= this.get_opt('max_len', 20)) { + start = caret_pos - query.length; + end = start + query.length; + this.pos = start; + query = { + 'text': query, + 'head_pos': start, + 'end_pos': end + }; + this.trigger("matched", [this.at, query.text]); + } else { + query = null; + this.view.hide(); + } + return this.query = query; + }; + + Controller.prototype.rect = function() { + var c, iframe_offset, scale_bottom; + if (!(c = this.$inputor.caret('offset', this.pos - 1, { + iframe: this.app.iframe + }))) { + return; + } + if (this.app.iframe && !this.app.iframeStandalone) { + iframe_offset = $(this.app.iframe).offset(); + c.left += iframe_offset.left; + c.top += iframe_offset.top; + } + if (this.$inputor.is('[contentEditable]')) { + c = this.cur_rect || (this.cur_rect = c); + } + scale_bottom = this.app.document.selection ? 0 : 2; + return { + left: c.left, + top: c.top, + bottom: c.top + c.height + scale_bottom + }; + }; + + Controller.prototype.reset_rect = function() { + if (this.$inputor.is('[contentEditable]')) { + return this.cur_rect = null; + } + }; + + Controller.prototype.mark_range = function() { + var sel; + if (!this.$inputor.is('[contentEditable]')) { + return; + } + if (this.app.window.getSelection && (sel = this.app.window.getSelection()).rangeCount > 0) { + return this.range = sel.getRangeAt(0); + } else if (this.app.document.selection) { + return this.ie8_range = this.app.document.selection.createRange(); + } + }; + + Controller.prototype.insert_content_for = function($li) { + var data, data_value, tpl; + data_value = $li.data('value'); + tpl = this.get_opt('insert_tpl'); + if (this.$inputor.is('textarea, input') || !tpl) { + return data_value; + } + data = $.extend({}, $li.data('item-data'), { + 'atwho-data-value': data_value, + 'atwho-at': this.at + }); + return this.callbacks("tpl_eval").call(this, tpl, data); + }; + + Controller.prototype.insert = function(content, $li) { + var $inputor, node, pos, range, sel, source, start_str, text, wrapped_contents, _i, _len, _ref; + $inputor = this.$inputor; + wrapped_contents = this.callbacks('inserting_wrapper').call(this, $inputor, content, this.get_opt("suffix")); + if ($inputor.is('textarea, input')) { + source = $inputor.val(); + start_str = source.slice(0, Math.max(this.query.head_pos - this.at.length, 0)); + text = "" + start_str + wrapped_contents + (source.slice(this.query['end_pos'] || 0)); + $inputor.val(text); + $inputor.caret('pos', start_str.length + wrapped_contents.length, { + iframe: this.app.iframe + }); + } else if (range = this.range) { + pos = range.startOffset - (this.query.end_pos - this.query.head_pos) - this.at.length; + range.setStart(range.endContainer, Math.max(pos, 0)); + range.setEnd(range.endContainer, range.endOffset); + range.deleteContents(); + _ref = $(wrapped_contents, this.app.document); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + range.insertNode(node); + range.setEndAfter(node); + range.collapse(false); + } + sel = this.app.window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } else if (range = this.ie8_range) { + range.moveStart('character', this.query.end_pos - this.query.head_pos - this.at.length); + range.pasteHTML(wrapped_contents); + range.collapse(false); + range.select(); + } + if (!$inputor.is(':focus')) { + $inputor.focus(); + } + return $inputor.change(); + }; + + Controller.prototype.render_view = function(data) { + var search_key; + search_key = this.get_opt("search_key"); + data = this.callbacks("sorter").call(this, this.query.text, data.slice(0, 1001), search_key); + return this.view.render(data.slice(0, this.get_opt('limit'))); + }; + + Controller.prototype.look_up = function() { + var query, _callback; + if (!(query = this.catch_query())) { + return; + } + _callback = function(data) { + if (data && data.length > 0) { + return this.render_view(data); + } else { + return this.view.hide(); + } + }; + this.model.query(query.text, $.proxy(_callback, this)); + return query; + }; + + return Controller; + +})(); + +Model = (function() { + function Model(context) { + this.context = context; + this.at = this.context.at; + this.storage = this.context.$inputor; + } + + Model.prototype.destroy = function() { + return this.storage.data(this.at, null); + }; + + Model.prototype.saved = function() { + return this.fetch() > 0; + }; + + Model.prototype.query = function(query, callback) { + var data, search_key, _remote_filter; + data = this.fetch(); + search_key = this.context.get_opt("search_key"); + data = this.context.callbacks('filter').call(this.context, query, data, search_key) || []; + _remote_filter = this.context.callbacks('remote_filter'); + if (data.length > 0 || (!_remote_filter && data.length === 0)) { + return callback(data); + } else { + return _remote_filter.call(this.context, query, callback); + } + }; + + Model.prototype.fetch = function() { + return this.storage.data(this.at) || []; + }; + + Model.prototype.save = function(data) { + return this.storage.data(this.at, this.context.callbacks("before_save").call(this.context, data || [])); + }; + + Model.prototype.load = function(data) { + if (!(this.saved() || !data)) { + return this._load(data); + } + }; + + Model.prototype.reload = function(data) { + return this._load(data); + }; + + Model.prototype._load = function(data) { + if (typeof data === "string") { + return $.ajax(data, { + dataType: "json" + }).done((function(_this) { + return function(data) { + return _this.save(data); + }; + })(this)); + } else { + return this.save(data); + } + }; + + return Model; + +})(); + +View = (function() { + function View(context) { + this.context = context; + this.$el = $("
    "); + this.timeout_id = null; + this.context.$el.append(this.$el); + this.bind_event(); + } + + View.prototype.init = function() { + var id; + id = this.context.get_opt("alias") || this.context.at.charCodeAt(0); + return this.$el.attr({ + 'id': "at-view-" + id + }); + }; + + View.prototype.destroy = function() { + return this.$el.remove(); + }; + + View.prototype.bind_event = function() { + var $menu; + $menu = this.$el.find('ul'); + return $menu.on('mouseenter.atwho-view', 'li', function(e) { + $menu.find('.cur').removeClass('cur'); + return $(e.currentTarget).addClass('cur'); + }).on('click.atwho-view', 'li', (function(_this) { + return function(e) { + $menu.find('.cur').removeClass('cur'); + $(e.currentTarget).addClass('cur'); + _this.choose(e); + return e.preventDefault(); + }; + })(this)); + }; + + View.prototype.visible = function() { + return this.$el.is(":visible"); + }; + + View.prototype.choose = function(e) { + var $li, content; + if (($li = this.$el.find(".cur")).length) { + content = this.context.insert_content_for($li); + this.context.insert(this.context.callbacks("before_insert").call(this.context, content, $li), $li); + this.context.trigger("inserted", [$li, e]); + this.hide(e); + } + if (this.context.get_opt("hide_without_suffix")) { + return this.stop_showing = true; + } + }; + + View.prototype.reposition = function(rect) { + var offset, overflowOffset, _ref, _window; + _window = this.context.app.iframeStandalone ? this.context.app.window : window; + if (rect.bottom + this.$el.height() - $(_window).scrollTop() > $(_window).height()) { + rect.bottom = rect.top - this.$el.height(); + } + if (rect.left > (overflowOffset = $(_window).width() - this.$el.width() - 5)) { + rect.left = overflowOffset; + } + offset = { + left: rect.left, + top: rect.bottom + }; + if ((_ref = this.context.callbacks("before_reposition")) != null) { + _ref.call(this.context, offset); + } + this.$el.offset(offset); + return this.context.trigger("reposition", [offset]); + }; + + View.prototype.next = function() { + var cur, next; + cur = this.$el.find('.cur').removeClass('cur'); + next = cur.next(); + if (!next.length) { + next = this.$el.find('li:first'); + } + next.addClass('cur'); + return this.$el.animate({ + scrollTop: Math.max(0, cur.innerHeight() * (next.index() + 2) - this.$el.height()) + }, 150); + }; + + View.prototype.prev = function() { + var cur, prev; + cur = this.$el.find('.cur').removeClass('cur'); + prev = cur.prev(); + if (!prev.length) { + prev = this.$el.find('li:last'); + } + prev.addClass('cur'); + return this.$el.animate({ + scrollTop: Math.max(0, cur.innerHeight() * (prev.index() + 2) - this.$el.height()) + }, 150); + }; + + View.prototype.show = function() { + var rect; + if (this.stop_showing) { + this.stop_showing = false; + return; + } + this.context.mark_range(); + if (!this.visible()) { + this.$el.show(); + this.$el.scrollTop(0); + this.context.trigger('shown'); + } + if (rect = this.context.rect()) { + return this.reposition(rect); + } + }; + + View.prototype.hide = function(e, time) { + var callback; + if (!this.visible()) { + return; + } + if (isNaN(time)) { + this.context.reset_rect(); + this.$el.hide(); + return this.context.trigger('hidden', [e]); + } else { + callback = (function(_this) { + return function() { + return _this.hide(); + }; + })(this); + clearTimeout(this.timeout_id); + return this.timeout_id = setTimeout(callback, time); + } + }; + + View.prototype.render = function(list) { + var $li, $ul, item, li, tpl, _i, _len; + if (!($.isArray(list) && list.length > 0)) { + this.hide(); + return; + } + this.$el.find('ul').empty(); + $ul = this.$el.find('ul'); + tpl = this.context.get_opt('tpl'); + for (_i = 0, _len = list.length; _i < _len; _i++) { + item = list[_i]; + item = $.extend({}, item, { + 'atwho-at': this.context.at + }); + li = this.context.callbacks("tpl_eval").call(this.context, tpl, item); + $li = $(this.context.callbacks("highlighter").call(this.context, li, this.context.query.text)); + $li.data("item-data", item); + $ul.append($li); + } + this.show(); + if (this.context.get_opt('highlight_first')) { + return $ul.find("li:first").addClass("cur"); + } + }; + + return View; + +})(); + +KEY_CODE = { + DOWN: 40, + UP: 38, + ESC: 27, + TAB: 9, + ENTER: 13, + CTRL: 17, + P: 80, + N: 78 +}; + +DEFAULT_CALLBACKS = { + before_save: function(data) { + var item, _i, _len, _results; + if (!$.isArray(data)) { + return data; + } + _results = []; + for (_i = 0, _len = data.length; _i < _len; _i++) { + item = data[_i]; + if ($.isPlainObject(item)) { + _results.push(item); + } else { + _results.push({ + name: item + }); + } + } + return _results; + }, + matcher: function(flag, subtext, should_start_with_space) { + var match, regexp, _a, _y; + flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + if (should_start_with_space) { + flag = '(?:^|\\s)' + flag; + } + _a = decodeURI("%C3%80"); + _y = decodeURI("%C3%BF"); + regexp = new RegExp("" + flag + "([A-Za-z" + _a + "-" + _y + "0-9_\+\-]*)$|" + flag + "([^\\x00-\\xff]*)$", 'gi'); + match = regexp.exec(subtext); + if (match) { + return match[2] || match[1]; + } else { + return null; + } + }, + filter: function(query, data, search_key) { + var item, _i, _len, _results; + _results = []; + for (_i = 0, _len = data.length; _i < _len; _i++) { + item = data[_i]; + if (~new String(item[search_key]).toLowerCase().indexOf(query.toLowerCase())) { + _results.push(item); + } + } + return _results; + }, + remote_filter: null, + sorter: function(query, items, search_key) { + var item, _i, _len, _results; + if (!query) { + return items; + } + _results = []; + for (_i = 0, _len = items.length; _i < _len; _i++) { + item = items[_i]; + item.atwho_order = new String(item[search_key]).toLowerCase().indexOf(query.toLowerCase()); + if (item.atwho_order > -1) { + _results.push(item); + } + } + return _results.sort(function(a, b) { + return a.atwho_order - b.atwho_order; + }); + }, + tpl_eval: function(tpl, map) { + var error; + try { + return tpl.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) { + return map[key]; + }); + } catch (_error) { + error = _error; + return ""; + } + }, + highlighter: function(li, query) { + var regexp; + if (!query) { + return li; + } + regexp = new RegExp(">\\s*(\\w*?)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig'); + return li.replace(regexp, function(str, $1, $2, $3) { + return '> ' + $1 + '' + $2 + '' + $3 + ' <'; + }); + }, + before_insert: function(value, $li) { + return value; + }, + inserting_wrapper: function($inputor, content, suffix) { + var wrapped_content; + suffix = suffix === "" ? suffix : suffix || " "; + if ($inputor.is('textarea, input')) { + return '' + content + suffix; + } else if ($inputor.attr('contentEditable') === 'true') { + suffix = suffix === " " ? " " : suffix; + if (/firefox/i.test(navigator.userAgent)) { + wrapped_content = "" + content + suffix + ""; + } else { + suffix = "" + suffix + ""; + wrapped_content = "" + content + suffix + ""; + } + if (this.app.document.selection) { + wrapped_content = "" + content + ""; + } + return wrapped_content + ""; + } + } +}; + +Api = { + load: function(at, data) { + var c; + if (c = this.controller(at)) { + return c.model.load(data); + } + }, + setIframe: function(iframe, standalone) { + this.setIframe(iframe, standalone); + return null; + }, + run: function() { + return this.dispatch(); + }, + destroy: function() { + this.shutdown(); + return this.$inputor.data('atwho', null); + } +}; + +$.fn.atwho = function(method) { + var result, _args; + _args = arguments; + result = null; + this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function() { + var $this, app; + if (!(app = ($this = $(this)).data("atwho"))) { + $this.data('atwho', (app = new App(this))); + } + if (typeof method === 'object' || !method) { + return app.reg(method.at, method); + } else if (Api[method] && app) { + return result = Api[method].apply(app, Array.prototype.slice.call(_args, 1)); + } else { + return $.error("Method " + method + " does not exist on jQuery.caret"); + } + }); + return result || this; +}; + +$.fn.atwho["default"] = { + at: void 0, + alias: void 0, + data: null, + tpl: "
  • ${name}
  • ", + insert_tpl: "${atwho-data-value}", + callbacks: DEFAULT_CALLBACKS, + search_key: "name", + suffix: void 0, + hide_without_suffix: false, + start_with_space: true, + highlight_first: true, + limit: 5, + max_len: 20, + display_timeout: 300, + delay: null +}; + + + +})); diff --git a/media/js/vendor/at/jquery.caret.js b/media/js/vendor/at/jquery.caret.js new file mode 100644 index 0000000..fa1bffb --- /dev/null +++ b/media/js/vendor/at/jquery.caret.js @@ -0,0 +1,379 @@ +/* + Implement Github like autocomplete mentions + http://ichord.github.com/At.js + + Copyright (c) 2013 chord.luo@gmail.com + Licensed under the MIT license. +*/ + + +/* +本插件操作 textarea 或者 input 内的插入符 +只实现了获得插入符在文本框中的位置,我设置 +插入符的位置. +*/ + + +(function() { + (function(factory) { + if (typeof define === 'function' && define.amd) { + return define(['jquery'], factory); + } else { + return factory(window.jQuery); + } + })(function($) { + "use strict"; + var EditableCaret, InputCaret, Mirror, Utils, discoveryIframeOf, methods, oDocument, oFrame, oWindow, pluginName, setContextBy; + pluginName = 'caret'; + EditableCaret = (function() { + function EditableCaret($inputor) { + this.$inputor = $inputor; + this.domInputor = this.$inputor[0]; + } + + EditableCaret.prototype.setPos = function(pos) { + return this.domInputor; + }; + + EditableCaret.prototype.getIEPosition = function() { + return this.getPosition(); + }; + + EditableCaret.prototype.getPosition = function() { + var inputor_offset, offset; + offset = this.getOffset(); + inputor_offset = this.$inputor.offset(); + offset.left -= inputor_offset.left; + offset.top -= inputor_offset.top; + return offset; + }; + + EditableCaret.prototype.getOldIEPos = function() { + var preCaretTextRange, textRange; + textRange = oDocument.selection.createRange(); + preCaretTextRange = oDocument.body.createTextRange(); + preCaretTextRange.moveToElementText(this.domInputor); + preCaretTextRange.setEndPoint("EndToEnd", textRange); + return preCaretTextRange.text.length; + }; + + EditableCaret.prototype.getPos = function() { + var clonedRange, pos, range; + if (range = this.range()) { + clonedRange = range.cloneRange(); + clonedRange.selectNodeContents(this.domInputor); + clonedRange.setEnd(range.endContainer, range.endOffset); + pos = clonedRange.toString().length; + clonedRange.detach(); + return pos; + } else if (oDocument.selection) { + return this.getOldIEPos(); + } + }; + + EditableCaret.prototype.getOldIEOffset = function() { + var range, rect; + range = oDocument.selection.createRange().duplicate(); + range.moveStart("character", -1); + rect = range.getBoundingClientRect(); + return { + height: rect.bottom - rect.top, + left: rect.left, + top: rect.top + }; + }; + + EditableCaret.prototype.getOffset = function(pos) { + var clonedRange, offset, range, rect, shadowCaret; + if (oWindow.getSelection && (range = this.range())) { + if (range.endOffset - 1 > 0 && range.endContainer === !this.domInputor) { + clonedRange = range.cloneRange(); + clonedRange.setStart(range.endContainer, range.endOffset - 1); + clonedRange.setEnd(range.endContainer, range.endOffset); + rect = clonedRange.getBoundingClientRect(); + offset = { + height: rect.height, + left: rect.left + rect.width, + top: rect.top + }; + clonedRange.detach(); + } + if (!offset || (offset != null ? offset.height : void 0) === 0) { + clonedRange = range.cloneRange(); + shadowCaret = $(oDocument.createTextNode("|")); + clonedRange.insertNode(shadowCaret[0]); + clonedRange.selectNode(shadowCaret[0]); + rect = clonedRange.getBoundingClientRect(); + offset = { + height: rect.height, + left: rect.left, + top: rect.top + }; + shadowCaret.remove(); + clonedRange.detach(); + } + } else if (oDocument.selection) { + offset = this.getOldIEOffset(); + } + if (offset) { + offset.top += $(oWindow).scrollTop(); + offset.left += $(oWindow).scrollLeft(); + } + return offset; + }; + + EditableCaret.prototype.range = function() { + var sel; + if (!oWindow.getSelection) { + return; + } + sel = oWindow.getSelection(); + if (sel.rangeCount > 0) { + return sel.getRangeAt(0); + } else { + return null; + } + }; + + return EditableCaret; + + })(); + InputCaret = (function() { + function InputCaret($inputor) { + this.$inputor = $inputor; + this.domInputor = this.$inputor[0]; + } + + InputCaret.prototype.getIEPos = function() { + var endRange, inputor, len, normalizedValue, pos, range, textInputRange; + inputor = this.domInputor; + range = oDocument.selection.createRange(); + pos = 0; + if (range && range.parentElement() === inputor) { + normalizedValue = inputor.value.replace(/\r\n/g, "\n"); + len = normalizedValue.length; + textInputRange = inputor.createTextRange(); + textInputRange.moveToBookmark(range.getBookmark()); + endRange = inputor.createTextRange(); + endRange.collapse(false); + if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) { + pos = len; + } else { + pos = -textInputRange.moveStart("character", -len); + } + } + return pos; + }; + + InputCaret.prototype.getPos = function() { + if (oDocument.selection) { + return this.getIEPos(); + } else { + return this.domInputor.selectionStart; + } + }; + + InputCaret.prototype.setPos = function(pos) { + var inputor, range; + inputor = this.domInputor; + if (oDocument.selection) { + range = inputor.createTextRange(); + range.move("character", pos); + range.select(); + } else if (inputor.setSelectionRange) { + inputor.setSelectionRange(pos, pos); + } + return inputor; + }; + + InputCaret.prototype.getIEOffset = function(pos) { + var h, textRange, x, y; + textRange = this.domInputor.createTextRange(); + pos || (pos = this.getPos()); + textRange.move('character', pos); + x = textRange.boundingLeft; + y = textRange.boundingTop; + h = textRange.boundingHeight; + return { + left: x, + top: y, + height: h + }; + }; + + InputCaret.prototype.getOffset = function(pos) { + var $inputor, offset, position; + $inputor = this.$inputor; + if (oDocument.selection) { + offset = this.getIEOffset(pos); + offset.top += $(oWindow).scrollTop() + $inputor.scrollTop(); + offset.left += $(oWindow).scrollLeft() + $inputor.scrollLeft(); + return offset; + } else { + offset = $inputor.offset(); + position = this.getPosition(pos); + return offset = { + left: offset.left + position.left - $inputor.scrollLeft(), + top: offset.top + position.top - $inputor.scrollTop(), + height: position.height + }; + } + }; + + InputCaret.prototype.getPosition = function(pos) { + var $inputor, at_rect, end_range, format, html, mirror, start_range; + $inputor = this.$inputor; + format = function(value) { + return $('
    ').text(value).html().replace(/\r\n|\r|\n/g, "
    ").replace(/\s/g, " "); + }; + if (pos === void 0) { + pos = this.getPos(); + } + start_range = $inputor.val().slice(0, pos); + end_range = $inputor.val().slice(pos); + html = "" + format(start_range) + ""; + html += "|"; + html += "" + format(end_range) + ""; + mirror = new Mirror($inputor); + return at_rect = mirror.create(html).rect(); + }; + + InputCaret.prototype.getIEPosition = function(pos) { + var h, inputorOffset, offset, x, y; + offset = this.getIEOffset(pos); + inputorOffset = this.$inputor.offset(); + x = offset.left - inputorOffset.left; + y = offset.top - inputorOffset.top; + h = offset.height; + return { + left: x, + top: y, + height: h + }; + }; + + return InputCaret; + + })(); + Mirror = (function() { + Mirror.prototype.css_attr = ["borderBottomWidth", "borderLeftWidth", "borderRightWidth", "borderTopStyle", "borderRightStyle", "borderBottomStyle", "borderLeftStyle", "borderTopWidth", "boxSizing", "fontFamily", "fontSize", "fontWeight", "height", "letterSpacing", "lineHeight", "marginBottom", "marginLeft", "marginRight", "marginTop", "outlineWidth", "overflow", "overflowX", "overflowY", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "textAlign", "textOverflow", "textTransform", "whiteSpace", "wordBreak", "wordWrap"]; + + function Mirror($inputor) { + this.$inputor = $inputor; + } + + Mirror.prototype.mirrorCss = function() { + var css, + _this = this; + css = { + position: 'absolute', + left: -9999, + top: 0, + zIndex: -20000 + }; + if (this.$inputor.prop('tagName') === 'TEXTAREA') { + this.css_attr.push('width'); + } + $.each(this.css_attr, function(i, p) { + return css[p] = _this.$inputor.css(p); + }); + return css; + }; + + Mirror.prototype.create = function(html) { + this.$mirror = $('
    '); + this.$mirror.css(this.mirrorCss()); + this.$mirror.html(html); + this.$inputor.after(this.$mirror); + return this; + }; + + Mirror.prototype.rect = function() { + var $flag, pos, rect; + $flag = this.$mirror.find("#caret"); + pos = $flag.position(); + rect = { + left: pos.left, + top: pos.top, + height: $flag.height() + }; + this.$mirror.remove(); + return rect; + }; + + return Mirror; + + })(); + Utils = { + contentEditable: function($inputor) { + return !!($inputor[0].contentEditable && $inputor[0].contentEditable === 'true'); + } + }; + methods = { + pos: function(pos) { + if (pos || pos === 0) { + return this.setPos(pos); + } else { + return this.getPos(); + } + }, + position: function(pos) { + if (oDocument.selection) { + return this.getIEPosition(pos); + } else { + return this.getPosition(pos); + } + }, + offset: function(pos) { + var offset; + offset = this.getOffset(pos); + return offset; + } + }; + oDocument = null; + oWindow = null; + oFrame = null; + setContextBy = function(settings) { + var iframe; + if (iframe = settings != null ? settings.iframe : void 0) { + oFrame = iframe; + oWindow = iframe.contentWindow; + return oDocument = iframe.contentDocument || oWindow.document; + } else { + oFrame = void 0; + oWindow = window; + return oDocument = document; + } + }; + discoveryIframeOf = function($dom) { + var error; + oDocument = $dom[0].ownerDocument; + oWindow = oDocument.defaultView || oDocument.parentWindow; + try { + return oFrame = oWindow.frameElement; + } catch (_error) { + error = _error; + } + }; + $.fn.caret = function(method, value, settings) { + var caret; + if (methods[method]) { + if ($.isPlainObject(value)) { + setContextBy(value); + value = void 0; + } else { + setContextBy(settings); + } + caret = Utils.contentEditable(this) ? new EditableCaret(this) : new InputCaret(this); + return methods[method].apply(caret, [value]); + } else { + return $.error("Method " + method + " does not exist on jQuery.caret"); + } + }; + $.fn.caret.EditableCaret = EditableCaret; + $.fn.caret.InputCaret = InputCaret; + $.fn.caret.Utils = Utils; + return $.fn.caret.apis = methods; + }); + +}).call(this); diff --git a/media/js/vendor/backbone.keys/backbone.keys.js b/media/js/vendor/backbone.keys/backbone.keys.js new file mode 100644 index 0000000..61bfdb8 --- /dev/null +++ b/media/js/vendor/backbone.keys/backbone.keys.js @@ -0,0 +1,205 @@ +// Backbone.keys.js 0.1 + +// (c) 2012 Raymond Julin, Keyteq AS +// Backbone.keys may be freely distributed under the MIT license. +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['underscore', 'backbone'], factory); + } else { + // Browser globals + factory(_, Backbone); + } +}(function (_, Backbone) { + // Alias the libraries from the global object + var document = this.document; + var $ = this.$; + var oldDelegateEvents = Backbone.View.prototype.delegateEvents; + var getKeyCode = function(key) { + return (key.length === 1) ? + key.toUpperCase().charCodeAt(0) : BackboneKeysMap[key]; + }; + + // Map keyname to keycode + var BackboneKeysMap = { + backspace: 8, + tab: 9, + enter: 13, + space: 32, + + // Temporal modifiers + shift: 16, + ctrl: 17, + alt: 18, + meta: 91, + + // Modal + caps_lock: 20, + esc: 27, + num_lock: 144, + + // Navigation + page_up: 33, + page_down: 34, + end: 35, + home: 36, + left: 37, + up: 38, + right: 39, + down: 40, + + // Insert/delete + insert: 45, + 'delete': 46, + + // F keys + f1: 112, + f2: 113, + f3: 114, + f4: 115, + f5: 116, + f6: 117, + f7: 118, + f8: 119, + f9: 120, + f10: 121, + f11: 122, + f12: 123 + }; + + // Aliased names to make sense on several platforms + _.each({ + 'options' : 'alt', + 'return': 'enter' + }, function(real, alias) { + BackboneKeysMap[alias] = BackboneKeysMap[real]; + }); + + + Backbone.View = Backbone.View.extend({ + + // Allow pr view what specific event to use + // Keydown is defaulted as it allows for press-and-hold + bindKeysOn : 'keydown', + + // The Backbone-y way would be to have + // keys scoped to `this.el` as default, + // however it would be a bigger surprise + // considering how you'd expect keyboard + // events to work + // But users should be able to choose themselves + bindKeysScoped : false, + + // Hash of bound listeners + _keyEventBindings : null, + + // Override delegate events + delegateEvents : function(events) { + // First delegate original events + oldDelegateEvents.apply(this, (events || [])); + + // Now delegate keys + this.delegateKeys(); + }, + + // Actual delegate keys + delegateKeys : function(keys) { + this.undelegateKeys(); + keys = keys || (this.keys); + if (keys) { + _.each(keys, function(method, key) { + this.keyOn(key, method); + }, this); + // Bind to DOM element in order to forward key events + var bindTo = (this.bindKeysScoped || typeof $ === "undefined") ? this.$el : $(document); + bindTo.on(this.bindKeysOn, _.bind(this.triggerKey, this)); + } + }, + + // Undelegate keys + undelegateKeys : function() { + this._keyEventBindings = {}; + }, + + // Utility to get the name of a key + // based on its keyCode + keyName : function(keyCode) { + var keyName; + for (keyName in BackboneKeysMap) + if (BackboneKeysMap[keyName] === keyCode) return keyName; + return String.fromCharCode(keyCode); + }, + + // Internal real listener for key events that + // forwards any relevant key presses + triggerKey : function(e) { + var key; + if (_.isObject(e)) key = e.which; + else if (_.isString(e)) key = getKeyCode(e); + else if (_.isNumber(e)) key = e; + + _(this._keyEventBindings[key]).each(function(listener) { + var trigger = true; + if (listener.modifiers) { + trigger = _(listener.modifiers).all(function(modifier) { + return e[modifier + 'Key'] === true; + }); + } + if (trigger) listener.method(e, listener.key); + }); + }, + + // Doing the real work of binding key events + keyOn : function(key, method) { + key = key.split(' '); + if (key.length > 1) { + var l = key.length; + while (l--) + this.keyOn(key[l], method); + return; + } + else key = key.pop().toLowerCase(); + + // Subtract modifiers + var components = key.split('+'); + key = components.shift(); + + var keyCode = getKeyCode(key); + + if (!this._keyEventBindings.hasOwnProperty(keyCode)) + this._keyEventBindings[keyCode] = []; + + if (!_.isFunction(method)) + method = this[method]; + + this._keyEventBindings[keyCode].push({ + key : key, + modifiers : (components || false), + method: _.bind(method, this) + }); + }, + + keyOff : function(key, method) { + method = (method || false); + if (key === null) { + this._keyEventBindings = {}; + return this; + } + var keyCode = getKeyCode(key); + if (!_.isFunction(method)) method = this[method]; + if (!method) { + this._keyEventBindings[keyCode] = []; + return this; + } + this._keyEventBindings[keyCode] = _.filter( + this._keyEventBindings[keyCode], + function(data, index) { + return data.method === method; + } + ); + return this; + } + }); + + return Backbone; +})); diff --git a/media/js/vendor/backbone/backbone.js b/media/js/vendor/backbone/backbone.js new file mode 100644 index 0000000..24a550a --- /dev/null +++ b/media/js/vendor/backbone/backbone.js @@ -0,0 +1,1608 @@ +// Backbone.js 1.1.2 + +// (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org + +(function(root, factory) { + + // Set up Backbone appropriately for the environment. Start with AMD. + if (typeof define === 'function' && define.amd) { + define(['underscore', 'jquery', 'exports'], function(_, $, exports) { + // Export global even in AMD case in case this script is loaded with + // others that may still expect a global Backbone. + root.Backbone = factory(root, exports, _, $); + }); + + // Next for Node.js or CommonJS. jQuery may not be needed as a module. + } else if (typeof exports !== 'undefined') { + var _ = require('underscore'); + factory(root, exports, _); + + // Finally, as a browser global. + } else { + root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$)); + } + +}(this, function(root, Backbone, _, $) { + + // Initial Setup + // ------------- + + // Save the previous value of the `Backbone` variable, so that it can be + // restored later on, if `noConflict` is used. + var previousBackbone = root.Backbone; + + // Create local references to array methods we'll want to use later. + var array = []; + var push = array.push; + var slice = array.slice; + var splice = array.splice; + + // Current version of the library. Keep in sync with `package.json`. + Backbone.VERSION = '1.1.2'; + + // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns + // the `$` variable. + Backbone.$ = $; + + // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable + // to its previous owner. Returns a reference to this Backbone object. + Backbone.noConflict = function() { + root.Backbone = previousBackbone; + return this; + }; + + // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option + // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and + // set a `X-Http-Method-Override` header. + Backbone.emulateHTTP = false; + + // Turn on `emulateJSON` to support legacy servers that can't deal with direct + // `application/json` requests ... will encode the body as + // `application/x-www-form-urlencoded` instead and will send the model in a + // form param named `model`. + Backbone.emulateJSON = false; + + // Backbone.Events + // --------------- + + // A module that can be mixed in to *any object* in order to provide it with + // custom events. You may bind with `on` or remove with `off` callback + // functions to an event; `trigger`-ing an event fires all callbacks in + // succession. + // + // var object = {}; + // _.extend(object, Backbone.Events); + // object.on('expand', function(){ alert('expanded'); }); + // object.trigger('expand'); + // + var Events = Backbone.Events = { + + // Bind an event to a `callback` function. Passing `"all"` will bind + // the callback to all events fired. + on: function(name, callback, context) { + if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; + this._events || (this._events = {}); + var events = this._events[name] || (this._events[name] = []); + events.push({callback: callback, context: context, ctx: context || this}); + return this; + }, + + // Bind an event to only be triggered a single time. After the first time + // the callback is invoked, it will be removed. + once: function(name, callback, context) { + if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; + var self = this; + var once = _.once(function() { + self.off(name, once); + callback.apply(this, arguments); + }); + once._callback = callback; + return this.on(name, once, context); + }, + + // Remove one or many callbacks. If `context` is null, removes all + // callbacks with that function. If `callback` is null, removes all + // callbacks for the event. If `name` is null, removes all bound + // callbacks for all events. + off: function(name, callback, context) { + var retain, ev, events, names, i, l, j, k; + if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; + if (!name && !callback && !context) { + this._events = void 0; + return this; + } + names = name ? [name] : _.keys(this._events); + for (i = 0, l = names.length; i < l; i++) { + name = names[i]; + if (events = this._events[name]) { + this._events[name] = retain = []; + if (callback || context) { + for (j = 0, k = events.length; j < k; j++) { + ev = events[j]; + if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || + (context && context !== ev.context)) { + retain.push(ev); + } + } + } + if (!retain.length) delete this._events[name]; + } + } + + return this; + }, + + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + trigger: function(name) { + if (!this._events) return this; + var args = slice.call(arguments, 1); + if (!eventsApi(this, 'trigger', name, args)) return this; + var events = this._events[name]; + var allEvents = this._events.all; + if (events) triggerEvents(events, args); + if (allEvents) triggerEvents(allEvents, arguments); + return this; + }, + + // Tell this object to stop listening to either specific events ... or + // to every object it's currently listening to. + stopListening: function(obj, name, callback) { + var listeningTo = this._listeningTo; + if (!listeningTo) return this; + var remove = !name && !callback; + if (!callback && typeof name === 'object') callback = this; + if (obj) (listeningTo = {})[obj._listenId] = obj; + for (var id in listeningTo) { + obj = listeningTo[id]; + obj.off(name, callback, this); + if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id]; + } + return this; + } + + }; + + // Regular expression used to split event strings. + var eventSplitter = /\s+/; + + // Implement fancy features of the Events API such as multiple event + // names `"change blur"` and jQuery-style event maps `{change: action}` + // in terms of the existing API. + var eventsApi = function(obj, action, name, rest) { + if (!name) return true; + + // Handle event maps. + if (typeof name === 'object') { + for (var key in name) { + obj[action].apply(obj, [key, name[key]].concat(rest)); + } + return false; + } + + // Handle space separated event names. + if (eventSplitter.test(name)) { + var names = name.split(eventSplitter); + for (var i = 0, l = names.length; i < l; i++) { + obj[action].apply(obj, [names[i]].concat(rest)); + } + return false; + } + + return true; + }; + + // A difficult-to-believe, but optimized internal dispatch function for + // triggering events. Tries to keep the usual cases speedy (most internal + // Backbone events have 3 arguments). + var triggerEvents = function(events, args) { + var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; + switch (args.length) { + case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; + case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; + case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; + case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; + default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; + } + }; + + var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; + + // Inversion-of-control versions of `on` and `once`. Tell *this* object to + // listen to an event in another object ... keeping track of what it's + // listening to. + _.each(listenMethods, function(implementation, method) { + Events[method] = function(obj, name, callback) { + var listeningTo = this._listeningTo || (this._listeningTo = {}); + var id = obj._listenId || (obj._listenId = _.uniqueId('l')); + listeningTo[id] = obj; + if (!callback && typeof name === 'object') callback = this; + obj[implementation](name, callback, this); + return this; + }; + }); + + // Aliases for backwards compatibility. + Events.bind = Events.on; + Events.unbind = Events.off; + + // Allow the `Backbone` object to serve as a global event bus, for folks who + // want global "pubsub" in a convenient place. + _.extend(Backbone, Events); + + // Backbone.Model + // -------------- + + // Backbone **Models** are the basic data object in the framework -- + // frequently representing a row in a table in a database on your server. + // A discrete chunk of data and a bunch of useful, related methods for + // performing computations and transformations on that data. + + // Create a new model with the specified attributes. A client id (`cid`) + // is automatically generated and assigned for you. + var Model = Backbone.Model = function(attributes, options) { + var attrs = attributes || {}; + options || (options = {}); + this.cid = _.uniqueId('c'); + this.attributes = {}; + if (options.collection) this.collection = options.collection; + if (options.parse) attrs = this.parse(attrs, options) || {}; + attrs = _.defaults({}, attrs, _.result(this, 'defaults')); + this.set(attrs, options); + this.changed = {}; + this.initialize.apply(this, arguments); + }; + + // Attach all inheritable methods to the Model prototype. + _.extend(Model.prototype, Events, { + + // A hash of attributes whose current and previous value differ. + changed: null, + + // The value returned during the last failed validation. + validationError: null, + + // The default name for the JSON `id` attribute is `"id"`. MongoDB and + // CouchDB users may want to set this to `"_id"`. + idAttribute: 'id', + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Return a copy of the model's `attributes` object. + toJSON: function(options) { + return _.clone(this.attributes); + }, + + // Proxy `Backbone.sync` by default -- but override this if you need + // custom syncing semantics for *this* particular model. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + + // Get the HTML-escaped value of an attribute. + escape: function(attr) { + return _.escape(this.get(attr)); + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has: function(attr) { + return this.get(attr) != null; + }, + + // Set a hash of model attributes on the object, firing `"change"`. This is + // the core primitive operation of a model, updating the data and notifying + // anyone who needs to know about the change in state. The heart of the beast. + set: function(key, val, options) { + var attr, attrs, unset, changes, silent, changing, prev, current; + if (key == null) return this; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options || (options = {}); + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Extract attributes and options. + unset = options.unset; + silent = options.silent; + changes = []; + changing = this._changing; + this._changing = true; + + if (!changing) { + this._previousAttributes = _.clone(this.attributes); + this.changed = {}; + } + current = this.attributes, prev = this._previousAttributes; + + // Check for changes of `id`. + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + + // For each `set` attribute, update or delete the current value. + for (attr in attrs) { + val = attrs[attr]; + if (!_.isEqual(current[attr], val)) changes.push(attr); + if (!_.isEqual(prev[attr], val)) { + this.changed[attr] = val; + } else { + delete this.changed[attr]; + } + unset ? delete current[attr] : current[attr] = val; + } + + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = options; + for (var i = 0, l = changes.length; i < l; i++) { + this.trigger('change:' + changes[i], this, current[changes[i]], options); + } + } + + // You might be wondering why there's a `while` loop here. Changes can + // be recursively nested within `"change"` events. + if (changing) return this; + if (!silent) { + while (this._pending) { + options = this._pending; + this._pending = false; + this.trigger('change', this, options); + } + } + this._pending = false; + this._changing = false; + return this; + }, + + // Remove an attribute from the model, firing `"change"`. `unset` is a noop + // if the attribute doesn't exist. + unset: function(attr, options) { + return this.set(attr, void 0, _.extend({}, options, {unset: true})); + }, + + // Clear all attributes on the model, firing `"change"`. + clear: function(options) { + var attrs = {}; + for (var key in this.attributes) attrs[key] = void 0; + return this.set(attrs, _.extend({}, options, {unset: true})); + }, + + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (attr == null) return !_.isEmpty(this.changed); + return _.has(this.changed, attr); + }, + + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; + var val, changed = false; + var old = this._changing ? this._previousAttributes : this.attributes; + for (var attr in diff) { + if (_.isEqual(old[attr], (val = diff[attr]))) continue; + (changed || (changed = {}))[attr] = val; + } + return changed; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (attr == null || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + return _.clone(this._previousAttributes); + }, + + // Fetch the model from the server. If the server's representation of the + // model differs from its current attributes, they will be overridden, + // triggering a `"change"` event. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === void 0) options.parse = true; + var model = this; + var success = options.success; + options.success = function(resp) { + if (!model.set(model.parse(resp, options), options)) return false; + if (success) success(model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Set a hash of model attributes, and sync the model to the server. + // If the server returns an attributes hash that differs, the model's + // state will be `set` again. + save: function(key, val, options) { + var attrs, method, xhr, attributes = this.attributes; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (key == null || typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options = _.extend({validate: true}, options); + + // If we're not waiting and attributes exist, save acts as + // `set(attr).save(null, opts)` with validation. Otherwise, check if + // the model will be valid when the attributes, if any, are set. + if (attrs && !options.wait) { + if (!this.set(attrs, options)) return false; + } else { + if (!this._validate(attrs, options)) return false; + } + + // Set temporary attributes if `{wait: true}`. + if (attrs && options.wait) { + this.attributes = _.extend({}, attributes, attrs); + } + + // After a successful server-side save, the client is (optionally) + // updated with the server-side state. + if (options.parse === void 0) options.parse = true; + var model = this; + var success = options.success; + options.success = function(resp) { + // Ensure attributes are restored during synchronous saves. + model.attributes = attributes; + var serverAttrs = model.parse(resp, options); + if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); + if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { + return false; + } + if (success) success(model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + + method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); + if (method === 'patch') options.attrs = attrs; + xhr = this.sync(method, this, options); + + // Restore attributes. + if (attrs && options.wait) this.attributes = attributes; + + return xhr; + }, + + // Destroy this model on the server if it was already persisted. + // Optimistically removes the model from its collection, if it has one. + // If `wait: true` is passed, waits for the server to respond before removal. + destroy: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + + var destroy = function() { + model.trigger('destroy', model, model.collection, options); + }; + + options.success = function(resp) { + if (options.wait || model.isNew()) destroy(); + if (success) success(model, resp, options); + if (!model.isNew()) model.trigger('sync', model, resp, options); + }; + + if (this.isNew()) { + options.success(); + return false; + } + wrapError(this, options); + + var xhr = this.sync('delete', this, options); + if (!options.wait) destroy(); + return xhr; + }, + + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url: function() { + var base = + _.result(this, 'urlRoot') || + _.result(this.collection, 'url') || + urlError(); + if (this.isNew()) return base; + return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id); + }, + + // **parse** converts a response into the hash of attributes to be `set` on + // the model. The default implementation is just to pass the response along. + parse: function(resp, options) { + return resp; + }, + + // Create a new model with identical attributes to this one. + clone: function() { + return new this.constructor(this.attributes); + }, + + // A model is new if it has never been saved to the server, and lacks an id. + isNew: function() { + return !this.has(this.idAttribute); + }, + + // Check if the model is currently in a valid state. + isValid: function(options) { + return this._validate({}, _.extend(options || {}, { validate: true })); + }, + + // Run validation against the next complete set of model attributes, + // returning `true` if all is well. Otherwise, fire an `"invalid"` event. + _validate: function(attrs, options) { + if (!options.validate || !this.validate) return true; + attrs = _.extend({}, this.attributes, attrs); + var error = this.validationError = this.validate(attrs, options) || null; + if (!error) return true; + this.trigger('invalid', this, error, _.extend(options, {validationError: error})); + return false; + } + + }); + + // Underscore methods that we want to implement on the Model. + var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; + + // Mix in each Underscore method as a proxy to `Model#attributes`. + _.each(modelMethods, function(method) { + Model.prototype[method] = function() { + var args = slice.call(arguments); + args.unshift(this.attributes); + return _[method].apply(_, args); + }; + }); + + // Backbone.Collection + // ------------------- + + // If models tend to represent a single row of data, a Backbone Collection is + // more analagous to a table full of data ... or a small slice or page of that + // table, or a collection of rows that belong together for a particular reason + // -- all of the messages in this particular folder, all of the documents + // belonging to this particular author, and so on. Collections maintain + // indexes of their models, both in order, and for lookup by `id`. + + // Create a new **Collection**, perhaps to contain a specific type of `model`. + // If a `comparator` is specified, the Collection will maintain + // its models in sort order, as they're added and removed. + var Collection = Backbone.Collection = function(models, options) { + options || (options = {}); + if (options.model) this.model = options.model; + if (options.comparator !== void 0) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, _.extend({silent: true}, options)); + }; + + // Default options for `Collection#set`. + var setOptions = {add: true, remove: true, merge: true}; + var addOptions = {add: true, remove: false}; + + // Define the Collection's inheritable methods. + _.extend(Collection.prototype, Events, { + + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. + model: Model, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // The JSON representation of a Collection is an array of the + // models' attributes. + toJSON: function(options) { + return this.map(function(model){ return model.toJSON(options); }); + }, + + // Proxy `Backbone.sync` by default. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Add a model, or list of models to the set. + add: function(models, options) { + return this.set(models, _.extend({merge: false}, options, addOptions)); + }, + + // Remove a model, or a list of models from the set. + remove: function(models, options) { + var singular = !_.isArray(models); + models = singular ? [models] : _.clone(models); + options || (options = {}); + var i, l, index, model; + for (i = 0, l = models.length; i < l; i++) { + model = models[i] = this.get(models[i]); + if (!model) continue; + delete this._byId[model.id]; + delete this._byId[model.cid]; + index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + this._removeReference(model, options); + } + return singular ? models[0] : models; + }, + + // Update a collection by `set`-ing a new list of models, adding new ones, + // removing models that are no longer present, and merging models that + // already exist in the collection, as necessary. Similar to **Model#set**, + // the core operation for updating the data contained by the collection. + set: function(models, options) { + options = _.defaults({}, options, setOptions); + if (options.parse) models = this.parse(models, options); + var singular = !_.isArray(models); + models = singular ? (models ? [models] : []) : _.clone(models); + var i, l, id, model, attrs, existing, sort; + var at = options.at; + var targetModel = this.model; + var sortable = this.comparator && (at == null) && options.sort !== false; + var sortAttr = _.isString(this.comparator) ? this.comparator : null; + var toAdd = [], toRemove = [], modelMap = {}; + var add = options.add, merge = options.merge, remove = options.remove; + var order = !sortable && add && remove ? [] : false; + + // Turn bare objects into model references, and prevent invalid models + // from being added. + for (i = 0, l = models.length; i < l; i++) { + attrs = models[i] || {}; + if (attrs instanceof Model) { + id = model = attrs; + } else { + id = attrs[targetModel.prototype.idAttribute || 'id']; + } + + // If a duplicate is found, prevent it from being added and + // optionally merge it into the existing model. + if (existing = this.get(id)) { + if (remove) modelMap[existing.cid] = true; + if (merge) { + attrs = attrs === model ? model.attributes : attrs; + if (options.parse) attrs = existing.parse(attrs, options); + existing.set(attrs, options); + if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; + } + models[i] = existing; + + // If this is a new, valid model, push it to the `toAdd` list. + } else if (add) { + model = models[i] = this._prepareModel(attrs, options); + if (!model) continue; + toAdd.push(model); + this._addReference(model, options); + } + + // Do not add multiple models with the same `id`. + model = existing || model; + if (order && (model.isNew() || !modelMap[model.id])) order.push(model); + modelMap[model.id] = true; + } + + // Remove nonexistent models if appropriate. + if (remove) { + for (i = 0, l = this.length; i < l; ++i) { + if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); + } + if (toRemove.length) this.remove(toRemove, options); + } + + // See if sorting is needed, update `length` and splice in new models. + if (toAdd.length || (order && order.length)) { + if (sortable) sort = true; + this.length += toAdd.length; + if (at != null) { + for (i = 0, l = toAdd.length; i < l; i++) { + this.models.splice(at + i, 0, toAdd[i]); + } + } else { + if (order) this.models.length = 0; + var orderedModels = order || toAdd; + for (i = 0, l = orderedModels.length; i < l; i++) { + this.models.push(orderedModels[i]); + } + } + } + + // Silently sort the collection if appropriate. + if (sort) this.sort({silent: true}); + + // Unless silenced, it's time to fire all appropriate add/sort events. + if (!options.silent) { + for (i = 0, l = toAdd.length; i < l; i++) { + (model = toAdd[i]).trigger('add', model, this, options); + } + if (sort || (order && order.length)) this.trigger('sort', this, options); + } + + // Return the added (or merged) model (or models). + return singular ? models[0] : models; + }, + + // When you have more items than you want to add or remove individually, + // you can reset the entire set with a new list of models, without firing + // any granular `add` or `remove` events. Fires `reset` when finished. + // Useful for bulk operations and optimizations. + reset: function(models, options) { + options || (options = {}); + for (var i = 0, l = this.models.length; i < l; i++) { + this._removeReference(this.models[i], options); + } + options.previousModels = this.models; + this._reset(); + models = this.add(models, _.extend({silent: true}, options)); + if (!options.silent) this.trigger('reset', this, options); + return models; + }, + + // Add a model to the end of the collection. + push: function(model, options) { + return this.add(model, _.extend({at: this.length}, options)); + }, + + // Remove a model from the end of the collection. + pop: function(options) { + var model = this.at(this.length - 1); + this.remove(model, options); + return model; + }, + + // Add a model to the beginning of the collection. + unshift: function(model, options) { + return this.add(model, _.extend({at: 0}, options)); + }, + + // Remove a model from the beginning of the collection. + shift: function(options) { + var model = this.at(0); + this.remove(model, options); + return model; + }, + + // Slice out a sub-array of models from the collection. + slice: function() { + return slice.apply(this.models, arguments); + }, + + // Get a model from the set by id. + get: function(obj) { + if (obj == null) return void 0; + return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid]; + }, + + // Get the model at the given index. + at: function(index) { + return this.models[index]; + }, + + // Return models with matching attributes. Useful for simple cases of + // `filter`. + where: function(attrs, first) { + if (_.isEmpty(attrs)) return first ? void 0 : []; + return this[first ? 'find' : 'filter'](function(model) { + for (var key in attrs) { + if (attrs[key] !== model.get(key)) return false; + } + return true; + }); + }, + + // Return the first model with matching attributes. Useful for simple cases + // of `find`. + findWhere: function(attrs) { + return this.where(attrs, true); + }, + + // Force the collection to re-sort itself. You don't need to call this under + // normal circumstances, as the set will maintain sort order as each item + // is added. + sort: function(options) { + if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); + options || (options = {}); + + // Run sort based on type of `comparator`. + if (_.isString(this.comparator) || this.comparator.length === 1) { + this.models = this.sortBy(this.comparator, this); + } else { + this.models.sort(_.bind(this.comparator, this)); + } + + if (!options.silent) this.trigger('sort', this, options); + return this; + }, + + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return _.invoke(this.models, 'get', attr); + }, + + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `reset: true` is passed, the response + // data will be passed through the `reset` method instead of `set`. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === void 0) options.parse = true; + var success = options.success; + var collection = this; + options.success = function(resp) { + var method = options.reset ? 'reset' : 'set'; + collection[method](resp, options); + if (success) success(collection, resp, options); + collection.trigger('sync', collection, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Create a new instance of a model in this collection. Add the model to the + // collection immediately, unless `wait: true` is passed, in which case we + // wait for the server to agree. + create: function(model, options) { + options = options ? _.clone(options) : {}; + if (!(model = this._prepareModel(model, options))) return false; + if (!options.wait) this.add(model, options); + var collection = this; + var success = options.success; + options.success = function(model, resp) { + if (options.wait) collection.add(model, options); + if (success) success(model, resp, options); + }; + model.save(null, options); + return model; + }, + + // **parse** converts a response into a list of models to be added to the + // collection. The default implementation is just to pass it through. + parse: function(resp, options) { + return resp; + }, + + // Create a new collection with an identical list of models as this one. + clone: function() { + return new this.constructor(this.models); + }, + + // Private method to reset all internal state. Called when the collection + // is first initialized or reset. + _reset: function() { + this.length = 0; + this.models = []; + this._byId = {}; + }, + + // Prepare a hash of attributes (or other model) to be added to this + // collection. + _prepareModel: function(attrs, options) { + if (attrs instanceof Model) return attrs; + options = options ? _.clone(options) : {}; + options.collection = this; + var model = new this.model(attrs, options); + if (!model.validationError) return model; + this.trigger('invalid', this, model.validationError, options); + return false; + }, + + // Internal method to create a model's ties to a collection. + _addReference: function(model, options) { + this._byId[model.cid] = model; + if (model.id != null) this._byId[model.id] = model; + if (!model.collection) model.collection = this; + model.on('all', this._onModelEvent, this); + }, + + // Internal method to sever a model's ties to a collection. + _removeReference: function(model, options) { + if (this === model.collection) delete model.collection; + model.off('all', this._onModelEvent, this); + }, + + // Internal method called every time a model in the set fires an event. + // Sets need to update their indexes when models change ids. All other + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent: function(event, model, collection, options) { + if ((event === 'add' || event === 'remove') && collection !== this) return; + if (event === 'destroy') this.remove(model, options); + if (model && event === 'change:' + model.idAttribute) { + delete this._byId[model.previous(model.idAttribute)]; + if (model.id != null) this._byId[model.id] = model; + } + this.trigger.apply(this, arguments); + } + + }); + + // Underscore methods that we want to implement on the Collection. + // 90% of the core usefulness of Backbone Collections is actually implemented + // right here: + var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', + 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', + 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', + 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', + 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle', + 'lastIndexOf', 'isEmpty', 'chain', 'sample']; + + // Mix in each Underscore method as a proxy to `Collection#models`. + _.each(methods, function(method) { + Collection.prototype[method] = function() { + var args = slice.call(arguments); + args.unshift(this.models); + return _[method].apply(_, args); + }; + }); + + // Underscore methods that take a property name as an argument. + var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy']; + + // Use attributes instead of properties. + _.each(attributeMethods, function(method) { + Collection.prototype[method] = function(value, context) { + var iterator = _.isFunction(value) ? value : function(model) { + return model.get(value); + }; + return _[method](this.models, iterator, context); + }; + }); + + // Backbone.View + // ------------- + + // Backbone Views are almost more convention than they are actual code. A View + // is simply a JavaScript object that represents a logical chunk of UI in the + // DOM. This might be a single item, an entire list, a sidebar or panel, or + // even the surrounding frame which wraps your whole app. Defining a chunk of + // UI as a **View** allows you to define your DOM events declaratively, without + // having to worry about render order ... and makes it easy for the view to + // react to specific changes in the state of your models. + + // Creating a Backbone.View creates its initial element outside of the DOM, + // if an existing element is not provided... + var View = Backbone.View = function(options) { + this.cid = _.uniqueId('view'); + options || (options = {}); + _.extend(this, _.pick(options, viewOptions)); + this._ensureElement(); + this.initialize.apply(this, arguments); + this.delegateEvents(); + }; + + // Cached regex to split keys for `delegate`. + var delegateEventSplitter = /^(\S+)\s*(.*)$/; + + // List of view options to be merged as properties. + var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; + + // Set up all inheritable **Backbone.View** properties and methods. + _.extend(View.prototype, Events, { + + // The default `tagName` of a View's element is `"div"`. + tagName: 'div', + + // jQuery delegate for element lookup, scoped to DOM elements within the + // current view. This should be preferred to global lookups where possible. + $: function(selector) { + return this.$el.find(selector); + }, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // **render** is the core function that your view should override, in order + // to populate its element (`this.el`), with the appropriate HTML. The + // convention is for **render** to always return `this`. + render: function() { + return this; + }, + + // Remove this view by taking the element out of the DOM, and removing any + // applicable Backbone.Events listeners. + remove: function() { + this.$el.remove(); + this.stopListening(); + return this; + }, + + // Change the view's element (`this.el` property), including event + // re-delegation. + setElement: function(element, delegate) { + if (this.$el) this.undelegateEvents(); + this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); + this.el = this.$el[0]; + if (delegate !== false) this.delegateEvents(); + return this; + }, + + // Set callbacks, where `this.events` is a hash of + // + // *{"event selector": "callback"}* + // + // { + // 'mousedown .title': 'edit', + // 'click .button': 'save', + // 'click .open': function(e) { ... } + // } + // + // pairs. Callbacks will be bound to the view, with `this` set properly. + // Uses event delegation for efficiency. + // Omitting the selector binds the event to `this.el`. + // This only works for delegate-able events: not `focus`, `blur`, and + // not `change`, `submit`, and `reset` in Internet Explorer. + delegateEvents: function(events) { + if (!(events || (events = _.result(this, 'events')))) return this; + this.undelegateEvents(); + for (var key in events) { + var method = events[key]; + if (!_.isFunction(method)) method = this[events[key]]; + if (!method) continue; + + var match = key.match(delegateEventSplitter); + var eventName = match[1], selector = match[2]; + method = _.bind(method, this); + eventName += '.delegateEvents' + this.cid; + if (selector === '') { + this.$el.on(eventName, method); + } else { + this.$el.on(eventName, selector, method); + } + } + return this; + }, + + // Clears all callbacks previously bound to the view with `delegateEvents`. + // You usually don't need to use this, but may wish to if you have multiple + // Backbone views attached to the same DOM element. + undelegateEvents: function() { + this.$el.off('.delegateEvents' + this.cid); + return this; + }, + + // Ensure that the View has a DOM element to render into. + // If `this.el` is a string, pass it through `$()`, take the first + // matching element, and re-assign it to `el`. Otherwise, create + // an element from the `id`, `className` and `tagName` properties. + _ensureElement: function() { + if (!this.el) { + var attrs = _.extend({}, _.result(this, 'attributes')); + if (this.id) attrs.id = _.result(this, 'id'); + if (this.className) attrs['class'] = _.result(this, 'className'); + var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); + this.setElement($el, false); + } else { + this.setElement(_.result(this, 'el'), false); + } + } + + }); + + // Backbone.sync + // ------------- + + // Override this function to change the manner in which Backbone persists + // models to the server. You will be passed the type of request, and the + // model in question. By default, makes a RESTful Ajax request + // to the model's `url()`. Some possible customizations could be: + // + // * Use `setTimeout` to batch rapid-fire updates into a single request. + // * Send up the models as XML instead of JSON. + // * Persist models via WebSockets instead of Ajax. + // + // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests + // as `POST`, with a `_method` parameter containing the true HTTP method, + // as well as all requests with the body as `application/x-www-form-urlencoded` + // instead of `application/json` with the model in a param named `model`. + // Useful when interfacing with server-side languages like **PHP** that make + // it difficult to read the body of `PUT` requests. + Backbone.sync = function(method, model, options) { + var type = methodMap[method]; + + // Default options, unless specified. + _.defaults(options || (options = {}), { + emulateHTTP: Backbone.emulateHTTP, + emulateJSON: Backbone.emulateJSON + }); + + // Default JSON-request options. + var params = {type: type, dataType: 'json'}; + + // Ensure that we have a URL. + if (!options.url) { + params.url = _.result(model, 'url') || urlError(); + } + + // Ensure that we have the appropriate request data. + if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { + params.contentType = 'application/json'; + params.data = JSON.stringify(options.attrs || model.toJSON(options)); + } + + // For older servers, emulate JSON by encoding the request into an HTML-form. + if (options.emulateJSON) { + params.contentType = 'application/x-www-form-urlencoded'; + params.data = params.data ? {model: params.data} : {}; + } + + // For older servers, emulate HTTP by mimicking the HTTP method with `_method` + // And an `X-HTTP-Method-Override` header. + if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { + params.type = 'POST'; + if (options.emulateJSON) params.data._method = type; + var beforeSend = options.beforeSend; + options.beforeSend = function(xhr) { + xhr.setRequestHeader('X-HTTP-Method-Override', type); + if (beforeSend) return beforeSend.apply(this, arguments); + }; + } + + // Don't process data on a non-GET request. + if (params.type !== 'GET' && !options.emulateJSON) { + params.processData = false; + } + + // If we're sending a `PATCH` request, and we're in an old Internet Explorer + // that still has ActiveX enabled by default, override jQuery to use that + // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. + if (params.type === 'PATCH' && noXhrPatch) { + params.xhr = function() { + return new ActiveXObject("Microsoft.XMLHTTP"); + }; + } + + // Make the request, allowing the user to override any Ajax options. + var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); + model.trigger('request', model, xhr, options); + return xhr; + }; + + var noXhrPatch = + typeof window !== 'undefined' && !!window.ActiveXObject && + !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent); + + // Map from CRUD to HTTP for our default `Backbone.sync` implementation. + var methodMap = { + 'create': 'POST', + 'update': 'PUT', + 'patch': 'PATCH', + 'delete': 'DELETE', + 'read': 'GET' + }; + + // Set the default implementation of `Backbone.ajax` to proxy through to `$`. + // Override this if you'd like to use a different library. + Backbone.ajax = function() { + return Backbone.$.ajax.apply(Backbone.$, arguments); + }; + + // Backbone.Router + // --------------- + + // Routers map faux-URLs to actions, and fire events when routes are + // matched. Creating a new one sets its `routes` hash, if not set statically. + var Router = Backbone.Router = function(options) { + options || (options = {}); + if (options.routes) this.routes = options.routes; + this._bindRoutes(); + this.initialize.apply(this, arguments); + }; + + // Cached regular expressions for matching named param parts and splatted + // parts of route strings. + var optionalParam = /\((.*?)\)/g; + var namedParam = /(\(\?)?:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; + + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Router.prototype, Events, { + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Manually bind a single named route to a callback. For example: + // + // this.route('search/:query/p:num', 'search', function(query, num) { + // ... + // }); + // + route: function(route, name, callback) { + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (_.isFunction(name)) { + callback = name; + name = ''; + } + if (!callback) callback = this[name]; + var router = this; + Backbone.history.route(route, function(fragment) { + var args = router._extractParameters(route, fragment); + router.execute(callback, args); + router.trigger.apply(router, ['route:' + name].concat(args)); + router.trigger('route', name, args); + Backbone.history.trigger('route', router, name, args); + }); + return this; + }, + + // Execute a route handler with the provided parameters. This is an + // excellent place to do pre-route setup or post-route cleanup. + execute: function(callback, args) { + if (callback) callback.apply(this, args); + }, + + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate: function(fragment, options) { + Backbone.history.navigate(fragment, options); + return this; + }, + + // Bind all defined routes to `Backbone.history`. We have to reverse the + // order of the routes here to support behavior where the most general + // routes can be defined at the bottom of the route map. + _bindRoutes: function() { + if (!this.routes) return; + this.routes = _.result(this, 'routes'); + var route, routes = _.keys(this.routes); + while ((route = routes.pop()) != null) { + this.route(route, this.routes[route]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(optionalParam, '(?:$1)?') + .replace(namedParam, function(match, optional) { + return optional ? match : '([^/?]+)'; + }) + .replace(splatParam, '([^?]*?)'); + return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted decoded parameters. Empty or unmatched parameters will be + // treated as `null` to normalize cross-browser behavior. + _extractParameters: function(route, fragment) { + var params = route.exec(fragment).slice(1); + return _.map(params, function(param, i) { + // Don't decode the search params. + if (i === params.length - 1) return param || null; + return param ? decodeURIComponent(param) : null; + }); + } + + }); + + // Backbone.History + // ---------------- + + // Handles cross-browser history management, based on either + // [pushState](http://diveintohtml5.info/history.html) and real URLs, or + // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) + // and URL fragments. If the browser supports neither (old IE, natch), + // falls back to polling. + var History = Backbone.History = function() { + this.handlers = []; + _.bindAll(this, 'checkUrl'); + + // Ensure that `History` can be used outside of the browser. + if (typeof window !== 'undefined') { + this.location = window.location; + this.history = window.history; + } + }; + + // Cached regex for stripping a leading hash/slash and trailing space. + var routeStripper = /^[#\/]|\s+$/g; + + // Cached regex for stripping leading and trailing slashes. + var rootStripper = /^\/+|\/+$/g; + + // Cached regex for detecting MSIE. + var isExplorer = /msie [\w.]+/; + + // Cached regex for removing a trailing slash. + var trailingSlash = /\/$/; + + // Cached regex for stripping urls of hash. + var pathStripper = /#.*$/; + + // Has the history handling already been started? + History.started = false; + + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(History.prototype, Events, { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // Are we at the app root? + atRoot: function() { + return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root; + }, + + // Gets the true hash value. Cannot use location.hash directly due to bug + // in Firefox where location.hash will always be decoded. + getHash: function(window) { + var match = (window || this).location.href.match(/#(.*)$/); + return match ? match[1] : ''; + }, + + // Get the cross-browser normalized URL fragment, either from the URL, + // the hash, or the override. + getFragment: function(fragment, forcePushState) { + if (fragment == null) { + if (this._hasPushState || !this._wantsHashChange || forcePushState) { + fragment = decodeURI(this.location.pathname + this.location.search); + var root = this.root.replace(trailingSlash, ''); + if (!fragment.indexOf(root)) fragment = fragment.slice(root.length); + } else { + fragment = this.getHash(); + } + } + return fragment.replace(routeStripper, ''); + }, + + // Start the hash change handling, returning `true` if the current URL matches + // an existing route, and `false` otherwise. + start: function(options) { + if (History.started) throw new Error("Backbone.history has already been started"); + History.started = true; + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + this.options = _.extend({root: '/'}, this.options, options); + this.root = this.options.root; + this._wantsHashChange = this.options.hashChange !== false; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); + + // Normalize root to always include a leading and trailing slash. + this.root = ('/' + this.root + '/').replace(rootStripper, '/'); + + if (oldIE && this._wantsHashChange) { + var frame = Backbone.$('') + storageContainer.close() + storageOwner = storageContainer.w.frames[0].document + storage = storageOwner.createElement('div') + } catch(e) { + // somehow ActiveXObject instantiation failed (perhaps some special + // security settings or otherwse), fall back to per-path storage + storage = doc.createElement('div') + storageOwner = doc.body + } + var withIEStorage = function(storeFunction) { + return function() { + var args = Array.prototype.slice.call(arguments, 0) + args.unshift(storage) + // See http://msdn.microsoft.com/en-us/library/ms531081(v=VS.85).aspx + // and http://msdn.microsoft.com/en-us/library/ms531424(v=VS.85).aspx + storageOwner.appendChild(storage) + storage.addBehavior('#default#userData') + storage.load(localStorageName) + var result = storeFunction.apply(store, args) + storageOwner.removeChild(storage) + return result + } + } + + // In IE7, keys cannot start with a digit or contain certain chars. + // See https://github.com/marcuswestin/store.js/issues/40 + // See https://github.com/marcuswestin/store.js/issues/83 + var forbiddenCharsRegex = new RegExp("[!\"#$%&'()*+,/\\\\:;<=>?@[\\]^`{|}~]", "g") + function ieKeyFix(key) { + return key.replace(/^d/, '___$&').replace(forbiddenCharsRegex, '___') + } + store.set = withIEStorage(function(storage, key, val) { + key = ieKeyFix(key) + if (val === undefined) { return store.remove(key) } + storage.setAttribute(key, store.serialize(val)) + storage.save(localStorageName) + return val + }) + store.get = withIEStorage(function(storage, key, defaultVal) { + key = ieKeyFix(key) + var val = store.deserialize(storage.getAttribute(key)) + return (val === undefined ? defaultVal : val) + }) + store.remove = withIEStorage(function(storage, key) { + key = ieKeyFix(key) + storage.removeAttribute(key) + storage.save(localStorageName) + }) + store.clear = withIEStorage(function(storage) { + var attributes = storage.XMLDocument.documentElement.attributes + storage.load(localStorageName) + for (var i=0, attr; attr=attributes[i]; i++) { + storage.removeAttribute(attr.name) + } + storage.save(localStorageName) + }) + store.getAll = function(storage) { + var ret = {} + store.forEach(function(key, val) { + ret[key] = val + }) + return ret + } + store.forEach = withIEStorage(function(storage, callback) { + var attributes = storage.XMLDocument.documentElement.attributes + for (var i=0, attr; attr=attributes[i]; ++i) { + callback(attr.name, store.deserialize(storage.getAttribute(attr.name))) + } + }) + } + + try { + var testKey = '__storejs__' + store.set(testKey, testKey) + if (store.get(testKey) != testKey) { store.disabled = true } + store.remove(testKey) + } catch(e) { + store.disabled = true + } + store.enabled = !store.disabled + + if (typeof module != 'undefined' && module.exports && this.module !== module) { module.exports = store } + else if (typeof define === 'function' && define.amd) { define(store) } + else { win.store = store } + +})(Function('return this')()); \ No newline at end of file diff --git a/media/js/vendor/sweetalert/sweet-alert.css b/media/js/vendor/sweetalert/sweet-alert.css new file mode 100755 index 0000000..d83b8a6 --- /dev/null +++ b/media/js/vendor/sweetalert/sweet-alert.css @@ -0,0 +1,606 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:400,600,700,300); +.sweet-overlay { + background-color: rgba(0, 0, 0, 0.4); + position: fixed; + left: 0; + right: 0; + top: 0; + bottom: 0; + display: none; + z-index: 1000; } + +.sweet-alert { + background-color: white; + font-family: 'Open Sans', sans-serif; + width: 478px; + padding: 17px; + border-radius: 5px; + text-align: center; + position: fixed; + left: 50%; + top: 50%; + margin-left: -256px; + margin-top: -200px; + overflow: hidden; + display: none; + z-index: 2000; } + @media all and (max-width: 540px) { + .sweet-alert { + width: auto; + margin-left: 0; + margin-right: 0; + left: 15px; + right: 15px; } } + .sweet-alert h2 { + color: #575757; + font-size: 30px; + text-align: center; + font-weight: 600; + text-transform: none; + position: relative; + margin: 25px 0; + padding: 0; + line-height: 40px; + display: block; } + .sweet-alert p { + color: #797979; + font-size: 16px; + text-align: center; + font-weight: 300; + position: relative; + text-align: inherit; + float: none; + margin: 0; + padding: 0; + line-height: normal; } + .sweet-alert button { + background-color: #AEDEF4; + color: white; + border: none; + box-shadow: none; + font-size: 17px; + font-weight: 500; + border-radius: 5px; + padding: 10px 32px; + margin: 26px 5px 0 5px; + cursor: pointer; } + .sweet-alert button:focus { + outline: none; + box-shadow: 0 0 2px rgba(128, 179, 235, 0.5), inset 0 0 0 1px rgba(0, 0, 0, 0.05); } + .sweet-alert button:hover { + background-color: #a1d9f2; } + .sweet-alert button:active { + background-color: #81ccee; } + .sweet-alert button.cancel { + background-color: #D0D0D0; } + .sweet-alert button.cancel:hover { + background-color: #c8c8c8; } + .sweet-alert button.cancel:active { + background-color: #b6b6b6; } + .sweet-alert button.cancel:focus { + box-shadow: rgba(197, 205, 211, 0.8) 0px 0px 2px, rgba(0, 0, 0, 0.0470588) 0px 0px 0px 1px inset !important; } + .sweet-alert button::-moz-focus-inner { + border: 0; } + .sweet-alert[data-has-cancel-button=false] button { + box-shadow: none !important; } + .sweet-alert .icon { + width: 80px; + height: 80px; + border: 4px solid gray; + border-radius: 50%; + margin: 20px auto; + padding: 0; + position: relative; + box-sizing: content-box; } + .sweet-alert .icon.error { + border-color: #F27474; } + .sweet-alert .icon.error .x-mark { + position: relative; + display: block; } + .sweet-alert .icon.error .line { + position: absolute; + height: 5px; + width: 47px; + background-color: #F27474; + display: block; + top: 37px; + border-radius: 2px; } + .sweet-alert .icon.error .line.left { + -webkit-transform: rotate(45deg); + transform: rotate(45deg); + left: 17px; } + .sweet-alert .icon.error .line.right { + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); + right: 16px; } + .sweet-alert .icon.warning { + border-color: #F8BB86; } + .sweet-alert .icon.warning .body { + position: absolute; + width: 5px; + height: 47px; + left: 50%; + top: 10px; + border-radius: 2px; + margin-left: -2px; + background-color: #F8BB86; } + .sweet-alert .icon.warning .dot { + position: absolute; + width: 7px; + height: 7px; + border-radius: 50%; + margin-left: -3px; + left: 50%; + bottom: 10px; + background-color: #F8BB86; } + .sweet-alert .icon.info { + border-color: #C9DAE1; } + .sweet-alert .icon.info::before { + content: ""; + position: absolute; + width: 5px; + height: 29px; + left: 50%; + bottom: 17px; + border-radius: 2px; + margin-left: -2px; + background-color: #C9DAE1; } + .sweet-alert .icon.info::after { + content: ""; + position: absolute; + width: 7px; + height: 7px; + border-radius: 50%; + margin-left: -3px; + top: 19px; + background-color: #C9DAE1; } + .sweet-alert .icon.success { + border-color: #A5DC86; } + .sweet-alert .icon.success::before, .sweet-alert .icon.success::after { + content: ''; + border-radius: 50%; + position: absolute; + width: 60px; + height: 120px; + background: white; + -webkit-transform: rotate(45deg); + transform: rotate(45deg); } + .sweet-alert .icon.success::before { + border-radius: 120px 0 0 120px; + top: -7px; + left: -33px; + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); + -webkit-transform-origin: 60px 60px; + transform-origin: 60px 60px; } + .sweet-alert .icon.success::after { + border-radius: 0 120px 120px 0; + top: -11px; + left: 30px; + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); + -webkit-transform-origin: 0px 60px; + transform-origin: 0px 60px; } + .sweet-alert .icon.success .placeholder { + width: 80px; + height: 80px; + border: 4px solid rgba(165, 220, 134, 0.2); + border-radius: 50%; + box-sizing: content-box; + position: absolute; + left: -4px; + top: -4px; + z-index: 2; } + .sweet-alert .icon.success .fix { + width: 5px; + height: 90px; + background-color: white; + position: absolute; + left: 28px; + top: 8px; + z-index: 1; + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); } + .sweet-alert .icon.success .line { + height: 5px; + background-color: #A5DC86; + display: block; + border-radius: 2px; + position: absolute; + z-index: 2; } + .sweet-alert .icon.success .line.tip { + width: 25px; + left: 14px; + top: 46px; + -webkit-transform: rotate(45deg); + transform: rotate(45deg); } + .sweet-alert .icon.success .line.long { + width: 47px; + right: 8px; + top: 38px; + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); } + .sweet-alert .icon.custom { + background-size: contain; + border-radius: 0; + border: none; + background-position: center center; + background-repeat: no-repeat; } + +/* + * Animations + */ +@-webkit-keyframes showSweetAlert { + 0% { + transform: scale(0.7); + -webkit-transform: scale(0.7); } + 45% { + transform: scale(1.05); + -webkit-transform: scale(1.05); } + 80% { + transform: scale(0.95); + -webkit-tranform: scale(0.95); } + 100% { + transform: scale(1); + -webkit-transform: scale(1); } } +@-moz-keyframes showSweetAlert { + 0% { + transform: scale(0.7); + -webkit-transform: scale(0.7); } + 45% { + transform: scale(1.05); + -webkit-transform: scale(1.05); } + 80% { + transform: scale(0.95); + -webkit-tranform: scale(0.95); } + 100% { + transform: scale(1); + -webkit-transform: scale(1); } } +@keyframes showSweetAlert { + 0% { + transform: scale(0.7); + -webkit-transform: scale(0.7); } + 45% { + transform: scale(1.05); + -webkit-transform: scale(1.05); } + 80% { + transform: scale(0.95); + -webkit-tranform: scale(0.95); } + 100% { + transform: scale(1); + -webkit-transform: scale(1); } } +@-webkit-keyframes hideSweetAlert { + 0% { + transform: scale(1); + -webkit-transform: scale(1); } + 100% { + transform: scale(0.5); + -webkit-transform: scale(0.5); } } +@-moz-keyframes hideSweetAlert { + 0% { + transform: scale(1); + -webkit-transform: scale(1); } + 100% { + transform: scale(0.5); + -webkit-transform: scale(0.5); } } +@keyframes hideSweetAlert { + 0% { + transform: scale(1); + -webkit-transform: scale(1); } + 100% { + transform: scale(0.5); + -webkit-transform: scale(0.5); } } +.showSweetAlert { + -webkit-animation: showSweetAlert 0.3s; + -moz-animation: showSweetAlert 0.3s; + animation: showSweetAlert 0.3s; } + +.hideSweetAlert { + -webkit-animation: hideSweetAlert 0.2s; + -moz-animation: hideSweetAlert 0.2s; + animation: hideSweetAlert 0.2s; } + +@-webkit-keyframes animateSuccessTip { + 0% { + width: 0; + left: 1px; + top: 19px; } + 54% { + width: 0; + left: 1px; + top: 19px; } + 70% { + width: 50px; + left: -8px; + top: 37px; } + 84% { + width: 17px; + left: 21px; + top: 48px; } + 100% { + width: 25px; + left: 14px; + top: 45px; } } +@-moz-keyframes animateSuccessTip { + 0% { + width: 0; + left: 1px; + top: 19px; } + 54% { + width: 0; + left: 1px; + top: 19px; } + 70% { + width: 50px; + left: -8px; + top: 37px; } + 84% { + width: 17px; + left: 21px; + top: 48px; } + 100% { + width: 25px; + left: 14px; + top: 45px; } } +@keyframes animateSuccessTip { + 0% { + width: 0; + left: 1px; + top: 19px; } + 54% { + width: 0; + left: 1px; + top: 19px; } + 70% { + width: 50px; + left: -8px; + top: 37px; } + 84% { + width: 17px; + left: 21px; + top: 48px; } + 100% { + width: 25px; + left: 14px; + top: 45px; } } +@-webkit-keyframes animateSuccessLong { + 0% { + width: 0; + right: 46px; + top: 54px; } + 65% { + width: 0; + right: 46px; + top: 54px; } + 84% { + width: 55px; + right: 0px; + top: 35px; } + 100% { + width: 47px; + right: 8px; + top: 38px; } } +@-moz-keyframes animateSuccessLong { + 0% { + width: 0; + right: 46px; + top: 54px; } + 65% { + width: 0; + right: 46px; + top: 54px; } + 84% { + width: 55px; + right: 0px; + top: 35px; } + 100% { + width: 47px; + right: 8px; + top: 38px; } } +@keyframes animateSuccessLong { + 0% { + width: 0; + right: 46px; + top: 54px; } + 65% { + width: 0; + right: 46px; + top: 54px; } + 84% { + width: 55px; + right: 0px; + top: 35px; } + 100% { + width: 47px; + right: 8px; + top: 38px; } } +@-webkit-keyframes rotatePlaceholder { + 0% { + transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); } + 5% { + transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); } + 12% { + transform: rotate(-405deg); + -webkit-transform: rotate(-405deg); } + 100% { + transform: rotate(-405deg); + -webkit-transform: rotate(-405deg); } } +@-moz-keyframes rotatePlaceholder { + 0% { + transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); } + 5% { + transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); } + 12% { + transform: rotate(-405deg); + -webkit-transform: rotate(-405deg); } + 100% { + transform: rotate(-405deg); + -webkit-transform: rotate(-405deg); } } +@keyframes rotatePlaceholder { + 0% { + transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); } + 5% { + transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); } + 12% { + transform: rotate(-405deg); + -webkit-transform: rotate(-405deg); } + 100% { + transform: rotate(-405deg); + -webkit-transform: rotate(-405deg); } } +.animateSuccessTip { + -webkit-animation: animateSuccessTip 0.75s; + -moz-animation: animateSuccessTip 0.75s; + animation: animateSuccessTip 0.75s; } + +.animateSuccessLong { + -webkit-animation: animateSuccessLong 0.75s; + -moz-animation: animateSuccessLong 0.75s; + animation: animateSuccessLong 0.75s; } + +.icon.success.animate::after { + -webkit-animation: rotatePlaceholder 4.25s ease-in; + -moz-animation: rotatePlaceholder 4.25s ease-in; + animation: rotatePlaceholder 4.25s ease-in; } + +@-webkit-keyframes animateErrorIcon { + 0% { + transform: rotateX(100deg); + -webkit-transform: rotateX(100deg); + opacity: 0; } + 100% { + transform: rotateX(0deg); + -webkit-transform: rotateX(0deg); + opacity: 1; } } +@-moz-keyframes animateErrorIcon { + 0% { + transform: rotateX(100deg); + -webkit-transform: rotateX(100deg); + opacity: 0; } + 100% { + transform: rotateX(0deg); + -webkit-transform: rotateX(0deg); + opacity: 1; } } +@keyframes animateErrorIcon { + 0% { + transform: rotateX(100deg); + -webkit-transform: rotateX(100deg); + opacity: 0; } + 100% { + transform: rotateX(0deg); + -webkit-transform: rotateX(0deg); + opacity: 1; } } +.animateErrorIcon { + -webkit-animation: animateErrorIcon 0.5s; + -moz-animation: animateErrorIcon 0.5s; + animation: animateErrorIcon 0.5s; } + +@-webkit-keyframes animateXMark { + 0% { + transform: scale(0.4); + -webkit-transform: scale(0.4); + margin-top: 26px; + opacity: 0; } + 50% { + transform: scale(0.4); + -webkit-transform: scale(0.4); + margin-top: 26px; + opacity: 0; } + 80% { + transform: scale(1.15); + -webkit-transform: scale(1.15); + margin-top: -6px; } + 100% { + transform: scale(1); + -webkit-transform: scale(1); + margin-top: 0; + opacity: 1; } } +@-moz-keyframes animateXMark { + 0% { + transform: scale(0.4); + -webkit-transform: scale(0.4); + margin-top: 26px; + opacity: 0; } + 50% { + transform: scale(0.4); + -webkit-transform: scale(0.4); + margin-top: 26px; + opacity: 0; } + 80% { + transform: scale(1.15); + -webkit-transform: scale(1.15); + margin-top: -6px; } + 100% { + transform: scale(1); + -webkit-transform: scale(1); + margin-top: 0; + opacity: 1; } } +@keyframes animateXMark { + 0% { + transform: scale(0.4); + -webkit-transform: scale(0.4); + margin-top: 26px; + opacity: 0; } + 50% { + transform: scale(0.4); + -webkit-transform: scale(0.4); + margin-top: 26px; + opacity: 0; } + 80% { + transform: scale(1.15); + -webkit-transform: scale(1.15); + margin-top: -6px; } + 100% { + transform: scale(1); + -webkit-transform: scale(1); + margin-top: 0; + opacity: 1; } } +.animateXMark { + -webkit-animation: animateXMark 0.5s; + -moz-animation: animateXMark 0.5s; + animation: animateXMark 0.5s; } + +@-webkit-keyframes pulseWarning { + 0% { + border-color: #F8D486; } + 100% { + border-color: #F8BB86; } } +@-moz-keyframes pulseWarning { + 0% { + border-color: #F8D486; } + 100% { + border-color: #F8BB86; } } +@keyframes pulseWarning { + 0% { + border-color: #F8D486; } + 100% { + border-color: #F8BB86; } } +.pulseWarning { + -webkit-animation: pulseWarning 0.75s infinite alternate; + -moz-animation: pulseWarning 0.75s infinite alternate; + animation: pulseWarning 0.75s infinite alternate; } + +@-webkit-keyframes pulseWarningIns { + 0% { + background-color: #F8D486; } + 100% { + background-color: #F8BB86; } } +@-moz-keyframes pulseWarningIns { + 0% { + background-color: #F8D486; } + 100% { + background-color: #F8BB86; } } +@keyframes pulseWarningIns { + 0% { + background-color: #F8D486; } + 100% { + background-color: #F8BB86; } } +.pulseWarningIns { + -webkit-animation: pulseWarningIns 0.75s infinite alternate; + -moz-animation: pulseWarningIns 0.75s infinite alternate; + animation: pulseWarningIns 0.75s infinite alternate; } diff --git a/media/js/vendor/sweetalert/sweet-alert.js b/media/js/vendor/sweetalert/sweet-alert.js new file mode 100755 index 0000000..84ceb40 --- /dev/null +++ b/media/js/vendor/sweetalert/sweet-alert.js @@ -0,0 +1,771 @@ +// SweetAlert +// 2014 (c) - Tristan Edwards +// github.com/t4t5/sweetalert +;(function(window, document) { + + var modalClass = '.sweet-alert', + overlayClass = '.sweet-overlay', + alertTypes = ['error', 'warning', 'info', 'success'], + defaultParams = { + title: '', + text: '', + type: null, + allowOutsideClick: false, + showCancelButton: false, + closeOnConfirm: true, + closeOnCancel: true, + confirmButtonText: 'OK', + confirmButtonColor: '#AEDEF4', + cancelButtonText: 'Cancel', + imageUrl: null, + imageSize: null, + timer: null + }; + + + /* + * Manipulate DOM + */ + + var getModal = function() { + return document.querySelector(modalClass); + }, + getOverlay = function() { + return document.querySelector(overlayClass); + }, + hasClass = function(elem, className) { + return new RegExp(' ' + className + ' ').test(' ' + elem.className + ' '); + }, + addClass = function(elem, className) { + if (!hasClass(elem, className)) { + elem.className += ' ' + className; + } + }, + removeClass = function(elem, className) { + var newClass = ' ' + elem.className.replace(/[\t\r\n]/g, ' ') + ' '; + if (hasClass(elem, className)) { + while (newClass.indexOf(' ' + className + ' ') >= 0) { + newClass = newClass.replace(' ' + className + ' ', ' '); + } + elem.className = newClass.replace(/^\s+|\s+$/g, ''); + } + }, + escapeHtml = function(str) { + var div = document.createElement('div'); + div.appendChild(document.createTextNode(str)); + return div.innerHTML; + }, + _show = function(elem) { + elem.style.opacity = ''; + elem.style.display = 'block'; + }, + show = function(elems) { + if (elems && !elems.length) { + return _show(elems); + } + for (var i = 0; i < elems.length; ++i) { + _show(elems[i]); + } + }, + _hide = function(elem) { + elem.style.opacity = ''; + elem.style.display = 'none'; + }, + hide = function(elems) { + if (elems && !elems.length) { + return _hide(elems); + } + for (var i = 0; i < elems.length; ++i) { + _hide(elems[i]); + } + }, + isDescendant = function(parent, child) { + var node = child.parentNode; + while (node !== null) { + if (node === parent) { + return true; + } + node = node.parentNode; + } + return false; + }, + getTopMargin = function(elem) { + elem.style.left = '-9999px'; + elem.style.display = 'block'; + + var height = elem.clientHeight, + padding; + if (typeof getComputedStyle !== "undefined") { /* IE 8 */ + padding = parseInt(getComputedStyle(elem).getPropertyValue('padding'), 10); + } else{ + padding = parseInt(elem.currentStyle.padding); + } + + elem.style.left = ''; + elem.style.display = 'none'; + return ('-' + parseInt(height / 2 + padding) + 'px'); + }, + fadeIn = function(elem, interval) { + if (+elem.style.opacity < 1) { + interval = interval || 16; + elem.style.opacity = 0; + elem.style.display = 'block'; + var last = +new Date(); + var tick = function() { + elem.style.opacity = +elem.style.opacity + (new Date() - last) / 100; + last = +new Date(); + + if (+elem.style.opacity < 1) { + setTimeout(tick, interval); + } + }; + tick(); + } + elem.style.display = 'block'; //fallback IE8 + }, + fadeOut = function(elem, interval) { + interval = interval || 16; + elem.style.opacity = 1; + var last = +new Date(); + var tick = function() { + elem.style.opacity = +elem.style.opacity - (new Date() - last) / 100; + last = +new Date(); + + if (+elem.style.opacity > 0) { + setTimeout(tick, interval); + } else { + elem.style.display = 'none'; + } + }; + tick(); + }, + fireClick = function(node) { + // Taken from http://www.nonobtrusive.com/2011/11/29/programatically-fire-crossbrowser-click-event-with-javascript/ + // Then fixed for today's Chrome browser. + if (MouseEvent) { + // Up-to-date approach + var mevt = new MouseEvent('click', { + view: window, + bubbles: false, + cancelable: true + }); + node.dispatchEvent(mevt); + } else if ( document.createEvent ) { + // Fallback + var evt = document.createEvent('MouseEvents'); + evt.initEvent('click', false, false); + node.dispatchEvent(evt); + } else if( document.createEventObject ) { + node.fireEvent('onclick') ; + } else if (typeof node.onclick === 'function' ) { + node.onclick(); + } + }, + stopEventPropagation = function(e) { + // In particular, make sure the space bar doesn't scroll the main window. + if (typeof e.stopPropagation === 'function') { + e.stopPropagation(); + e.preventDefault(); + } else if (window.event && window.event.hasOwnProperty('cancelBubble')) { + window.event.cancelBubble = true; + } + }; + + // Remember state in cases where opening and handling a modal will fiddle with it. + var previousActiveElement, + previousDocumentClick, + previousWindowKeyDown, + lastFocusedButton; + + /* + * Add modal + overlay to DOM + */ + + window.sweetAlertInitialize = function() { + var sweetHTML = '

    Title

    Text

    ', + sweetWrap = document.createElement('div'); + + sweetWrap.innerHTML = sweetHTML; + + // For readability: check sweet-alert.html + document.body.appendChild(sweetWrap); + }; + + /* + * Global sweetAlert function + */ + + window.sweetAlert = window.swal = function() { + // Copy arguments to the local args variable + var args = arguments; + if (getModal() !== null) { + // If getModal returns values then continue + modalDependant.apply(this, args); + } else { + // If getModal returns null i.e. no matches, then set up a interval event to check the return value until it is not null + var modalCheckInterval = setInterval(function() { + if (getModal() !== null) { + clearInterval(modalCheckInterval); + modalDependant.apply(this, args); + } + }, 100); + } + }; + + function modalDependant() { + + if (arguments[0] === undefined) { + window.console.error('sweetAlert expects at least 1 attribute!'); + return false; + } + + var params = extend({}, defaultParams); + + switch (typeof arguments[0]) { + + case 'string': + params.title = arguments[0]; + params.text = arguments[1] || ''; + params.type = arguments[2] || ''; + + break; + + case 'object': + if (arguments[0].title === undefined) { + window.console.error('Missing "title" argument!'); + return false; + } + + params.title = arguments[0].title; + params.text = arguments[0].text || defaultParams.text; + params.type = arguments[0].type || defaultParams.type; + params.customClass = arguments[0].customClass || params.customClass; + params.allowOutsideClick = arguments[0].allowOutsideClick || defaultParams.allowOutsideClick; + params.showCancelButton = arguments[0].showCancelButton !== undefined ? arguments[0].showCancelButton : defaultParams.showCancelButton; + params.closeOnConfirm = arguments[0].closeOnConfirm !== undefined ? arguments[0].closeOnConfirm : defaultParams.closeOnConfirm; + params.closeOnCancel = arguments[0].closeOnCancel !== undefined ? arguments[0].closeOnCancel : defaultParams.closeOnCancel; + params.timer = arguments[0].timer || defaultParams.timer; + + // Show "Confirm" instead of "OK" if cancel button is visible + params.confirmButtonText = (defaultParams.showCancelButton) ? 'Confirm' : defaultParams.confirmButtonText; + params.confirmButtonText = arguments[0].confirmButtonText || defaultParams.confirmButtonText; + params.confirmButtonColor = arguments[0].confirmButtonColor || defaultParams.confirmButtonColor; + params.cancelButtonText = arguments[0].cancelButtonText || defaultParams.cancelButtonText; + params.imageUrl = arguments[0].imageUrl || defaultParams.imageUrl; + params.imageSize = arguments[0].imageSize || defaultParams.imageSize; + params.doneFunction = arguments[1] || null; + + break; + + default: + window.console.error('Unexpected type of argument! Expected "string" or "object", got ' + typeof arguments[0]); + return false; + + } + + setParameters(params); + fixVerticalPosition(); + openModal(); + + + // Modal interactions + var modal = getModal(); + + // Mouse interactions + var onButtonEvent = function(event) { + var e = event || window.event; + var target = e.target || e.srcElement, + targetedConfirm = (target.className === 'confirm'), + modalIsVisible = hasClass(modal, 'visible'), + doneFunctionExists = (params.doneFunction && modal.getAttribute('data-has-done-function') === 'true'); + + switch (e.type) { + case ("mouseover"): + if (targetedConfirm) { + target.style.backgroundColor = colorLuminance(params.confirmButtonColor, -0.04); + } + break; + case ("mouseout"): + if (targetedConfirm) { + target.style.backgroundColor = params.confirmButtonColor; + } + break; + case ("mousedown"): + if (targetedConfirm) { + target.style.backgroundColor = colorLuminance(params.confirmButtonColor, -0.14); + } + break; + case ("mouseup"): + if (targetedConfirm) { + target.style.backgroundColor = colorLuminance(params.confirmButtonColor, -0.04); + } + break; + case ("focus"): + var $confirmButton = modal.querySelector('button.confirm'), + $cancelButton = modal.querySelector('button.cancel'); + + if (targetedConfirm) { + $cancelButton.style.boxShadow = 'none'; + } else { + $confirmButton.style.boxShadow = 'none'; + } + break; + case ("click"): + if (targetedConfirm && doneFunctionExists && modalIsVisible) { // Clicked "confirm" + + params.doneFunction(true); + + if (params.closeOnConfirm) { + closeModal(); + } + } else if (doneFunctionExists && modalIsVisible) { // Clicked "cancel" + + // Check if callback function expects a parameter (to track cancel actions) + var functionAsStr = String(params.doneFunction).replace(/\s/g, ''); + var functionHandlesCancel = functionAsStr.substring(0, 9) === "function(" && functionAsStr.substring(9, 10) !== ")"; + + if (functionHandlesCancel) { + params.doneFunction(false); + } + + if (params.closeOnCancel) { + closeModal(); + } + } else { + closeModal(); + } + + break; + } + }; + + var $buttons = modal.querySelectorAll('button'); + for (var i = 0; i < $buttons.length; i++) { + $buttons[i].onclick = onButtonEvent; + $buttons[i].onmouseover = onButtonEvent; + $buttons[i].onmouseout = onButtonEvent; + $buttons[i].onmousedown = onButtonEvent; + //$buttons[i].onmouseup = onButtonEvent; + $buttons[i].onfocus = onButtonEvent; + } + + // Remember the current document.onclick event. + previousDocumentClick = document.onclick; + document.onclick = function(event) { + var e = event || window.event; + var target = e.target || e.srcElement; + + var clickedOnModal = (modal === target), + clickedOnModalChild = isDescendant(modal, target), + modalIsVisible = hasClass(modal, 'visible'), + outsideClickIsAllowed = modal.getAttribute('data-allow-ouside-click') === 'true'; + + if (!clickedOnModal && !clickedOnModalChild && modalIsVisible && outsideClickIsAllowed) { + closeModal(); + } + }; + + + // Keyboard interactions + var $okButton = modal.querySelector('button.confirm'), + $cancelButton = modal.querySelector('button.cancel'), + $modalButtons = modal.querySelectorAll('button:not([type=hidden])'); + + + function handleKeyDown(event) { + var e = event || window.event; + var keyCode = e.keyCode || e.which; + + if ([9,13,32,27].indexOf(keyCode) === -1) { + // Don't do work on keys we don't care about. + return; + } + + var $targetElement = e.target || e.srcElement; + + var btnIndex = -1; // Find the button - note, this is a nodelist, not an array. + for (var i = 0; i < $modalButtons.length; i++) { + if ($targetElement === $modalButtons[i]) { + btnIndex = i; + break; + } + } + + if (keyCode === 9) { + // TAB + if (btnIndex === -1) { + // No button focused. Jump to the confirm button. + $targetElement = $okButton; + } else { + // Cycle to the next button + if (btnIndex === $modalButtons.length - 1) { + $targetElement = $modalButtons[0]; + } else { + $targetElement = $modalButtons[btnIndex + 1]; + } + } + + stopEventPropagation(e); + $targetElement.focus(); + setFocusStyle($targetElement, params.confirmButtonColor); // TODO + + } else { + if (keyCode === 13 || keyCode === 32) { + if (btnIndex === -1) { + // ENTER/SPACE clicked outside of a button. + $targetElement = $okButton; + } else { + // Do nothing - let the browser handle it. + $targetElement = undefined; + } + } else if (keyCode === 27 && !($cancelButton.hidden || $cancelButton.style.display === 'none')) { + // ESC to cancel only if there's a cancel button displayed (like the alert() window). + $targetElement = $cancelButton; + } else { + // Fallback - let the browser handle it. + $targetElement = undefined; + } + + if ($targetElement !== undefined) { + fireClick($targetElement, e); + } + } + } + + previousWindowKeyDown = window.onkeydown; + window.onkeydown = handleKeyDown; + + function handleOnBlur(event) { + var e = event || window.event; + var $targetElement = e.target || e.srcElement, + $focusElement = e.relatedTarget, + modalIsVisible = hasClass(modal, 'visible'); + + if (modalIsVisible) { + var btnIndex = -1; // Find the button - note, this is a nodelist, not an array. + + if ($focusElement !== null) { + // If we picked something in the DOM to focus to, let's see if it was a button. + for (var i = 0; i < $modalButtons.length; i++) { + if ($focusElement === $modalButtons[i]) { + btnIndex = i; + break; + } + } + + if (btnIndex === -1) { + // Something in the dom, but not a visible button. Focus back on the button. + $targetElement.focus(); + } + } else { + // Exiting the DOM (e.g. clicked in the URL bar); + lastFocusedButton = $targetElement; + } + } + } + + $okButton.onblur = handleOnBlur; + $cancelButton.onblur = handleOnBlur; + + window.onfocus = function() { + // When the user has focused away and focused back from the whole window. + window.setTimeout(function() { + // Put in a timeout to jump out of the event sequence. Calling focus() in the event + // sequence confuses things. + if (lastFocusedButton !== undefined) { + lastFocusedButton.focus(); + lastFocusedButton = undefined; + } + }, 0); + }; + } + + /** + * Set default params for each popup + * @param {Object} userParams + */ + window.swal.setDefaults = function(userParams) { + if (!userParams) { + throw new Error('userParams is required'); + } + if (typeof userParams !== 'object') { + throw new Error('userParams has to be a object'); + } + + extend(defaultParams, userParams); + }; + + /* + * Set type, text and actions on modal + */ + + function setParameters(params) { + var modal = getModal(); + + var $title = modal.querySelector('h2'), + $text = modal.querySelector('p'), + $cancelBtn = modal.querySelector('button.cancel'), + $confirmBtn = modal.querySelector('button.confirm'); + + // Title + $title.innerHTML = escapeHtml(params.title).split("\n").join("
    "); + + // Text + $text.innerHTML = escapeHtml(params.text || '').split("\n").join("
    "); + if (params.text) { + show($text); + } + + //Custom Class + if (params.customClass) { + addClass(modal, params.customClass); + } + + // Icon + hide(modal.querySelectorAll('.icon')); + if (params.type) { + var validType = false; + for (var i = 0; i < alertTypes.length; i++) { + if (params.type === alertTypes[i]) { + validType = true; + break; + } + } + if (!validType) { + window.console.error('Unknown alert type: ' + params.type); + return false; + } + var $icon = modal.querySelector('.icon.' + params.type); + show($icon); + + // Animate icon + switch (params.type) { + case "success": + addClass($icon, 'animate'); + addClass($icon.querySelector('.tip'), 'animateSuccessTip'); + addClass($icon.querySelector('.long'), 'animateSuccessLong'); + break; + case "error": + addClass($icon, 'animateErrorIcon'); + addClass($icon.querySelector('.x-mark'), 'animateXMark'); + break; + case "warning": + addClass($icon, 'pulseWarning'); + addClass($icon.querySelector('.body'), 'pulseWarningIns'); + addClass($icon.querySelector('.dot'), 'pulseWarningIns'); + break; + } + + } + + // Custom image + if (params.imageUrl) { + var $customIcon = modal.querySelector('.icon.custom'); + + $customIcon.style.backgroundImage = 'url(' + params.imageUrl + ')'; + show($customIcon); + + var _imgWidth = 80, + _imgHeight = 80; + + if (params.imageSize) { + var imgWidth = params.imageSize.split('x')[0]; + var imgHeight = params.imageSize.split('x')[1]; + + if (!imgWidth || !imgHeight) { + window.console.error("Parameter imageSize expects value with format WIDTHxHEIGHT, got " + params.imageSize); + } else { + _imgWidth = imgWidth; + _imgHeight = imgHeight; + + $customIcon.css({ + 'width': imgWidth + 'px', + 'height': imgHeight + 'px' + }); + } + } + $customIcon.setAttribute('style', $customIcon.getAttribute('style') + 'width:' + _imgWidth + 'px; height:' + _imgHeight + 'px'); + } + + // Cancel button + modal.setAttribute('data-has-cancel-button', params.showCancelButton); + if (params.showCancelButton) { + $cancelBtn.style.display = 'inline-block'; + } else { + hide($cancelBtn); + } + + // Edit text on cancel and confirm buttons + if (params.cancelButtonText) { + $cancelBtn.innerHTML = escapeHtml(params.cancelButtonText); + } + if (params.confirmButtonText) { + $confirmBtn.innerHTML = escapeHtml(params.confirmButtonText); + } + + // Set confirm button to selected background color + $confirmBtn.style.backgroundColor = params.confirmButtonColor; + + // Set box-shadow to default focused button + setFocusStyle($confirmBtn, params.confirmButtonColor); + + // Allow outside click? + modal.setAttribute('data-allow-ouside-click', params.allowOutsideClick); + + // Done-function + var hasDoneFunction = (params.doneFunction) ? true : false; + modal.setAttribute('data-has-done-function', hasDoneFunction); + + // Close timer + modal.setAttribute('data-timer', params.timer); + } + + + /* + * Set hover, active and focus-states for buttons (source: http://www.sitepoint.com/javascript-generate-lighter-darker-color) + */ + + function colorLuminance(hex, lum) { + // Validate hex string + hex = String(hex).replace(/[^0-9a-f]/gi, ''); + if (hex.length < 6) { + hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; + } + lum = lum || 0; + + // Convert to decimal and change luminosity + var rgb = "#", c, i; + for (i = 0; i < 3; i++) { + c = parseInt(hex.substr(i*2,2), 16); + c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16); + rgb += ("00"+c).substr(c.length); + } + + return rgb; + } + + function extend(a, b){ + for (var key in b) { + if (b.hasOwnProperty(key)) { + a[key] = b[key]; + } + } + + return a; + } + + function hexToRgb(hex) { + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? parseInt(result[1], 16) + ', ' + parseInt(result[2], 16) + ', ' + parseInt(result[3], 16) : null; + } + + // Add box-shadow style to button (depending on its chosen bg-color) + function setFocusStyle($button, bgColor) { + var rgbColor = hexToRgb(bgColor); + $button.style.boxShadow = '0 0 2px rgba(' + rgbColor +', 0.8), inset 0 0 0 1px rgba(0, 0, 0, 0.05)'; + } + + + + /* + * Animations + */ + + function openModal() { + var modal = getModal(); + fadeIn(getOverlay(), 10); + show(modal); + addClass(modal, 'showSweetAlert'); + removeClass(modal, 'hideSweetAlert'); + + previousActiveElement = document.activeElement; + var $okButton = modal.querySelector('button.confirm'); + $okButton.focus(); + + setTimeout(function() { + addClass(modal, 'visible'); + }, 500); + + var timer = modal.getAttribute('data-timer'); + + if (timer !== "null" && timer !== "") { + modal.timeout = setTimeout(function() { + closeModal(); + }, timer); + } + } + + function closeModal() { + var modal = getModal(); + fadeOut(getOverlay(), 5); + fadeOut(modal, 5); + removeClass(modal, 'showSweetAlert'); + addClass(modal, 'hideSweetAlert'); + removeClass(modal, 'visible'); + + + // Reset icon animations + + var $successIcon = modal.querySelector('.icon.success'); + removeClass($successIcon, 'animate'); + removeClass($successIcon.querySelector('.tip'), 'animateSuccessTip'); + removeClass($successIcon.querySelector('.long'), 'animateSuccessLong'); + + var $errorIcon = modal.querySelector('.icon.error'); + removeClass($errorIcon, 'animateErrorIcon'); + removeClass($errorIcon.querySelector('.x-mark'), 'animateXMark'); + + var $warningIcon = modal.querySelector('.icon.warning'); + removeClass($warningIcon, 'pulseWarning'); + removeClass($warningIcon.querySelector('.body'), 'pulseWarningIns'); + removeClass($warningIcon.querySelector('.dot'), 'pulseWarningIns'); + + + // Reset the page to its previous state + window.onkeydown = previousWindowKeyDown; + document.onclick = previousDocumentClick; + if (previousActiveElement) { + previousActiveElement.focus(); + } + lastFocusedButton = undefined; + clearTimeout(modal.timeout); + } + + + /* + * Set "margin-top"-property on modal based on its computed height + */ + + function fixVerticalPosition() { + var modal = getModal(); + + modal.style.marginTop = getTopMargin(getModal()); + } + + + + /* + * If library is injected after page has loaded + */ + + (function () { + if (document.readyState === "complete" || document.readyState === "interactive" && document.body) { + window.sweetAlertInitialize(); + } else { + if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', function factorial() { + document.removeEventListener('DOMContentLoaded', arguments.callee, false); + window.sweetAlertInitialize(); + }, false); + } else if (document.attachEvent) { + document.attachEvent('onreadystatechange', function() { + if (document.readyState === 'complete') { + document.detachEvent('onreadystatechange', arguments.callee); + window.sweetAlertInitialize(); + } + }); + } + } + })(); + +})(window, document); diff --git a/media/js/vendor/underscore/underscore.js b/media/js/vendor/underscore/underscore.js new file mode 100644 index 0000000..d5b3375 --- /dev/null +++ b/media/js/vendor/underscore/underscore.js @@ -0,0 +1,1416 @@ +// Underscore.js 1.7.0 +// http://underscorejs.org +// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `exports` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var + push = ArrayProto.push, + slice = ArrayProto.slice, + concat = ArrayProto.concat, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.7.0'; + + // Internal function that returns an efficient (for current engines) version + // of the passed-in callback, to be repeatedly applied in other Underscore + // functions. + var createCallback = function(func, context, argCount) { + if (context === void 0) return func; + switch (argCount == null ? 3 : argCount) { + case 1: return function(value) { + return func.call(context, value); + }; + case 2: return function(value, other) { + return func.call(context, value, other); + }; + case 3: return function(value, index, collection) { + return func.call(context, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(context, accumulator, value, index, collection); + }; + } + return function() { + return func.apply(context, arguments); + }; + }; + + // A mostly-internal function to generate callbacks that can be applied + // to each element in a collection, returning the desired result — either + // identity, an arbitrary callback, a property matcher, or a property accessor. + _.iteratee = function(value, context, argCount) { + if (value == null) return _.identity; + if (_.isFunction(value)) return createCallback(value, context, argCount); + if (_.isObject(value)) return _.matches(value); + return _.property(value); + }; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles raw objects in addition to array-likes. Treats all + // sparse array-likes as if they were dense. + _.each = _.forEach = function(obj, iteratee, context) { + if (obj == null) return obj; + iteratee = createCallback(iteratee, context); + var i, length = obj.length; + if (length === +length) { + for (i = 0; i < length; i++) { + iteratee(obj[i], i, obj); + } + } else { + var keys = _.keys(obj); + for (i = 0, length = keys.length; i < length; i++) { + iteratee(obj[keys[i]], keys[i], obj); + } + } + return obj; + }; + + // Return the results of applying the iteratee to each element. + _.map = _.collect = function(obj, iteratee, context) { + if (obj == null) return []; + iteratee = _.iteratee(iteratee, context); + var keys = obj.length !== +obj.length && _.keys(obj), + length = (keys || obj).length, + results = Array(length), + currentKey; + for (var index = 0; index < length; index++) { + currentKey = keys ? keys[index] : index; + results[index] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + }; + + var reduceError = 'Reduce of empty array with no initial value'; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. + _.reduce = _.foldl = _.inject = function(obj, iteratee, memo, context) { + if (obj == null) obj = []; + iteratee = createCallback(iteratee, context, 4); + var keys = obj.length !== +obj.length && _.keys(obj), + length = (keys || obj).length, + index = 0, currentKey; + if (arguments.length < 3) { + if (!length) throw new TypeError(reduceError); + memo = obj[keys ? keys[index++] : index++]; + } + for (; index < length; index++) { + currentKey = keys ? keys[index] : index; + memo = iteratee(memo, obj[currentKey], currentKey, obj); + } + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + _.reduceRight = _.foldr = function(obj, iteratee, memo, context) { + if (obj == null) obj = []; + iteratee = createCallback(iteratee, context, 4); + var keys = obj.length !== + obj.length && _.keys(obj), + index = (keys || obj).length, + currentKey; + if (arguments.length < 3) { + if (!index) throw new TypeError(reduceError); + memo = obj[keys ? keys[--index] : --index]; + } + while (index--) { + currentKey = keys ? keys[index] : index; + memo = iteratee(memo, obj[currentKey], currentKey, obj); + } + return memo; + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, predicate, context) { + var result; + predicate = _.iteratee(predicate, context); + _.some(obj, function(value, index, list) { + if (predicate(value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Aliased as `select`. + _.filter = _.select = function(obj, predicate, context) { + var results = []; + if (obj == null) return results; + predicate = _.iteratee(predicate, context); + _.each(obj, function(value, index, list) { + if (predicate(value, index, list)) results.push(value); + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, predicate, context) { + return _.filter(obj, _.negate(_.iteratee(predicate)), context); + }; + + // Determine whether all of the elements match a truth test. + // Aliased as `all`. + _.every = _.all = function(obj, predicate, context) { + if (obj == null) return true; + predicate = _.iteratee(predicate, context); + var keys = obj.length !== +obj.length && _.keys(obj), + length = (keys || obj).length, + index, currentKey; + for (index = 0; index < length; index++) { + currentKey = keys ? keys[index] : index; + if (!predicate(obj[currentKey], currentKey, obj)) return false; + } + return true; + }; + + // Determine if at least one element in the object matches a truth test. + // Aliased as `any`. + _.some = _.any = function(obj, predicate, context) { + if (obj == null) return false; + predicate = _.iteratee(predicate, context); + var keys = obj.length !== +obj.length && _.keys(obj), + length = (keys || obj).length, + index, currentKey; + for (index = 0; index < length; index++) { + currentKey = keys ? keys[index] : index; + if (predicate(obj[currentKey], currentKey, obj)) return true; + } + return false; + }; + + // Determine if the array or object contains a given value (using `===`). + // Aliased as `include`. + _.contains = _.include = function(obj, target) { + if (obj == null) return false; + if (obj.length !== +obj.length) obj = _.values(obj); + return _.indexOf(obj, target) >= 0; + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); + return _.map(obj, function(value) { + return (isFunc ? method : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, _.property(key)); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs) { + return _.filter(obj, _.matches(attrs)); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.find(obj, _.matches(attrs)); + }; + + // Return the maximum element (or element-based computation). + _.max = function(obj, iteratee, context) { + var result = -Infinity, lastComputed = -Infinity, + value, computed; + if (iteratee == null && obj != null) { + obj = obj.length === +obj.length ? obj : _.values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value > result) { + result = value; + } + } + } else { + iteratee = _.iteratee(iteratee, context); + _.each(obj, function(value, index, list) { + computed = iteratee(value, index, list); + if (computed > lastComputed || computed === -Infinity && result === -Infinity) { + result = value; + lastComputed = computed; + } + }); + } + return result; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iteratee, context) { + var result = Infinity, lastComputed = Infinity, + value, computed; + if (iteratee == null && obj != null) { + obj = obj.length === +obj.length ? obj : _.values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value < result) { + result = value; + } + } + } else { + iteratee = _.iteratee(iteratee, context); + _.each(obj, function(value, index, list) { + computed = iteratee(value, index, list); + if (computed < lastComputed || computed === Infinity && result === Infinity) { + result = value; + lastComputed = computed; + } + }); + } + return result; + }; + + // Shuffle a collection, using the modern version of the + // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + _.shuffle = function(obj) { + var set = obj && obj.length === +obj.length ? obj : _.values(obj); + var length = set.length; + var shuffled = Array(length); + for (var index = 0, rand; index < length; index++) { + rand = _.random(0, index); + if (rand !== index) shuffled[index] = shuffled[rand]; + shuffled[rand] = set[index]; + } + return shuffled; + }; + + // Sample **n** random values from a collection. + // If **n** is not specified, returns a single random element. + // The internal `guard` argument allows it to work with `map`. + _.sample = function(obj, n, guard) { + if (n == null || guard) { + if (obj.length !== +obj.length) obj = _.values(obj); + return obj[_.random(obj.length - 1)]; + } + return _.shuffle(obj).slice(0, Math.max(0, n)); + }; + + // Sort the object's values by a criterion produced by an iteratee. + _.sortBy = function(obj, iteratee, context) { + iteratee = _.iteratee(iteratee, context); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value: value, + index: index, + criteria: iteratee(value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(behavior) { + return function(obj, iteratee, context) { + var result = {}; + iteratee = _.iteratee(iteratee, context); + _.each(obj, function(value, index) { + var key = iteratee(value, index, obj); + behavior(result, value, key); + }); + return result; + }; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = group(function(result, value, key) { + if (_.has(result, key)) result[key].push(value); else result[key] = [value]; + }); + + // Indexes the object's values by a criterion, similar to `groupBy`, but for + // when you know that your index values will be unique. + _.indexBy = group(function(result, value, key) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = group(function(result, value, key) { + if (_.has(result, key)) result[key]++; else result[key] = 1; + }); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iteratee, context) { + iteratee = _.iteratee(iteratee, context, 1); + var value = iteratee(obj); + var low = 0, high = array.length; + while (low < high) { + var mid = low + high >>> 1; + if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; + } + return low; + }; + + // Safely create a real, live array from anything iterable. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (obj.length === +obj.length) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return obj.length === +obj.length ? obj.length : _.keys(obj).length; + }; + + // Split a collection into two arrays: one whose elements all satisfy the given + // predicate, and one whose elements all do not satisfy the predicate. + _.partition = function(obj, predicate, context) { + predicate = _.iteratee(predicate, context); + var pass = [], fail = []; + _.each(obj, function(value, key, obj) { + (predicate(value, key, obj) ? pass : fail).push(value); + }); + return [pass, fail]; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + if (n == null || guard) return array[0]; + if (n < 0) return []; + return slice.call(array, 0, n); + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if (array == null) return void 0; + if (n == null || guard) return array[array.length - 1]; + return slice.call(array, Math.max(array.length - n, 0)); + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, n == null || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, _.identity); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, strict, output) { + if (shallow && _.every(input, _.isArray)) { + return concat.apply(output, input); + } + for (var i = 0, length = input.length; i < length; i++) { + var value = input[i]; + if (!_.isArray(value) && !_.isArguments(value)) { + if (!strict) output.push(value); + } else if (shallow) { + push.apply(output, value); + } else { + flatten(value, shallow, strict, output); + } + } + return output; + }; + + // Flatten out an array, either recursively (by default), or just one level. + _.flatten = function(array, shallow) { + return flatten(array, shallow, false, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iteratee, context) { + if (array == null) return []; + if (!_.isBoolean(isSorted)) { + context = iteratee; + iteratee = isSorted; + isSorted = false; + } + if (iteratee != null) iteratee = _.iteratee(iteratee, context); + var result = []; + var seen = []; + for (var i = 0, length = array.length; i < length; i++) { + var value = array[i]; + if (isSorted) { + if (!i || seen !== value) result.push(value); + seen = value; + } else if (iteratee) { + var computed = iteratee(value, i, array); + if (_.indexOf(seen, computed) < 0) { + seen.push(computed); + result.push(value); + } + } else if (_.indexOf(result, value) < 0) { + result.push(value); + } + } + return result; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(flatten(arguments, true, true, [])); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + if (array == null) return []; + var result = []; + var argsLength = arguments.length; + for (var i = 0, length = array.length; i < length; i++) { + var item = array[i]; + if (_.contains(result, item)) continue; + for (var j = 1; j < argsLength; j++) { + if (!_.contains(arguments[j], item)) break; + } + if (j === argsLength) result.push(item); + } + return result; + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = flatten(slice.call(arguments, 1), true, true, []); + return _.filter(array, function(value){ + return !_.contains(rest, value); + }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function(array) { + if (array == null) return []; + var length = _.max(arguments, 'length').length; + var results = Array(length); + for (var i = 0; i < length; i++) { + results[i] = _.pluck(arguments, i); + } + return results; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + if (list == null) return {}; + var result = {}; + for (var i = 0, length = list.length; i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // Return the position of the first occurrence of an item in an array, + // or -1 if the item is not included in the array. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i = 0, length = array.length; + if (isSorted) { + if (typeof isSorted == 'number') { + i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted; + } else { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + } + for (; i < length; i++) if (array[i] === item) return i; + return -1; + }; + + _.lastIndexOf = function(array, item, from) { + if (array == null) return -1; + var idx = array.length; + if (typeof from == 'number') { + idx = from < 0 ? idx + from + 1 : Math.min(idx, from + 1); + } + while (--idx >= 0) if (array[idx] === item) return idx; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = step || 1; + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var range = Array(length); + + for (var idx = 0; idx < length; idx++, start += step) { + range[idx] = start; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Reusable constructor function for prototype setting. + var Ctor = function(){}; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + var args, bound; + if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function'); + args = slice.call(arguments, 2); + bound = function() { + if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); + Ctor.prototype = func.prototype; + var self = new Ctor; + Ctor.prototype = null; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (_.isObject(result)) return result; + return self; + }; + return bound; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. _ acts + // as a placeholder, allowing any combination of arguments to be pre-filled. + _.partial = function(func) { + var boundArgs = slice.call(arguments, 1); + return function() { + var position = 0; + var args = boundArgs.slice(); + for (var i = 0, length = args.length; i < length; i++) { + if (args[i] === _) args[i] = arguments[position++]; + } + while (position < arguments.length) args.push(arguments[position++]); + return func.apply(this, args); + }; + }; + + // Bind a number of an object's methods to that object. Remaining arguments + // are the method names to be bound. Useful for ensuring that all callbacks + // defined on an object belong to it. + _.bindAll = function(obj) { + var i, length = arguments.length, key; + if (length <= 1) throw new Error('bindAll must be passed function names'); + for (i = 1; i < length; i++) { + key = arguments[i]; + obj[key] = _.bind(obj[key], obj); + } + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memoize = function(key) { + var cache = memoize.cache; + var address = hasher ? hasher.apply(this, arguments) : key; + if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); + return cache[address]; + }; + memoize.cache = {}; + return memoize; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ + return func.apply(null, args); + }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + _.throttle = function(func, wait, options) { + var context, args, result; + var timeout = null; + var previous = 0; + if (!options) options = {}; + var later = function() { + previous = options.leading === false ? 0 : _.now(); + timeout = null; + result = func.apply(context, args); + if (!timeout) context = args = null; + }; + return function() { + var now = _.now(); + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + if (!timeout) context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, args, context, timestamp, result; + + var later = function() { + var last = _.now() - timestamp; + + if (last < wait && last > 0) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + if (!timeout) context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = _.now(); + var callNow = immediate && !timeout; + if (!timeout) timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + + return result; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return _.partial(wrapper, func); + }; + + // Returns a negated version of the passed-in predicate. + _.negate = function(predicate) { + return function() { + return !predicate.apply(this, arguments); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var args = arguments; + var start = args.length - 1; + return function() { + var i = start; + var result = args[start].apply(this, arguments); + while (i--) result = args[i].call(this, result); + return result; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Returns a function that will only be executed before being called N times. + _.before = function(times, func) { + var memo; + return function() { + if (--times > 0) { + memo = func.apply(this, arguments); + } else { + func = null; + } + return memo; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = _.partial(_.before, 2); + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = function(obj) { + if (!_.isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys.push(key); + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var values = Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[keys[i]]; + } + return values; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var pairs = Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [keys[i], obj[keys[i]]]; + } + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + result[obj[keys[i]]] = keys[i]; + } + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + if (!_.isObject(obj)) return obj; + var source, prop; + for (var i = 1, length = arguments.length; i < length; i++) { + source = arguments[i]; + for (prop in source) { + if (hasOwnProperty.call(source, prop)) { + obj[prop] = source[prop]; + } + } + } + return obj; + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(obj, iteratee, context) { + var result = {}, key; + if (obj == null) return result; + if (_.isFunction(iteratee)) { + iteratee = createCallback(iteratee, context); + for (key in obj) { + var value = obj[key]; + if (iteratee(value, key, obj)) result[key] = value; + } + } else { + var keys = concat.apply([], slice.call(arguments, 1)); + obj = new Object(obj); + for (var i = 0, length = keys.length; i < length; i++) { + key = keys[i]; + if (key in obj) result[key] = obj[key]; + } + } + return result; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj, iteratee, context) { + if (_.isFunction(iteratee)) { + iteratee = _.negate(iteratee); + } else { + var keys = _.map(concat.apply([], slice.call(arguments, 1)), String); + iteratee = function(value, key) { + return !_.contains(keys, key); + }; + } + return _.pick(obj, iteratee, context); + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + if (!_.isObject(obj)) return obj; + for (var i = 1, length = arguments.length; i < length; i++) { + var source = arguments[i]; + for (var prop in source) { + if (obj[prop] === void 0) obj[prop] = source[prop]; + } + } + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a === 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + switch (className) { + // Strings, numbers, regular expressions, dates, and booleans are compared by value. + case '[object RegExp]': + // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return '' + a === '' + b; + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. + // Object(NaN) is equivalent to NaN + if (+a !== +a) return +b !== +b; + // An `egal` comparison is performed for other numeric values. + return +a === 0 ? 1 / +a === 1 / b : +a === +b; + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a === +b; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) return bStack[length] === b; + } + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if ( + aCtor !== bCtor && + // Handle Object.create(x) cases + 'constructor' in a && 'constructor' in b && + !(_.isFunction(aCtor) && aCtor instanceof aCtor && + _.isFunction(bCtor) && bCtor instanceof bCtor) + ) { + return false; + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size, result; + // Recursively compare objects and arrays. + if (className === '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size === b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + if (!(result = eq(a[size], b[size], aStack, bStack))) break; + } + } + } else { + // Deep compare objects. + var keys = _.keys(a), key; + size = keys.length; + // Ensure that both objects contain the same number of properties before comparing deep equality. + result = _.keys(b).length === size; + if (result) { + while (size--) { + // Deep compare each member + key = keys[size]; + if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; + } + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return result; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, [], []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (_.isArray(obj) || _.isString(obj) || _.isArguments(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) === '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + var type = typeof obj; + return type === 'function' || type === 'object' && !!obj; + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. + _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) === '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return _.has(obj, 'callee'); + }; + } + + // Optimize `isFunction` if appropriate. Work around an IE 11 bug. + if (typeof /./ !== 'function') { + _.isFunction = function(obj) { + return typeof obj == 'function' || false; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj !== +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return obj != null && hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iteratees. + _.identity = function(value) { + return value; + }; + + // Predicate-generating functions. Often useful outside of Underscore. + _.constant = function(value) { + return function() { + return value; + }; + }; + + _.noop = function(){}; + + _.property = function(key) { + return function(obj) { + return obj[key]; + }; + }; + + // Returns a predicate for checking whether an object has a given set of `key:value` pairs. + _.matches = function(attrs) { + var pairs = _.pairs(attrs), length = pairs.length; + return function(obj) { + if (obj == null) return !length; + obj = new Object(obj); + for (var i = 0; i < length; i++) { + var pair = pairs[i], key = pair[0]; + if (pair[1] !== obj[key] || !(key in obj)) return false; + } + return true; + }; + }; + + // Run a function **n** times. + _.times = function(n, iteratee, context) { + var accum = Array(Math.max(0, n)); + iteratee = createCallback(iteratee, context, 1); + for (var i = 0; i < n; i++) accum[i] = iteratee(i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // A (possibly faster) way to get the current timestamp as an integer. + _.now = Date.now || function() { + return new Date().getTime(); + }; + + // List of HTML entities for escaping. + var escapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }; + var unescapeMap = _.invert(escapeMap); + + // Functions for escaping and unescaping strings to/from HTML interpolation. + var createEscaper = function(map) { + var escaper = function(match) { + return map[match]; + }; + // Regexes for identifying a key that needs to be escaped + var source = '(?:' + _.keys(map).join('|') + ')'; + var testRegexp = RegExp(source); + var replaceRegexp = RegExp(source, 'g'); + return function(string) { + string = string == null ? '' : '' + string; + return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; + }; + }; + _.escape = createEscaper(escapeMap); + _.unescape = createEscaper(unescapeMap); + + // If the value of the named `property` is a function then invoke it with the + // `object` as context; otherwise, return it. + _.result = function(object, property) { + if (object == null) return void 0; + var value = object[property]; + return _.isFunction(value) ? object[property]() : value; + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\u2028|\u2029/g; + + var escapeChar = function(match) { + return '\\' + escapes[match]; + }; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + // NB: `oldSettings` only exists for backwards compatibility. + _.template = function(text, settings, oldSettings) { + if (!settings && oldSettings) settings = oldSettings; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset).replace(escaper, escapeChar); + index = offset + match.length; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } else if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } else if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + + // Adobe VMs need the match returned to produce the correct offest. + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + 'return __p;\n'; + + try { + var render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled source as a convenience for precompilation. + var argument = settings.variable || 'obj'; + template.source = 'function(' + argument + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function. Start chaining a wrapped Underscore object. + _.chain = function(obj) { + var instance = _(obj); + instance._chain = true; + return instance; + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(obj) { + return this._chain ? _(obj).chain() : obj; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + _.each(_.functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result.call(this, func.apply(_, args)); + }; + }); + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; + return result.call(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + _.each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result.call(this, method.apply(this._wrapped, arguments)); + }; + }); + + // Extracts the result from a wrapped and chained object. + _.prototype.value = function() { + return this._wrapped; + }; + + // AMD registration happens at the end for compatibility with AMD loaders + // that may not enforce next-turn semantics on modules. Even though general + // practice for AMD registration is to be anonymous, underscore registers + // as a named module because, like jQuery, it is a base library that is + // popular enough to be bundled in a third party lib, but not be part of + // an AMD load request. Those cases could generate an error when an + // anonymous define() is called outside of a loader request. + if (typeof define === 'function' && define.amd) { + define('underscore', [], function() { + return _; + }); + } +}.call(this)); \ No newline at end of file diff --git a/media/js/views/browser.js b/media/js/views/browser.js new file mode 100644 index 0000000..90bf48c --- /dev/null +++ b/media/js/views/browser.js @@ -0,0 +1,178 @@ +/* + * BROWSER VIEW + * This is the "All Rooms" browser! + */ + +'use strict'; + ++function(window, $, _) { + + window.LCB = window.LCB || {}; + + window.LCB.BrowserView = Backbone.View.extend({ + events: { + 'submit .lcb-rooms-add': 'create', + 'keyup .lcb-rooms-browser-filter-input': 'filter', + 'change .lcb-rooms-switch': 'toggle', + 'click .lcb-rooms-switch-label': 'toggle' + }, + initialize: function(options) { + this.client = options.client; + this.template = Handlebars.compile($('#template-room-browser-item').html()); + this.userTemplate = Handlebars.compile($('#template-room-browser-item-user').html()); + this.rooms = options.rooms; + this.rooms.on('add', this.add, this); + this.rooms.on('remove', this.remove, this); + this.rooms.on('change:description change:name', this.update, this); + this.rooms.on('change:lastActive', _.debounce(this.updateLastActive, 200), this); + this.rooms.on('change:joined', this.updateToggles, this); + this.rooms.on('users:add', this.addUser, this); + this.rooms.on('users:remove', this.removeUser, this); + this.rooms.on('users:add users:remove add remove', this.sort, this); + this.rooms.current.on('change:id', function(current, id) { + // We only care about the list pane + if (id !== 'list') return; + this.sort(); + }, this); + }, + updateToggles: function(room, joined) { + this.$('.lcb-rooms-switch[data-id=' + room.id + ']').prop('checked', joined); + }, + toggle: function(e) { + e.preventDefault(); + var $target = $(e.currentTarget), + $input = $target.is(':checkbox') && $target || $target.siblings('[type="checkbox"]'), + id = $input.data('id'), + room = this.rooms.get(id); + + if (!room) { + return; + } + + if (room.get('joined')) { + this.client.leaveRoom(room.id); + } else { + this.client.joinRoom(room); + } + }, + add: function(room) { + var room = room.toJSON ? room.toJSON() : room, + context = _.extend(room, { + lastActive: moment(room.lastActive).calendar() + }); + this.$('.lcb-rooms-list').append(this.template(context)); + }, + remove: function(room) { + this.$('.lcb-rooms-list-item[data-id=' + room.id + ']').remove(); + }, + update: function(room) { + this.$('.lcb-rooms-list-item[data-id=' + room.id + '] .lcb-rooms-list-item-name').text(room.get('name')); + this.$('.lcb-rooms-list-item[data-id=' + room.id + '] .lcb-rooms-list-item-description').text(room.get('description')); + this.$('.lcb-rooms-list-item[data-id=' + room.id + '] .lcb-rooms-list-item-participants').text(room.get('participants')); + }, + updateLastActive: function(room) { + this.$('.lcb-rooms-list-item[data-id=' + room.id + '] .lcb-rooms-list-item-last-active .value').text(moment(room.get('lastActive')).calendar()); + }, + sort: function(model) { + var that = this, + $items = this.$('.lcb-rooms-list-item'); + // We only care about other users + if (this.$el.hasClass('hide') && model && model.id === this.client.user.id) + return; + $items.sort(function(a, b){ + var ar = that.rooms.get($(a).data('id')), + br = that.rooms.get($(b).data('id')), + au = ar.users.length, + bu = br.users.length, + aj = ar.get('joined'), + bj = br.get('joined'); + if ((aj && bj) || (!aj && !bj)) { + if (au > bu) return -1; + if (au < bu) return 1; + } + if (aj) return -1; + if (bj) return 1; + return 0; + }); + $items.detach().appendTo(this.$('.lcb-rooms-list')); + }, + filter: function(e) { + e.preventDefault(); + var $input = $(e.currentTarget), + needle = $input.val().toLowerCase(); + this.$('.lcb-rooms-list-item').each(function () { + var haystack = $(this).find('.lcb-rooms-list-item-name').text().toLowerCase(); + $(this).toggle(haystack.indexOf(needle) >= 0); + }); + }, + create: function(e) { + var that = this; + e.preventDefault(); + var $form = this.$(e.target), + $modal = this.$('#lcb-add-room'), + $name = this.$('.lcb-room-name'), + $slug = this.$('.lcb-room-slug'), + $description = this.$('.lcb-room-description'), + $password = this.$('.lcb-room-password'), + $confirmPassword = this.$('.lcb-room-confirm-password'), + $private = this.$('.lcb-room-private'), + data = { + name: $name.val().trim(), + slug: $slug.val().trim(), + description: $description.val(), + password: $password.val(), + private: !!$private.prop('checked'), + callback: function success() { + $modal.modal('hide'); + $form.trigger('reset'); + } + }; + + $name.parent().removeClass('has-error'); + $slug.parent().removeClass('has-error'); + $confirmPassword.parent().removeClass('has-error'); + + // we require name is non-empty + if (!data.name) { + $name.parent().addClass('has-error'); + return; + } + + // we require slug is non-empty + if (!data.slug) { + $slug.parent().addClass('has-error'); + return; + } + + // remind the user, that users may share the password with others + if (data.password) { + if (data.password !== $confirmPassword.val()) { + $confirmPassword.parent().addClass('has-error'); + return; + } + + swal({ + title: 'Password-protected room', + text: 'You\'re creating a room with a shared password.\n' + + 'Anyone who obtains the password may enter the room.', + showCancelButton: true + }, function(){ + that.client.events.trigger('rooms:create', data); + }); + return; + } + + this.client.events.trigger('rooms:create', data); + }, + addUser: function(user, room) { + this.$('.lcb-rooms-list-item[data-id="' + room.id + '"]') + .find('.lcb-rooms-list-users').prepend(this.userTemplate(user.toJSON())); + }, + removeUser: function(user, room) { + this.$('.lcb-rooms-list-item[data-id="' + room.id + '"]') + .find('.lcb-rooms-list-user[data-id="' + user.id + '"]').remove(); + } + + }); + +}(window, $, _); diff --git a/media/js/views/client.js b/media/js/views/client.js new file mode 100644 index 0000000..b14b305 --- /dev/null +++ b/media/js/views/client.js @@ -0,0 +1,109 @@ +/* + * CLIENT VIEW + * The king of all views. + */ + +'use strict'; + ++function(window, $, _) { + + window.LCB = window.LCB || {}; + + window.LCB.ClientView = Backbone.View.extend({ + el: '#lcb-client', + events: { + 'click .lcb-tab': 'toggleSideBar', + 'click .lcb-header-toggle': 'toggleSideBar' + }, + initialize: function(options) { + this.client = options.client; + // + // Subviews + // + this.browser = new window.LCB.BrowserView({ + el: this.$el.find('.lcb-rooms-browser'), + rooms: this.client.rooms, + client: this.client + }); + this.tabs = new window.LCB.TabsView({ + el: this.$el.find('.lcb-tabs'), + rooms: this.client.rooms, + client: this.client + }); + this.panes = new window.LCB.PanesView({ + el: this.$el.find('.lcb-panes'), + rooms: this.client.rooms, + client: this.client + }); + this.window = new window.LCB.WindowView({ + rooms: this.client.rooms, + client: this.client + }); + this.hotKeys = new window.LCB.HotKeysView({ + rooms: this.client.rooms, + client: this.client + }); + this.status = new window.LCB.StatusView({ + el: this.$el.find('.lcb-status-indicators'), + client: this.client + }); + this.accountButton = new window.LCB.AccountButtonView({ + el: this.$el.find('.lcb-account-button'), + model: this.client.user + }); + this.desktopNotifications = new window.LCB.DesktopNotificationsView({ + rooms: this.client.rooms, + client: this.client + }); + if (this.client.options.filesEnabled) { + this.upload = new window.LCB.UploadView({ + el: this.$el.find('#lcb-upload'), + rooms: this.client.rooms + }); + } + + // + // Modals + // + this.profileModal = new window.LCB.ProfileModalView({ + el: this.$el.find('#lcb-profile'), + model: this.client.user + }); + this.accountModal = new window.LCB.AccountModalView({ + el: this.$el.find('#lcb-account'), + model: this.client.user + }); + this.tokenModal = new window.LCB.AuthTokensModalView({ + el: this.$el.find('#lcb-tokens') + }); + this.notificationsModal = new window.LCB.NotificationsModalView({ + el: this.$el.find('#lcb-notifications') + }); + this.giphyModal = new window.LCB.GiphyModalView({ + el: this.$el.find('#lcb-giphy') + }); + // + // Misc + // + this.client.status.once('change:connected', _.bind(function(status, connected) { + this.$el.find('.lcb-client-loading').hide(connected); + }, this)); + return this; + }, + toggleSideBar: function(e) { + this.$el.toggleClass('lcb-sidebar-opened'); + } + }); + + window.LCB.AccountButtonView = Backbone.View.extend({ + initialize: function() { + this.model.on('change', this.update, this); + }, + update: function(user){ + this.$('.lcb-account-button-username').text('@' + user.get('username')); + this.$('.lcb-account-button-name').text(user.get('displayName')); + } + }); + + +}(window, $, _); diff --git a/media/js/views/modals.js b/media/js/views/modals.js new file mode 100644 index 0000000..6ac2c12 --- /dev/null +++ b/media/js/views/modals.js @@ -0,0 +1,271 @@ +/* + * MODAL VIEWS + */ + +'use strict'; + ++function(window, $, _) { + + window.LCB = window.LCB || {}; + + window.LCB.ModalView = Backbone.View.extend({ + events: { + 'submit form': 'submit' + }, + initialize: function(options) { + this.render(); + }, + render: function() { + this.$('form.validate').validate(); + this.$el.on('shown.bs.modal hidden.bs.modal', + _.bind(this.refresh, this)); + }, + refresh: function() { + var that = this; + this.$('[data-model]').each(function() { + $(this).val && $(this).val(that.model.get($(this).data('model'))); + }); + }, + success: function() { + swal('Updated!', '', 'success'); + this.$el.modal('hide'); + }, + error: function() { + swal('Woops!', '', 'error'); + }, + submit: function(e) { + e && e.preventDefault(); + + var $form = this.$('form[action]'); + var opts = { + type: $form.attr('method') || 'POST', + url: $form.attr('action'), + data: $form.serialize(), + dataType: 'json' + }; + + if (this.success) { + opts.success = _.bind(this.success, this); + } + if (this.error) { + opts.error = _.bind(this.error, this); + } + if (this.complete) { + opts.complete = _.bind(this.complete, this); + } + + $.ajax(opts); + } + }); + + window.LCB.ProfileModalView = window.LCB.ModalView.extend({ + success: function() { + swal('Profile Updated!', 'Your profile has been updated.', + 'success'); + this.$el.modal('hide'); + }, + error: function() { + swal('Woops!', 'Your profile was not updated.', 'error'); + } + }); + + window.LCB.AccountModalView = window.LCB.ModalView.extend({ + success: function() { + swal('Account Updated!', 'Your account has been updated.', 'success'); + this.$el.modal('hide'); + this.$('[type="password"]').val(''); + }, + error: function(req) { + var message = req.responseJSON && req.responseJSON.reason || + 'Your account was not updated.'; + + swal('Woops!', message, 'error'); + }, + complete: function() { + this.$('[name="current-password"]').val(''); + } + }); + + window.LCB.RoomPasswordModalView = Backbone.View.extend({ + events: { + 'click .btn-primary': 'enterRoom' + }, + initialize: function(options) { + this.render(); + this.$name = this.$('.lcb-room-password-name'); + this.$password = this.$('input.lcb-room-password-required'); + }, + render: function() { + // this.$el.on('shown.bs.modal hidden.bs.modal', + // _.bind(this.refresh, this)); + }, + show: function(options) { + this.callback = options.callback; + this.$password.val(''); + this.$name.text(options.roomName || ''); + this.$el.modal('show'); + }, + enterRoom: function() { + this.$el.modal('hide'); + this.callback(this.$password.val()); + } + }); + + window.LCB.AuthTokensModalView = Backbone.View.extend({ + events: { + 'click .generate-token': 'generateToken', + 'click .revoke-token': 'revokeToken' + }, + initialize: function(options) { + this.render(); + }, + render: function() { + this.$el.on('shown.bs.modal hidden.bs.modal', + _.bind(this.refresh, this)); + }, + refresh: function() { + this.$('.token').val(''); + this.$('.generated-token').hide(); + }, + getToken: function() { + var that = this; + $.post('./account/token/generate', function(data) { + if (data.token) { + that.$('.token').val(data.token); + that.$('.generated-token').show(); + } + }); + }, + removeToken: function() { + var that = this; + $.post('./account/token/revoke', function(data) { + that.refresh(); + swal('Success', 'Authentication token revoked!', 'success'); + }); + }, + generateToken: function() { + swal({ + title: 'Are you sure?', + text: 'This will overwrite any existing authentication token you may have.', type: 'warning', + showCancelButton: true, + confirmButtonText: 'Yes', + closeOnConfirm: true }, + _.bind(this.getToken, this) + ); + }, + revokeToken: function() { + swal({ + title: 'Are you sure?', + text: 'This will revoke access from any process using your current authentication token.', type: 'warning', + showCancelButton: true, + confirmButtonText: 'Yes', + closeOnConfirm: false }, + _.bind(this.removeToken, this) + ); + } + }); + + window.LCB.NotificationsModalView = Backbone.View.extend({ + events: { + 'click [name=desktop-notifications]': 'toggleDesktopNotifications' + }, + initialize: function() { + this.render(); + }, + render: function() { + var $input = this.$('[name=desktop-notifications]'); + $input.find('.disabled').show() + .siblings().hide(); + if (!notify.isSupported) { + $input.attr('disabled', true); + // Welp we're done here + return; + } + if (notify.permissionLevel() === notify.PERMISSION_GRANTED) { + $input.find('.enabled').show() + .siblings().hide(); + } + if (notify.permissionLevel() === notify.PERMISSION_DENIED) { + $input.find('.blocked').show() + .siblings().hide(); + } + }, + toggleDesktopNotifications: function() { + var that = this; + if (!notify.isSupported) { + return; + } + notify.requestPermission(function() { + that.render(); + }); + } + }); + + window.LCB.GiphyModalView = Backbone.View.extend({ + events: { + 'keypress .search-giphy': 'stopReturn', + 'keyup .search-giphy': 'loadGifs' + }, + initialize: function(options) { + this.render(); + }, + render: function() { + this.$el.on('shown.bs.modal hidden.bs.modal', + _.bind(this.refresh, this)); + }, + refresh: function() { + this.$el.find('.giphy-results ul').empty(); + this.$('.search-giphy').val('').focus(); + }, + stopReturn: function(e) { + if(e.keyCode === 13) { + return false; + } + }, + loadGifs: _.debounce(function() { + var that = this; + var search = this.$el.find('.search-giphy').val(); + + $.get('https://api.giphy.com/v1/gifs/search', { + q: search, + rating: this.$el.data('rating'), + limit: this.$el.data('limit'), + api_key: this.$el.data('apikey') + }) + .done(function(result) { + var images = result.data.filter(function(entry) { + return entry.images.fixed_width.url; + }).map(function(entry) { + return entry.images.fixed_width.url; + }); + + that.appendGifs(images); + }); + }, 400), + appendGifs: function(images) { + var eles = images.map(function(url) { + var that = this; + var $img = $('gif'); + + $img.click(function() { + var src = $(this).attr('src'); + $('.lcb-entry-input:visible').val(src); + $('.lcb-entry-button:visible').click(); + that.$el.modal('hide'); + }); + + return $("
  • ").append($img); + }, this); + + var $div = this.$el.find('.giphy-results ul'); + + $div.empty(); + + eles.forEach(function($ele) { + $div.append($ele); + }); + } + }); + +}(window, $, _); diff --git a/media/js/views/panes.js b/media/js/views/panes.js new file mode 100644 index 0000000..d788aad --- /dev/null +++ b/media/js/views/panes.js @@ -0,0 +1,157 @@ +/* + * TABS/PANES VIEW + */ + +'use strict'; + ++function(window, $, _) { + + window.LCB = window.LCB || {}; + + window.LCB.TabsView = Backbone.View.extend({ + events: { + 'click .lcb-tab-close': 'leave' + }, + focus: true, + initialize: function(options) { + this.client = options.client; + this.template = Handlebars.compile($('#template-room-tab').html()); + this.rooms = options.rooms; + // Room joining + this.rooms.on('change:joined', function(room, joined) { + if (joined) { + this.add(room.toJSON()); + return; + } + this.remove(room.id); + }, this); + // Room meta updates + this.rooms.on('change:name change:description', this.update, this); + // Current room switching + this.rooms.current.on('change:id', function(current, id) { + this.switch(id); + this.clearAlerts(id); + }, this); + // Alerts + this.rooms.on('messages:new', this.alert, this); + // Initial switch since router runs before view is loaded + this.switch(this.rooms.current.get('id')); + // Blur/Focus events + $(window).on('focus blur', _.bind(this.onFocusBlur, this)); + this.render(); + }, + add: function(room) { + this.$el.append(this.template(room)); + }, + remove: function(id) { + this.$el.find('.lcb-tab[data-id=' + id + ']').remove(); + }, + update: function(room) { + this.$el.find('.lcb-tab[data-id=' + room.id + '] .lcb-tab-title').text(room.get('name')); + }, + switch: function(id) { + if (!id) { + id = 'list'; + } + this.$el.find('.lcb-tab').removeClass('selected') + .filter('[data-id=' + id + ']').addClass('selected'); + }, + leave: function(e) { + e.preventDefault(); + var id = $(e.currentTarget).closest('[data-id]').data('id'); + this.client.events.trigger('rooms:leave', id); + }, + alert: function(message) { + var $tab = this.$('.lcb-tab[data-id=' + message.room.id + ']'), + $total = $tab.find('.lcb-tab-alerts-total'), + $mentions = $tab.find('.lcb-tab-alerts-mentions'); + if (message.historical || $tab.length === 0 + || ((this.rooms.current.get('id') === message.room.id) && this.focus)) { + // Nothing to do here! + return; + } + var total = parseInt($tab.data('count-total')) || 0, + mentions = parseInt($tab.data('count-mentions')) || 0; + // All messages + $tab.data('count-total', ++total); + $total.text(total); + // Just mentions + if (new RegExp('\\B@(' + this.client.user.get('username') + ')(?!@)\\b', 'i').test(message.text)) { + $tab.data('count-mentions', ++mentions); + $mentions.text(mentions); + } + }, + clearAlerts: function(id) { + var $tab = this.$('.lcb-tab[data-id=' + id + ']'), + $total = $tab.find('.lcb-tab-alerts-total'), + $mentions = $tab.find('.lcb-tab-alerts-mentions'); + $tab.data('count-total', 0).data('count-mentions', 0); + $total.text(''); + $mentions.text(''); + }, + onFocusBlur: function(e) { + var that = this; + this.focus = (e.type === 'focus'); + clearTimeout(this.clearTimer); + if (this.focus) { + this.clearTimer = setTimeout(function() { + that.clearAlerts(that.rooms.current.get('id')); + }, 1000); + return; + } + that.clearAlerts(that.rooms.current.get('id')); + } + }); + + window.LCB.PanesView = Backbone.View.extend({ + initialize: function(options) { + this.client = options.client; + this.template = Handlebars.compile($('#template-room').html()); + this.rooms = options.rooms; + this.views = {}; + this.rooms.on('change:joined', function(room, joined) { + if (joined) { + this.add(room); + return; + } + this.remove(room.id); + }, this); + // Switch room + this.rooms.current.on('change:id', function(current, id) { + this.switch(id); + }, this); + // Initial switch since router runs before view is loaded + this.switch(this.rooms.current.get('id')); + }, + switch: function(id) { + if (!id) { + id = 'list'; + } + var $pane = this.$el.find('.lcb-pane[data-id=' + id + ']'); + $pane.removeClass('hide').siblings().addClass('hide'); + $(window).width() > 767 && $pane.find('[autofocus]').focus(); + this.views[id] && this.views[id].scrollMessages(true); + }, + add: function(room) { + if (this.views[room.id]) { + // Nothing to do, this room is already here + return; + } + this.views[room.id] = new window.LCB.RoomView({ + client: this.client, + template: this.template, + model: room + }); + this.$el.append(this.views[room.id].$el); + }, + remove: function(id) { + if (!this.views[id]) { + // Nothing to do here + return; + } + this.views[id].destroy(); + delete this.views[id]; + } + }); + +}(window, $, _); diff --git a/media/js/views/room.js b/media/js/views/room.js new file mode 100644 index 0000000..8b46d14 --- /dev/null +++ b/media/js/views/room.js @@ -0,0 +1,508 @@ +/* + * ROOM VIEW + * TODO: Break it up :/ + */ + +'use strict'; + ++function(window, $, _) { + + window.LCB = window.LCB || {}; + + window.LCB.RoomView = Backbone.View.extend({ + events: { + 'scroll .lcb-messages': 'updateScrollLock', + 'keypress .lcb-entry-input': 'sendMessage', + 'click .lcb-entry-button': 'sendMessage', + 'DOMCharacterDataModified .lcb-room-heading, .lcb-room-description': 'sendMeta', + 'click .lcb-room-toggle-sidebar': 'toggleSidebar', + 'click .show-edit-room': 'showEditRoom', + 'click .hide-edit-room': 'hideEditRoom', + 'click .submit-edit-room': 'submitEditRoom', + 'click .archive-room': 'archiveRoom', + 'click .lcb-room-poke': 'poke', + 'click .lcb-upload-trigger': 'upload' + }, + initialize: function(options) { + this.client = options.client; + + var iAmOwner = this.model.get('owner') === this.client.user.id; + var iCanEdit = iAmOwner || !this.model.get('hasPassword'); + + this.model.set('iAmOwner', iAmOwner); + this.model.set('iCanEdit', iCanEdit); + + this.template = options.template; + this.messageTemplate = + Handlebars.compile($('#template-message').html()); + this.render(); + this.model.on('messages:new', this.addMessage, this); + this.model.on('change', this.updateMeta, this); + this.model.on('remove', this.goodbye, this); + this.model.users.on('change', this.updateUser, this); + + // + // Subviews + // + this.usersList = new window.LCB.RoomUsersView({ + el: this.$('.lcb-room-sidebar-users'), + collection: this.model.users + }); + this.filesList = new window.LCB.RoomFilesView({ + el: this.$('.lcb-room-sidebar-files'), + collection: this.model.files + }); + }, + render: function() { + this.$el = $(this.template(_.extend(this.model.toJSON(), { + sidebar: store.get('sidebar') + }))); + this.$messages = this.$('.lcb-messages'); + // Scroll Locking + this.scrollLocked = true; + this.$messages.on('scroll', _.bind(this.updateScrollLock, this)); + this.atwhoMentions(); + this.atwhoAllMentions(); + this.atwhoRooms(); + this.atwhoEmotes(); + this.selectizeParticipants(); + }, + atwhoTplEval: function(tpl, map) { + var error; + try { + return tpl.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) { + return (map[key] || '') + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); + }); + } catch (_error) { + error = _error; + return ""; + } + }, + getAtwhoUserFilter: function(collection) { + var currentUser = this.client.user; + + return function filter(query, data, searchKey) { + var q = query.toLowerCase(); + var results = collection.filter(function(user) { + var attr = user.attributes; + + if (user.id === currentUser.id) { + return false; + } + + if (!attr.safeName) { + attr.safeName = attr.displayName.replace(/\W/g, ''); + } + + var val1 = attr.username.toLowerCase(); + var val1i = val1.indexOf(q); + if (val1i > -1) { + attr.atwho_order = val1i; + return true; + } + + var val2 = attr.safeName.toLowerCase(); + var val2i = val2.indexOf(q); + if (val2i > -1) { + attr.atwho_order = val2i + attr.username.length; + return true; + } + + return false; + }); + + return results.map(function(user) { + return user.attributes; + }); + }; + }, + atwhoMentions: function () { + + function sorter(query, items, search_key) { + return items.sort(function(a, b) { + return a.atwho_order - b.atwho_order; + }); + } + var options = { + at: '@', + tpl: '
  • @${username} ${displayName}
  • ', + callbacks: { + filter: this.getAtwhoUserFilter(this.model.users), + sorter: sorter, + tpl_eval: this.atwhoTplEval + } + }; + + this.$('.lcb-entry-input').atwho(options); + }, + atwhoAllMentions: function () { + var that = this; + + function filter(query, data, searchKey) { + var users = that.client.getUsersSync(); + var filt = that.getAtwhoUserFilter(users); + return filt(query, data, searchKey); + } + + function sorter(query, items, search_key) { + return items.sort(function(a, b) { + return a.atwho_order - b.atwho_order; + }); + } + + var options = { + at: '@@', + tpl: '
  • @${username} ${displayName}
  • ', + callbacks: { + filter: filter, + sorter: sorter, + tpl_eval: that.atwhoTplEval + } + }; + + this.$('.lcb-entry-input').atwho(options); + + var opts = _.extend(options, { at: '@'}); + this.$('.lcb-entry-participants').atwho(opts); + this.$('.lcb-room-participants').atwho(opts); + }, + selectizeParticipants: function () { + var that = this; + + this.$('.lcb-entry-participants').selectize({ + delimiter: ',', + create: false, + load: function(query, callback) { + if (!query.length) return callback(); + + var users = that.client.getUsersSync(); + + var usernames = users.map(function(user) { + return user.attributes.username; + }); + + usernames = _.filter(usernames, function(username) { + return username.indexOf(query) !== -1; + }); + + users = _.map(usernames, function(username) { + return { + value: username, + text: username + }; + }); + + callback(users); + } + }); + }, + atwhoRooms: function() { + var rooms = this.client.rooms; + + function filter(query, data, searchKey) { + var q = query.toLowerCase(); + var results = rooms.filter(function(room) { + var val = room.attributes.slug.toLowerCase(); + return val.indexOf(q) > -1; + }); + + return results.map(function(room) { + return room.attributes; + }); + } + + this.$('.lcb-entry-input') + .atwho({ + at: '#', + search_key: 'slug', + callbacks: { + filter: filter, + tpl_eval: this.atwhoTplEval + }, + tpl: '
  • #${slug} ${name}
  • ' + }); + }, + atwhoEmotes: function() { + var that = this; + this.client.getEmotes(function(emotes) { + that.$('.lcb-entry-input') + .atwho({ + at: ':', + search_key: 'emote', + data: emotes, + tpl: '
  • :${emote}: :${emote}:
  • ' + }); + }); + }, + goodbye: function() { + swal('Archived!', '"' + this.model.get('name') + '" has been archived.', 'warning'); + }, + updateMeta: function() { + this.$('.lcb-room-heading .name').text(this.model.get('name')); + this.$('.lcb-room-heading .slug').text('#' + this.model.get('slug')); + this.$('.lcb-room-description').text(this.model.get('description')); + this.$('.lcb-room-participants').text(this.model.get('participants')); + }, + sendMeta: function(e) { + this.model.set({ + name: this.$('.lcb-room-heading').text(), + description: this.$('.lcb-room-description').text(), + participants: this.$('.lcb-room-participants').text() + }); + this.client.events.trigger('rooms:update', { + id: this.model.id, + name: this.model.get('name'), + description: this.model.get('description'), + participants: this.model.get('participants') + }); + }, + showEditRoom: function(e) { + if (e) { + e.preventDefault(); + } + + var $modal = this.$('.lcb-room-edit'), + $name = $modal.find('input[name="name"]'), + $description = $modal.find('textarea[name="description"]'), + $password = $modal.find('input[name="password"]'), + $confirmPassword = $modal.find('input[name="confirmPassword"]'); + + $name.val(this.model.get('name')); + $description.val(this.model.get('description')); + $password.val(''); + $confirmPassword.val(''); + + $modal.modal(); + }, + hideEditRoom: function(e) { + if (e) { + e.preventDefault(); + } + this.$('.lcb-room-edit').modal('hide'); + }, + submitEditRoom: function(e) { + if (e) { + e.preventDefault(); + } + + var $modal = this.$('.lcb-room-edit'), + $name = $modal.find('input[name="name"]'), + $description = $modal.find('textarea[name="description"]'), + $password = $modal.find('input[name="password"]'), + $confirmPassword = $modal.find('input[name="confirmPassword"]'), + $participants = + this.$('.edit-room textarea[name="participants"]'); + + $name.parent().removeClass('has-error'); + $confirmPassword.parent().removeClass('has-error'); + + if (!$name.val()) { + $name.parent().addClass('has-error'); + return; + } + + if ($password.val() && $password.val() !== $confirmPassword.val()) { + $confirmPassword.parent().addClass('has-error'); + return; + } + + this.client.events.trigger('rooms:update', { + id: this.model.id, + name: $name.val(), + description: $description.val(), + password: $password.val(), + participants: $participants.val() + }); + + $modal.modal('hide'); + }, + archiveRoom: function(e) { + var that = this; + swal({ + title: 'Do you really want to archive "' + + this.model.get('name') + '"?', + text: "You will not be able to open it!", + type: "error", + confirmButtonText: "Yes, I'm sure", + allowOutsideClick: true, + confirmButtonColor: "#DD6B55", + showCancelButton: true, + closeOnConfirm: true, + }, function(isConfirm) { + if (isConfirm) { + that.$('.lcb-room-edit').modal('hide'); + that.client.events.trigger('rooms:archive', { + room: that.model.id + }); + } + }); + }, + sendMessage: function(e) { + if (e.type === 'keypress' && e.keyCode !== 13 || e.altKey) return; + if (e.type === 'keypress' && e.keyCode === 13 && e.shiftKey) return; + e.preventDefault(); + if (!this.client.status.get('connected')) return; + var $textarea = this.$('.lcb-entry-input'); + if (!$textarea.val()) return; + this.client.events.trigger('messages:send', { + room: this.model.id, + text: $textarea.val() + }); + $textarea.val(''); + this.scrollLocked = true; + this.scrollMessages(); + }, + addMessage: function(message) { + // Smells like pasta + message.paste = /\n/i.test(message.text); + + var posted = moment(message.posted); + + // Fragment or new message? + message.fragment = this.lastMessageOwner === message.owner.id && + posted.diff(this.lastMessagePosted, 'minutes') < 2; + + // Mine? Mine? Mine? Mine? + message.own = this.client.user.id === message.owner.id; + + // WHATS MY NAME + message.mentioned = new RegExp('\\B@(' + this.client.user.get('username') + '|all)(?!@)\\b', 'i').test(message.text); + + // Templatin' time + var $html = $(this.messageTemplate(message).trim()); + var $text = $html.find('.lcb-message-text'); + + var that = this; + this.formatMessage($text.html(), function(text) { + $text.html(text); + $html.find('time').updateTimeStamp(); + that.$messages.append($html); + + if (!message.fragment) { + that.lastMessagePosted = posted; + that.lastMessageOwner = message.owner.id; + } + + that.scrollMessages(); + }); + + }, + formatMessage: function(text, cb) { + var client = this.client; + client.getEmotes(function(emotes) { + client.getReplacements(function(replacements) { + var data = { + emotes: emotes, + replacements: replacements, + rooms: client.rooms + }; + + var msg = window.utils.message.format(text, data); + cb(msg); + }); + }); + }, + updateScrollLock: function() { + this.scrollLocked = this.$messages[0].scrollHeight - + this.$messages.scrollTop() - 5 <= this.$messages.outerHeight(); + return this.scrollLocked; + }, + scrollMessages: function(force) { + if ((!force && !this.scrollLocked) || this.$el.hasClass('hide')) { + return; + } + this.$messages[0].scrollTop = this.$messages[0].scrollHeight; + }, + toggleSidebar: function(e) { + e && e.preventDefault && e.preventDefault(); + // Target siblings too! + this.$el.siblings('.lcb-room').andSelf().toggleClass('lcb-room-sidebar-opened'); + // Save to localstorage + if ($(window).width() > 767) { + this.scrollMessages(); + store.set('sidebar', + this.$el.hasClass('lcb-room-sidebar-opened')); + } + }, + destroy: function() { + this.undelegateEvents(); + this.$el.removeData().unbind(); + this.remove(); + Backbone.View.prototype.remove.call(this); + }, + poke: function(e) { + var $target = $(e.currentTarget), + $root = $target.closest('[data-id],[data-owner]'), + id = $root.data('owner') || $root.data('id'), + user = this.model.users.findWhere({ + id: id + }); + if (!user) return; + var $input = this.$('.lcb-entry-input'), + text = $.trim($input.val()), + at = (text.length > 0 ? ' ' : '') + '@' + user.get('username') + ' ' + $input.val(text + at).focus(); + }, + upload: function(e) { + e.preventDefault(); + this.model.trigger('upload:show', this.model); + }, + updateUser: function(user) { + var $messages = this.$('.lcb-message[data-owner="' + user.id + '"]'); + $messages.find('.lcb-message-username').text('@' + user.get('username')); + $messages.find('.lcb-message-displayname').text(user.get('displayName')); + } + }); + + window.LCB.RoomSidebarListView = Backbone.View.extend({ + initialize: function(options) { + this.template = Handlebars.compile($(this.templateSelector).html()); + this.collection.on('add remove', function() { + this.count(); + }, this); + this.collection.on('add', function(model) { + this.add(model.toJSON()); + }, this); + this.collection.on('change', function(model) { + this.update(model.toJSON()); + }, this); + this.collection.on('remove', function(model) { + this.remove(model.id); + }, this); + this.render(); + }, + render: function() { + this.collection.each(function(model) { + this.add(model.toJSON()); + }, this); + this.count(); + }, + add: function(model) { + this.$('.lcb-room-sidebar-list').prepend(this.template(model)); + }, + remove: function(id) { + this.$('.lcb-room-sidebar-item[data-id=' + id + ']').remove(); + }, + count: function(models) { + this.$('.lcb-room-sidebar-items-count').text(this.collection.length); + }, + update: function(model){ + this.$('.lcb-room-sidebar-item[data-id=' + model.id + ']') + .replaceWith(this.template(model)); + } + }); + + window.LCB.RoomUsersView = window.LCB.RoomSidebarListView.extend({ + templateSelector: '#template-user' + }); + + window.LCB.RoomFilesView = window.LCB.RoomSidebarListView.extend({ + templateSelector: '#template-file' + }); + +}(window, $, _); diff --git a/media/js/views/status.js b/media/js/views/status.js new file mode 100644 index 0000000..0089fe5 --- /dev/null +++ b/media/js/views/status.js @@ -0,0 +1,23 @@ +/* + * STATUS VIEW + * Shows the user connected/disconnected + */ + +'use strict'; + ++function(window, $, _) { + + window.LCB = window.LCB || {}; + + window.LCB.StatusView = Backbone.View.extend({ + initialize: function(options) { + var that = this; + this.client = options.client; + this.client.status.on('change:connected', function(status, connected) { + that.$el.find('[data-status="connected"]').toggle(connected); + that.$el.find('[data-status="disconnected"]').toggle(!connected); + }); + } + }); + +}(window, $, _); \ No newline at end of file diff --git a/media/js/views/transcript.js b/media/js/views/transcript.js new file mode 100644 index 0000000..4a56b4b --- /dev/null +++ b/media/js/views/transcript.js @@ -0,0 +1,131 @@ +'use strict'; + ++function(window, $, _) { + + window.LCB = window.LCB || {}; + + window.LCB.TranscriptView = Backbone.View.extend({ + events: { + 'keyup .lcb-search-entry': 'search' + }, + initialize: function(options) { + + var that = this; + + this.options = options; + this.room = options.room; + + this.$messages = this.$('.lcb-transcript-messages'); + this.messageTemplate = + Handlebars.compile($('#template-message').html()); + + this.$query = this.$('.lcb-search-entry'); + + $.when( + $.get('./extras/emotes'), $.get('./extras/replacements') + ).done(function(emotes, replacements) { + that.formatData = { + emotes: emotes[0], + replacements: replacements[0] + }; + that.setup(); + }); + + }, + setup: function() { + + var that = this, + format = 'MMMM D, YYYY', + ranges = { + 'Today': [moment(), moment()], + 'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')], + 'Last 7 Days': [moment().subtract(6, 'days'), moment()], + 'Last 30 Days': [moment().subtract(29, 'days'), moment()], + } + + function setRange(start, end) { + that.startDate = moment(start).local().startOf('day'); + that.endDate = moment(end).local().endOf('day'); + + var str = that.startDate.format(format) + ' - ' + that.endDate.format(format); + that.$('.lcb-transcript-daterange-range').html(str); + + that.loadTranscript(); + } + + setRange(moment(), moment()); + + this.$daterange = this.$('.lcb-transcript-daterange') + .daterangepicker({ + format: format, + startDate: this.startDate, + endDate: this.endDate, + dateLimit: { + months: 1 + }, + ranges: ranges + }, setRange); + + this.$query.jvFloat(); + + }, + search: _.throttle(function() { + this.query = this.$query.val() + this.loadTranscript(); + }, 400, {leading: false}), + loadTranscript: function() { + var that = this; + this.clearMessages(); + $.get('./messages', { + room: this.room.id, + from: moment(this.startDate).utc().toISOString(), + to: moment(this.endDate).utc().toISOString(), + query: this.query, + expand: 'owner', + reverse: false, + take: 5000 + }, function(messages) { + _.each(messages, function(message) { + that.addMessage(message); + }); + }); + }, + clearMessages: function() { + this.$messages.html(''); + delete this.lastMessageOwner; + delete this.lastMessagePosted; + }, + addMessage: function(message) { + // Smells like pasta + message.paste = /\n/i.test(message.text); + + var posted = moment(message.posted); + + // Fragment or new message? + message.fragment = this.lastMessageOwner === message.owner.id && + posted.diff(this.lastMessagePosted, 'minutes') < 2; + + // Templatin' time + var $html = $(this.messageTemplate(message).trim()); + var $text = $html.find('.lcb-message-text'); + + $text.html(this.formatMessage($text.html())); + + this.formatTimestamp($html.find('time')); + this.$messages.append($html); + + if (!message.fragment) { + this.lastMessageOwner = message.owner.id; + this.lastMessagePosted = posted; + } + }, + formatMessage: function(text) { + return window.utils.message.format(text, this.formatData); + }, + formatTimestamp: function($el) { + var time = moment($el.attr('title')).format('ddd, MMM Do YYYY, h:mm:ss a'); + $el.text(time); + } + }); + +}(window, $, _); diff --git a/media/js/views/upload.js b/media/js/views/upload.js new file mode 100644 index 0000000..2b9a54c --- /dev/null +++ b/media/js/views/upload.js @@ -0,0 +1,110 @@ +/* + * UPLOAD/FILE VIEWS + * The king of all views. + */ + +'use strict'; + +Dropzone && (Dropzone.autoDiscover = false); + ++function(window, $, _) { + + window.LCB = window.LCB || {}; + + window.LCB.UploadView = Backbone.View.extend({ + events: { + 'submit form': 'submit' + }, + initialize: function(options) { + this.template = $('#template-upload').html(); + this.rooms = options.rooms; + this.rooms.current.on('change:id', this.setRoom, this); + this.rooms.on('add remove', this.populateRooms, this); + this.rooms.on('change:joined', this.populateRooms, this); + this.rooms.on('upload:show', this.show, this); + this.render(); + }, + render: function() { + // + // Dropzone + // + var $ele = this.$el.closest('.lcb-client').get(0); + this.dropzone = new Dropzone($ele, { + url: './files', + autoProcessQueue: false, + clickable: [this.$('.lcb-upload-target').get(0)], + previewsContainer: this.$('.lcb-upload-preview-files').get(0), + addRemoveLinks: true, + dictRemoveFile: 'Remove', + parallelUploads: 8, + maxFiles: 8, + previewTemplate: this.template + }); + this.dropzone + .on('sending', _.bind(this.sending, this)) + .on('sendingmultiple', _.bind(this.sending, this)) + .on('addedfile', _.bind(this.show, this)) + .on('queuecomplete', _.bind(this.complete, this)); + // + // Selectize + // + this.selectize = this.$('select[name="room"]').selectize({ + valueField: 'id', + labelField: 'name', + searchField: 'name' + }).get(0).selectize; + // + // Modal events + // + this.$el.on('hidden.bs.modal', _.bind(this.clear, this)); + this.$el.on('shown.bs.modal', _.bind(this.setRoom, this)); + }, + show: function() { + this.$el.modal('show'); + }, + hide: function() { + this.$el.modal('hide'); + }, + clear: function() { + this.dropzone.removeAllFiles(); + }, + complete: function(e) { + var remaining = _.some(this.dropzone.files, function(file) { + return file.status !== 'success'; + }); + if (remaining) { + swal('Woops!', 'There were some issues uploading your files.', 'warning'); + return; + } + this.hide(); + swal('Success', 'Files uploaded!', 'success'); + }, + sending: function(file, xhr, formData) { + formData.append('room', this.$('select[name="room"]').val()); + formData.append('post', this.$('input[name="post"]').is(':checked')); + }, + submit: function(e) { + e.preventDefault(); + if (!this.$('select[name="room"]').val()) { + swal('Woops!', 'Please specify a room.', 'warning'); + return; + } + this.dropzone.processQueue(); + }, + setRoom: function() { + this.selectize.setValue(this.rooms.current.id); + }, + populateRooms: function() { + this.selectize.clearOptions(); + this.rooms.each(function(room) { + if (room.get('joined')) { + this.selectize.addOption({ + id: room.id, + name: room.get('name') + }); + } + }, this); + } + }); + +}(window, $, _); diff --git a/media/js/views/window.js b/media/js/views/window.js new file mode 100644 index 0000000..f7cda80 --- /dev/null +++ b/media/js/views/window.js @@ -0,0 +1,241 @@ +/* + * WINDOW VIEW + * TODO: Break it up :/ + */ + +'use strict'; + ++function(window, $, _, notify) { + + window.LCB = window.LCB || {}; + + window.LCB.WindowView = Backbone.View.extend({ + el: 'html', + focus: true, + count: 0, + mentions: 0, + countFavicon: new Favico({ + position: 'down', + animation: 'none', + bgColor: '#b94a48' + }), + mentionsFavicon: new Favico({ + position: 'left', + animation: 'none', + bgColor: '#f22472' + }), + initialize: function(options) { + + var that = this; + + this.client = options.client; + this.rooms = options.rooms; + this.originalTitle = document.title; + this.title = this.originalTitle; + + $(window).on('focus blur', _.bind(this.onFocusBlur, this)); + + this.rooms.current.on('change:id', function(current, id) { + var room = this.rooms.get(id), + title = room ? room.get('name') : 'Rooms'; + this.updateTitle(title); + }, this); + + this.rooms.on('change:name', function(room) { + if (room.id !== this.rooms.current.get('id')) { + return; + } + this.updateTitle(room.get('name')); + }, this); + + this.rooms.on('messages:new', this.onNewMessage, this); + + // Last man standing + _.defer(function() { + that.updateTitle(); + }); + + }, + onFocusBlur: function(e) { + this.focus = (e.type === 'focus'); + if (this.focus) { + clearInterval(this.titleTimer); + clearInterval(this.faviconBadgeTimer); + this.count = 0; + this.mentions = 0; + this.titleTimer = false; + this.titleTimerFlip = false; + this.faviconBadgeTimer = false; + this.faviconBadgeTimerFlip = false; + this.updateTitle(); + this.mentionsFavicon.reset(); + } + }, + onNewMessage: function(message) { + if (this.focus || message.historical || message.owner.id === this.client.user.id) { + return; + } + this.countMessage(message); + this.flashTitle() + this.flashFaviconBadge(); + }, + countMessage: function(message) { + ++this.count; + message.mentioned && ++this.mentions; + }, + flashTitle: function() { + var titlePrefix = ''; + if (this.count > 0) { + titlePrefix += '(' + parseInt(this.count); + if (this.mentions > 0) { + titlePrefix += '/' + parseInt(this.mentions) + '@'; + } + titlePrefix += ') '; + } + document.title = titlePrefix + this.title; + }, + flashFaviconBadge: function() { + if (!this.faviconBadgeTimer) { + this._flashFaviconBadge(); + var flashFaviconBadge = _.bind(this._flashFaviconBadge, this); + this.faviconBadgeTimer = setInterval(flashFaviconBadge, 1 * 2000); + } + }, + _flashFaviconBadge: function() { + if (this.mentions > 0 && this.faviconBadgeTimerFlip) { + this.mentionsFavicon.badge(this.mentions); + } else { + this.countFavicon.badge(this.count); + } + this.faviconBadgeTimerFlip = !this.faviconBadgeTimerFlip; + }, + updateTitle: function(name) { + if (!name) { + var room = this.rooms.get(this.rooms.current.get('id')); + name = (room && room.get('name')) || 'Rooms'; + } + if (name) { + this.title = name + ' \u00B7 ' + this.originalTitle; + } else { + this.title = this.originalTitle; + } + document.title = this.title; + } + }); + + window.LCB.HotKeysView = Backbone.View.extend({ + el: 'html', + keys: { + 'up+shift+alt down+shift+alt': 'nextRoom', + 's+shift+alt': 'toggleRoomSidebar', + 'g+shift+alt': 'openGiphyModal', + 'space+shift+alt': 'recallRoom' + }, + initialize: function(options) { + this.client = options.client; + this.rooms = options.rooms; + }, + nextRoom: function(e) { + var method = e.keyCode === 40 ? 'next' : 'prev', + selector = e.keyCode === 40 ? 'first' : 'last', + $next = this.$('.lcb-tabs').find('[data-id].selected')[method](); + if ($next.length === 0) { + $next = this.$('.lcb-tabs').find('[data-id]:' + selector); + } + this.client.events.trigger('rooms:switch', $next.data('id')); + }, + recallRoom: function() { + this.client.events.trigger('rooms:switch', this.rooms.last.get('id')); + }, + toggleRoomSidebar: function(e) { + e.preventDefault(); + var view = this.client.view.panes.views[this.rooms.current.get('id')]; + view && view.toggleSidebar && view.toggleSidebar(); + }, + openGiphyModal: function(e) { + if (this.client.options.giphyEnabled) { + e.preventDefault(); + $('.lcb-giphy').modal('show'); + } + } + }); + + window.LCB.DesktopNotificationsView = Backbone.View.extend({ + focus: true, + openNotifications: [], + openMentions: [], + initialize: function(options) { + notify.config({ + pageVisibility: false + }); + this.client = options.client; + this.rooms = options.rooms; + $(window).on('focus blur unload', _.bind(this.onFocusBlur, this)); + this.rooms.on('messages:new', this.onNewMessage, this); + }, + onFocusBlur: function(e) { + this.focus = (e.type === 'focus'); + _.each(_.merge(this.openNotifications, this.openMentions), function(notification) { + notification.close && notification.close(); + }); + }, + onNewMessage: function(message) { + if (this.focus || message.historical || message.owner.id === this.client.user.id) { + return; + } + this.createDesktopNotification(message); + }, + createDesktopNotification: function(message) { + + var that = this; + + if (!notify.isSupported || + notify.permissionLevel() != notify.PERMISSION_GRANTED) { + return; + } + + var roomID = message.room.id, + avatar = message.owner.avatar, + icon = 'https://www.gravatar.com/avatar/' + avatar + '?s=50', + title = message.owner.displayName + ' in ' + message.room.name, + mention = message.mentioned; + + var notification = notify.createNotification(title, { + body: message.text, + icon: icon, + tag: message.id, + onclick: function() { + window.focus(); + that.client.events.trigger('rooms:switch', roomID); + } + }); + + // + // Mentions + // + if (mention) { + if (this.openMentions.length > 2) { + this.openMentions[0].close(); + this.openMentions.shift(); + } + this.openMentions.push(notification); + // Quit early! + return; + } + // + // Everything else + // + if (this.openNotifications.length > 2) { + this.openNotifications[0].close(); + this.openNotifications.shift(); + } + this.openNotifications.push(notification); + + setTimeout(function() { + notification.close && notification.close(); + }, 3000); + + } + }); + +}(window, $, _, notify); diff --git a/media/less/style.less b/media/less/style.less new file mode 100644 index 0000000..162516d --- /dev/null +++ b/media/less/style.less @@ -0,0 +1,22 @@ +/********************* + * LCB Styles + *********************/ + +@import "vendor/bootstrap/variables.less"; +@import "vendor/bootstrap/mixins.less"; +@import 'vendor/animate/animate.less'; +@import 'vendor/flexbox.less'; +@import 'vendor/hat.less'; + +@import 'style/base.less'; +@import 'style/login.less'; +@import 'style/transcript.less'; + +@import 'style/chat/client.less'; +@import 'style/chat/loading.less'; +@import 'style/chat/tabs.less'; +@import 'style/chat/browser.less'; +@import 'style/chat/rooms.less'; +@import 'style/chat/messages.less'; +@import 'style/chat/uploads.less'; + diff --git a/media/less/style/base.less b/media/less/style/base.less new file mode 100644 index 0000000..6c88df7 --- /dev/null +++ b/media/less/style/base.less @@ -0,0 +1,50 @@ +/********************* + * Let's Chat + *********************/ + +html, +body { + height: 100%; +} + +[contenteditable]:empty::before { + content: attr(placeholder); + color: inherit; + .opacity(.4); +} + +.modal { + overflow-y: scroll; +} + +.modal-content { + border: none; +} + +.form-control { + height: auto; + font-size: 14px; + padding: 10px; +} + +.form-group { + margin-bottom: 20px; +} + +.jvFloat { + display: block; + margin: 0; + .placeHolder { + background-color: #eee; + height: 16px; + line-height: 16px; + color: #555; + font-weight: 300; + font-size: 11px; + padding: 0 8px; + top: -5px; + right: 0; + left: auto; + .text-overflow; + } +} diff --git a/media/less/style/chat/browser.less b/media/less/style/chat/browser.less new file mode 100644 index 0000000..f1e654d --- /dev/null +++ b/media/less/style/chat/browser.less @@ -0,0 +1,147 @@ +/********************* + * Rooms Browser + *********************/ + +@roomBrowserHeader: 50px; + +.lcb-rooms-browser { + background: #fff; + .flex(1); + .flex-display; + .flex-direction(column); + .lcb-room-meta { + .flex(initial); + } +} + +.lcb-rooms-list, +.lcb-rooms-list-item { + display: block; + padding: 0; + margin: 0; +} + +.lcb-rooms-list { + overflow-y: auto; + -webkit-overflow-scrolling: touch; + .transform(translate3d(0,0,0)); +} + +.lcb-rooms-list-item { + color: #777; + position: relative; + padding: 10px 15px; + margin-bottom: 5px; + border-bottom: 1px #f2f2f2 solid; + overflow: hidden; + &:hover { + background: #f2f2f2; + } +} + +.lcb-rooms-list-item-top { + padding-right: 60px; +} + +.lcb-rooms-list-item-name, +.lcb-rooms-list-item-slug { + max-width: 49%; + vertical-align: middle; + display: inline-block; + .text-overflow; + @media (max-width: @screen-xs-max) { + margin-bottom: 0; + max-width: 100%; + } +} + +.lcb-rooms-list-item-name { + font-size: 20px; +} + +.lcb-rooms-list-item-slug, .lcb-rooms-list-item-password { + font-size: 16px; + font-weight: 300; + color: #aaa; + @media (max-width: @screen-xs-max) { + margin-bottom: 10px; + display: block; + } +} + +.lcb-rooms-list-item-description { + .text-overflow; +} + +.lcb-rooms-list-item-last-active { + font-size: 12px; + font-weight: 300; + position: relative; + float: right; +} + +.lcb-rooms-list-users, +.lcb-rooms-list-user { + padding: 0; + margin: 0; +} + +.lcb-rooms-list-users { + margin-top: 5px; + margin-left: -2px; + overflow: hidden; +} + +.lcb-rooms-list-user { + margin: 2px; + display: block; + float: left; +} + +.lcb-rooms-list-user-avatar { + .size(30px); + border-radius: 100%; +} + +.lcb-rooms-list-item-switch { + font-size: 9px; + position: absolute; + right: 24px; + top: 12px; + @media (min-width: @screen-sm-max) { + font-size: 8px; + } +} + +.lcb-rooms-browser-filter { + font-weight: 300; + font-size: 22px; + color: #aaa; + .flex-display; + .flex(1); + @media (max-width: @screen-xs-max) { + font-size: 18px; + } +} + +.lcb-rooms-browser-filter-label { + margin-right: 5px; +} + +.lcb-rooms-browser-filter-input { + background: none; + border: none; + .flex(1); + &:focus { + outline: none; + } +} + +// Eugh Firefox +@-moz-document url-prefix() { + .lcb-rooms-list-item { + width: 100%; + display: table; + table-layout: fixed; + } +} diff --git a/media/less/style/chat/client.less b/media/less/style/chat/client.less new file mode 100644 index 0000000..abf9336 --- /dev/null +++ b/media/less/style/chat/client.less @@ -0,0 +1,213 @@ +/********************* + * Client Base + *********************/ + +.lcb-client { + background: #222; + .size(100%); + position: fixed; + top: 0; + left: 0; + .flex-display; + .flex-direction(row); + -webkit-transition: -webkit-transform .1s ease; + -moz-transition: -moz-transform .1s ease; + -ms-transition: -ms-transform .1s ease; + transition: transform .1s ease; + @media (max-width: @screen-xs-max) { + padding-top: 43px; + &.lcb-sidebar-opened { + .translateX(200px); + .modal { + .translateX(-200px); + } + } + } +} + +.lcb-header { + background: #222; + width: 100%; + height: 43px; + text-align: center; + line-height: 44px; + position: fixed; + top: 0; + left: 0; + border-bottom: 1px #111 solid; + z-index: 1; + display: none; + &:after { + background-color: #333; + width: 100%; + height: 1px; + content: ''; + position: absolute; + left: 0; + bottom: -2px; + } + @media (max-width: @screen-xs-max) { + display: block; + } +} + +.lcb-header-toggle { + width: 40px; + height: 30px; + line-height: 100%; + position: absolute; + top: 7px; + left: 7px; + padding: 0; + background-color: #c0c0c0; +} + +.lcb-header-logo { + color: #fff; + font-family: Pacifico; + font-size: 20px; +} + +.lcb-sidebar { + background: #272727; + width: 200px; + min-width: 200px; + height: 100%; + border-right: 1px #333 solid; + position: relative; + z-index: 1; + .flex-display; + .flex-direction(column); + @media (max-width: @screen-xs-max) { + height: 100%; + position: absolute; + top: 0; + left: -200px; + } +} + +.lcb-panes { + .flex(1); + .flex-display; +} + +.lcb-account-button { + background: #222; + height: 45px; + line-height: 48px; + color: #bbb; + padding: 0 6px; + display: block; + position: relative; + .user-select(none); + .transition(color .2s linear); + &:hover, + &:focus { + color: #fff; + cursor: pointer; + text-decoration: none; + } + &:active { + background: #222; + } +} + +.lcb-account-button-avatar { + .size(32px); + position: absolute; + top: 6px; + left: 5px; +} + +.lcb-account-button-name { + display: block; + margin-left: 40px; + margin-right: 15px; + .text-overflow; + line-height: 18px; + padding-top: 6px; + font-size: 18px; +} + +.lcb-account-button-username { + display: block; + margin-left: 40px; + margin-right: 15px; + .text-overflow; + line-height: 15px; + font-size: 12px; +} + +.lcb-account-button-chevron { + height: 100%; + line-height: inherit; + color: rgba(255, 255, 255, .8); + font-size: 80%; + position: absolute; + top: 0; + right: 10px; + .transition(opacity .2s linear); + .opacity(.4); + .lcb-account-button:hover & { + .opacity(1); + } +} + +.lcb-status-text { + color: rgba(255, 255, 255, .7); + position: absolute; + padding: 6px; + font-size: 10px; + .flex-display; + .justify-content(center); + .align-items(center); +} + +.lcb-status-indicators { + text-transform: uppercase; + bottom: 0; + left: 0; + .lcb-status-text; +} + +.lcb-status-indicator { + margin: 0 3px; +} + +.lcb-status-indicator-error { + color: #ff7878; +} + +.lcb-version { + bottom: 1px; + right: 4px; + .opacity(0.4); + .lcb-status-text; +} + +.lcb-avatar { + border-radius: 100%; +} + +.lcb-giphy .giphy-results { + max-height: 350px; + overflow: auto; + + ul { + margin: 0; + padding: 0; + width: 100%; + text-align: center; + + li { + display: inline-block; + *display: inline; + *zoom: 1; + margin:5px; + + img { + cursor: pointer; + } + } + } +} diff --git a/media/less/style/chat/loading.less b/media/less/style/chat/loading.less new file mode 100644 index 0000000..b3a23ce --- /dev/null +++ b/media/less/style/chat/loading.less @@ -0,0 +1,20 @@ +/********************* + * Client Loading + *********************/ + +.lcb-loading { + background: #222 url('../img/dark-noise.png'); + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 2; + .flex-display; + .justify-content(center); + .align-items(center); +} + +.lcb-loading-indicator { + .square(64px); +} \ No newline at end of file diff --git a/media/less/style/chat/messages.less b/media/less/style/chat/messages.less new file mode 100644 index 0000000..659dc57 --- /dev/null +++ b/media/less/style/chat/messages.less @@ -0,0 +1,121 @@ +/********************* + * Messages + *********************/ + + @messagePadding: 7px; + +.lcb-messages { + background: #fff; + padding: 0; + margin: 0; + overflow: auto; + -webkit-overflow-scrolling: touch; + .flex(1 0 0); + .transform(translate3d(0,0,0)); +} + +.lcb-message { + min-height: 47px; + border-top: 1px #eee solid; + padding: @messagePadding; + font-size: 13px; + margin: 0; + display: block; + position: relative; + &:first-child { + border-top: none; + } + &.lcb-message-fragment { + min-height: 0; + margin-top: -@messagePadding / 2; + border-top: none; + } + &.lcb-message-own { + background: #fcffd8; + } + &.lcb-message-mentioned { + background: #ffe9ee; + } + &.lcb-message-mentioned:before { + background: #f22472; + width: 3px; + height: 100%; + content: ''; + position: absolute; + top: 0; + left: 0; + } + pre { + background: none; + border-width: 3px; + border-top: none; + border-bottom: none; + margin-bottom: 3px; + } + .thumbnail { + max-width: 100%; + overflow: hidden; + display: inline-block; + margin: 0; + img { + height: 200px; + width: auto; + object-fit: cover; + } + } +} + +.lcb-message-avatar { + position: absolute; +} + +.lcb-message-meta { + margin-left: 40px; +} + +.lcb-message-name { + color: #666; + font-weight: 700; + margin-bottom: @messagePadding - 2px; + display: block; + .lcb-message-username { + color: #aaa; + font-weight: 400; + } +} + +.lcb-message-time { + color: #bbb; + position: absolute; + top: @messagePadding; + right: @messagePadding; +} + +.lcb-message-text { + word-wrap: break-word; + pre { + padding: 10px; + margin: 0; + border: 3px solid #cccccc; + border-top: none; + border-bottom: none; + border-radius: 4px; + } +} + +.lcb-message-mention { + font-weight: bold; +} + +// Eugh Firefox +@-moz-document url-prefix() { + .lcb-message-text { + width: 100%; + display: table; + table-layout: fixed; + word-break: break-all; + pre { + white-space: pre-wrap; + } + } +} diff --git a/media/less/style/chat/rooms.less b/media/less/style/chat/rooms.less new file mode 100644 index 0000000..248e391 --- /dev/null +++ b/media/less/style/chat/rooms.less @@ -0,0 +1,324 @@ +/********************* + * Rooms + *********************/ + +.lcb-room, +.lcb-room-chat, +.lcb-room-main { + .flex(1); + .flex-display; +} + +.lcb-room { + .flex-direction(column); + -webkit-transition: -webkit-transform .1s ease; + -moz-transition: -moz-transform .1s ease; + -ms-transition: -ms-transform .1s ease; + transition: transform .1s ease; + @media (max-width: @screen-sm-max) { + &.lcb-room-sidebar-opened { + .translateX(-200px); + } + } +} + +.lcb-room-chat { + .flex-direction(column); +} + +.lcb-room-main { + .flex-direction(row); +} + +.lcb-room-header { + background: #f3f3f3; + height: 60px; + min-height: 60px; + line-height: 60px; + padding: 0 15px; + border-bottom: 1px #ddd solid; + position: relative; + box-shadow: 0 4px 5px -5px rgba(0, 0, 0, .1); + z-index: 1; + .flex-display; + .flex-direction(row); +} + +.lcb-room-meta { + padding-right: 15px; + .text-overflow; + .flex(1); +} + +.lcb-room-heading { + font-size: 24px; + font-weight: 300; + color: #555; + line-height: inherit; + margin: 0; + margin-right: 5px; + display: inline; + @media (max-width: @screen-xs-max) { + font-size: 20px; + } + .password { + font-size: 14px; + cursor: default; + } +} + +.lcb-room-heading-loud { + text-transform: uppercase; +} + +.lcb-room-description { + font-weight: 300; + color: #9f9f9f; + margin: 0; + display: inline; + @media (max-width: @screen-xs-max) { + font-size: 12px; + } +} + +.lcb-room-header-actions { + .btn-action { + background: #eee; + color: #777; + border-color: #ddd; + &:hover { + background: #f7f7f7; + } + } +} + +.lcb-room-participants { + .text-overflow; + color: #777; + @media (max-width: @screen-xs-max) { + font-size: 12px; + } +} + +.lcb-entry { + background: #fff; + height: 70px; + min-height: 70px; + border-top: 1px #eee solid; + .flex-display; + @media (max-width: @screen-xs-max) { + height: 42px; + } +} + +.lcb-entry-input { + max-height: 100%; + padding: 15px; + border: none; + resize: none; + .flex(1); + &:focus { + outline: none; + } + @media (max-width: @screen-xs-max) { + padding: 10px; + } +} + +.lcb-entry-action { + background: none; + font-size: 18px; + color: #999; + &:active, + &:focus { + outline: none !important; + box-shadow: none !important; + } + &:active { + color: #999 !important; + } +} + +.lcb-entry-button { + background: none; + color: #777; + border: none; + outline: none; + padding: 0 25px; + border-radius: 0; + &:hover { + color: #999; + } + &:active { + color: #555; + } +} + +.lcb-room-toggle-sidebar { + .lcb-room-sidebar-opened & { + background: #fefefe; + } + .fa { + .transition(all .2s ease-in); + .lcb-room-sidebar-opened & { + .rotateY(180deg); + } + } +} + +.lcb-room-sidebar { + background: #f7f7f7; + width: 20%; + min-width: 160px; + max-width: 240px; + border-left: 1px #ddd solid; + .flex-display; + .flex-direction(column); + @media (max-width: @screen-sm-max) { + width: 200px; + height: 100%; + padding: 0; + padding-left: 3px; + border: none; + position: absolute; + top: 0; + right: -200px; + } + @media (min-width: @screen-sm-max) { + display: none; + .lcb-room-sidebar-opened & { + .flex-display; + } + } +} + +.lcb-room-sidebar-group { + min-height: 36px; + overflow: hidden; + border-bottom: 1px #ddd solid; + .flex-display; + .flex-direction(column); + .flex(1 0 0); +} + +.lcb-room-sidebar-header { + min-height: inherit; + line-height: 36px; + padding: 0 10px; + border-bottom: 1px #ddd solid; + position: relative; +} + +.lcb-room-sidebar-heading { + height: 36px; + line-height: 36px; + font-size: 13px; + font-weight: 400; + margin: 0; + .fa { + margin-right: 4px; + } +} + +.lcb-room-sidebar-list, +.lcb-room-sidebar-item { + padding: 0; + margin: 0; +} + +.lcb-room-sidebar-list { + overflow: hidden; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + .transform(translate3d(0,0,0)); +} + +.lcb-room-sidebar-item { + background: #fff; + font-size: 12px; + padding: 5px; + margin: 0; + border-bottom: 1px #eee solid; + display: block; + position: relative; + .text-overflow; + &:last-child { + border-bottom: none; + } +} + +.lcb-room-sidebar-user { + min-height: 40px; +} + +.lcb-room-sidebar-user-name { + font-weight: 400; + font-size: 14px; + line-height: 17px; + margin-left: 36px; + display: block; + .text-overflow; +} + +.lcb-room-sidebar-user-username { + color: #888; + font-size: 12px; + line-height: 13px; + margin-left: 36px; + display: block; + .text-overflow; +} + +.lcb-room-sidebar-user-avatar { + .square(30px); + position: absolute; + top: 5px; + left: 5px; +} + +.lcb-room-poke { + &:hover { + cursor: pointer; + } +} + +.lcb-room-sidebar-files-add { + .size(24px); + font-size: 11px; + padding: 0; + position: absolute; + top: 6px; + right: 6px; + &:focus, + &:active { + outline: none !important; + } +} + +.lcb-room-sidebar-file-icon { + position: absolute; + top: 8px; + left: 7px; +} + +.lcb-room-sidebar-file-meta { + font-size: 11px; + color: #777; + margin-left: 20px; +} + +.lcb-room-sidebar-file-name { + font-size: 13px; + display: block; + .text-overflow; +} + +@-moz-document url-prefix() { + .lcb-room-meta-text { + width: 100%; + display: table; + table-layout: fixed; + .text-overflow; + } +} + diff --git a/media/less/style/chat/tabs.less b/media/less/style/chat/tabs.less new file mode 100644 index 0000000..85f2f7e --- /dev/null +++ b/media/less/style/chat/tabs.less @@ -0,0 +1,123 @@ +/********************* + * Tabs + *********************/ + +.lcb-tabs, +.lab-tab { + padding: 0; + margin: 0; +} + +.lcb-tabs-outer { + width: 100%; + margin-bottom: 24px; + padding-right: 1px; + overflow-y: auto; + .box-sizing(content-box); + .flex(1); +} + +.lcb-tabs { + margin: 20px 0; +} + +.lcb-tab { + height: 36px; + line-height: 36px; + font-size: 12px; + display: block; + position: relative; + z-index: 1; + .box-sizing(content-box); + .opacity(.7); + &:after { + background-color: transparent; + width: 1px; + height: 100%; + content: ''; + position: absolute; + top: 0; + left: 0; + .transition(background .02s linear); + } + a { + height: inherit; + color: #999; + text-decoration: none; + padding-left: 10px; + border-bottom: none; + display: block; + position: relative; + .transition(color .07s linear); + } + &:hover { + .opacity(.8); + a { + color: #bbb; + } + } + &.selected { + background: #323232; + &:after { + background: rgba(255, 255, 255, .9); + } + a { + color: #fff; + } + .opacity(1); + } +} + +.lcb-tab-title { + padding-right: 22px; + display: block; + .text-overflow; +} + +.lcb-tab-alerts { + background-color: #b94a48; + color: #fff; + font-weight: bold; + font-size: 11px; + line-height: 100%; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + position: absolute; + top: 10px; + right: 24px; + border-radius: 3px; +} + +.lcb-tab-alerts-mentions, +.lcb-tab-alerts-total { + padding: 2px 5px; + float: left; + border-radius: 3px; + &:empty { + display: none; + } +} + +.lcb-tab-alerts-mentions { + background: #f22472; +} + +.lcb-tab-alerts-total { + background: #b94a48; +} + +.lcb-tab-close { + line-height: 32px; + font-size: 20px; + color: #ccc; + position: absolute; + top: 0; + right: 8px; + .transition(opacity .07s linear); + .opacity(0); + &:hover { + .opacity(1); + } + .lcb-tab:hover & { + .opacity(.4); + } +} diff --git a/media/less/style/chat/uploads.less b/media/less/style/chat/uploads.less new file mode 100644 index 0000000..c7e42fd --- /dev/null +++ b/media/less/style/chat/uploads.less @@ -0,0 +1,60 @@ +/********************* + * Uploads + *********************/ + +.lcb-upload-preview-files { + overflow: hidden; +} + +.lcb-upload-item { + width: 25%; + margin-bottom: 5px; + text-align: center; + float: left; + .dz-remove { + font-size: 11px; + } +} + +.lcb-upload-item-name { + display: block; +} + +.lcb-upload-item-box { + background: #fff; + border: 1px #eee solid; + margin: 3px; + border-radius: 3px; + overflow: hidden; +} + +.lcb-upload-item-preview { + background: #333; + min-height: 28px; + position: relative; + &, img { + width: 100%; + } +} + +.lcb-upload-item-size { + width: 100%; + color: #fff; + font-size: 12px; + text-shadow: 0 0 5px rgba(0,0,0, 0.7); + position: absolute; + bottom: 4px; + left: 0; + strong { + font-weight: 300; + } +} + +.lcb-upload-item-progress { + height: 3px; + margin-bottom: 0; +} + +.lcb-upload-item-progress-bar { + .progress-bar-variant(#f22472); +} \ No newline at end of file diff --git a/media/less/style/login.less b/media/less/style/login.less new file mode 100644 index 0000000..6e21e60 --- /dev/null +++ b/media/less/style/login.less @@ -0,0 +1,157 @@ +/********************* + * Login + *********************/ + +.lcb-login { + background-color: #222; + background-image: url('../img/photos/reflection.jpg'); + background-size: cover; + background-position: center center; + background-attachment: fixed; + padding-top: 40px; + .flex-display; + .align-items(center); + .justify-content(center); + &:before { + background-image: url('../img/photo-overlay.png'); + .size(100%); + content: ''; + position: fixed; + top: 0; + left: 0; + .opacity(.4); + } + + @media (max-width: @screen-xs-max) { + display: block; + } +} + +.lcb-login-main { + max-width: 420px; + text-align: center; + padding: 20px; + padding-bottom: 60px; + position: relative; +} + +.lcb-login-logo { + font-family: Pacifico; + font-size: 85px; + text-shadow: 0 0 10px rgba(0,0,0,.2); + color: #fff; + margin: 0; + margin-bottom: 30px; + .flavour { + color: #f22472; + } + @media (max-width: @screen-xs-max) { + font-size: 56px; + } +} + +.lcb-login-box { + background-color: #fff; + padding: 20px; + border: 1px solid #e3e3e3; + position: relative; + z-index: 1; + border-radius: 3px; + .box-shadow(0 0 20px 3px rgba(0,0,0,.1)); + .response:empty { + display: none; + } + + @media (min-width: @screen-sm-min) { + .row { + .col-sm-9:first-child { + padding-right: 10px; + } + .col-sm-9:last-child { + padding-left: 10px; + } + } + } +} + +.lcb-login-box-heading { + line-height: 100%; + color: #555; + font-family: Pacifico; + font-size: 24px; + padding: 0; + padding-bottom: 20px; + margin: 0; + margin-bottom: 25px; + border-bottom: 1px solid #e5e5e5; +} + +.lcb-login-avatar { + .size(0); + margin: 0 auto; + margin-bottom: 25px; + border-radius: 100%; + border: 1px #eee solid; + &.show { + .size(100px); + .animated; + .fadeInUp; + } +} + +.lcb-login-box-bottom { + overflow: hidden; + .links { + margin-top: 12px; + margin-right: 12px; + } +} + +.lcb-login-footer-heart { + float: left; +} + +.lcb-login-footer-github { + float: right; +} + +.lcb-login-footer { + background-color: rgba(0,0,0,.4); + width: 100%; + color: #f2f2f2; + font-family: Tahoma; + font-size: 11px; + text-align: center; + position: fixed; + left: 0; + bottom: 0; + padding: 5px 0; + p { + margin: 0 10px; + } + a { + color: #fff; + } + .fa { + margin-right: 3px; + } + .fa-heart { + color: #f22472; + } + @media (max-width: @screen-xs-max) { + position: relative; + font-size: 14px; + margin-top: -5px; + border-radius: 0 0 3px 3px; + .lcb-login-footer-heart, + .lcb-login-footer-github { + margin: 10px 0; + float: none; + } + .dash { + line-height: 0; + visibility: hidden; + display: block; + } + } +} diff --git a/media/less/style/transcript.less b/media/less/style/transcript.less new file mode 100644 index 0000000..5042a94 --- /dev/null +++ b/media/less/style/transcript.less @@ -0,0 +1,50 @@ +/********************* + * Transcript + *********************/ + +.lcb-transcript { + position: fixed; + top: 0; + right: 0; + left: 0; + bottom: 0; + .flex-display; + .flex-direction(column); + @media print { + & { + position: static; + display: block; + } + } +} + +.lcb-transcript-header { + background: #ddd; + padding: 12px; +} + +.lcb-transcript-heading { + font-size: 24px; + font-weight: 400; + margin: 0; + margin-bottom: 12px; +} + +.lcb-search-entry, +.lcb-transcript-daterange { + width: auto; + padding: 8px 12px; + margin-right: 10px; + float: left; + border: none; + box-shadow: 0 1px 3px rgba(0,0,0,.25), inset 0 -1px 0 rgba(0,0,0,.1); + border-radius: 0; +} + +.lcb-transcript-daterange { + background: #fff; + color: #333; + cursor: pointer; + float: left; + box-shadow: 0 1px 3px rgba(0,0,0,.25), inset 0 -1px 0 rgba(0,0,0,.1); +} \ No newline at end of file diff --git a/media/less/vendor.less b/media/less/vendor.less new file mode 100644 index 0000000..9395ad6 --- /dev/null +++ b/media/less/vendor.less @@ -0,0 +1,20 @@ +/********************* +* LCB Styles +*********************/ + +@font-face { + font-family: 'Pacifico'; + font-style: normal; + font-weight: 400; + src: local('Pacifico Regular'), local('Pacifico-Regular'), url('../font/pacifico.woff') format('woff'); +} + +@import 'vendor/bootstrap/bootstrap.less'; +@import 'vendor/font-awesome/font-awesome.less'; +@import 'vendor/onoffswitch.less'; + +@import (inline) '../js/vendor/JVFloat/jvfloat.css'; +@import (inline) '../js/vendor/sweetalert/sweet-alert.css'; +@import (inline) '../js/vendor/at/jquery.atwho.css'; +@import (inline) '../js/vendor/selectize/selectize.css'; +@import (inline) '../js/vendor/selectize/selectize.bootstrap3.css'; diff --git a/media/less/vendor/animate/animate.less b/media/less/vendor/animate/animate.less new file mode 100644 index 0000000..a2fea3a --- /dev/null +++ b/media/less/vendor/animate/animate.less @@ -0,0 +1,58 @@ +@import "mixins.less"; +@import "animationclasses.less"; +@import "bounce.less"; +@import "bounceIn.less"; +@import "bounceInDown.less"; +@import "bounceInLeft.less"; +@import "bounceInRight.less"; +@import "bounceInUp.less"; +@import "bounceOut.less"; +@import "bounceOutDown.less"; +@import "bounceOutLeft.less"; +@import "bounceOutRight.less"; +@import "bounceOutUp.less"; +@import "fadeIn.less"; +@import "fadeInDown.less"; +@import "fadeInDownBig.less"; +@import "fadeInLeft.less"; +@import "fadeInLeftBig.less"; +@import "fadeInRight.less"; +@import "fadeInRightBig.less"; +@import "fadeInUp.less"; +@import "fadeInUpBig.less"; +@import "fadeOut.less"; +@import "fadeOutDown.less"; +@import "fadeOutDownBig.less"; +@import "fadeOutLeft.less"; +@import "fadeOutLeftBig.less"; +@import "fadeOutRight.less"; +@import "fadeOutRightBig.less"; +@import "fadeOutUp.less"; +@import "fadeOutUpBig.less"; +@import "flash.less"; +@import "flip.less"; +@import "flipInX.less"; +@import "flipInY.less"; +@import "flipOutX.less"; +@import "flipOutY.less"; +@import "hinge.less"; +@import "lightSpeedIn.less"; +@import "lightSpeedOut.less"; +@import "mixins.less"; +@import "pulse.less"; +@import "rollIn.less"; +@import "rollOut.less"; +@import "rotateIn.less"; +@import "rotateInDownLeft.less"; +@import "rotateInDownRight.less"; +@import "rotateInUpLeft.less"; +@import "rotateInUpRight.less"; +@import "rotateOut.less"; +@import "rotateOutDownLeft.less"; +@import "rotateOutDownRight.less"; +@import "rotateOutUpLeft.less"; +@import "rotateOutUpRight.less"; +@import "shake.less"; +@import "swing.less"; +@import "tada.less"; +@import "wobble.less"; diff --git a/media/less/vendor/animate/animationclasses.less b/media/less/vendor/animate/animationclasses.less new file mode 100644 index 0000000..f073060 --- /dev/null +++ b/media/less/vendor/animate/animationclasses.less @@ -0,0 +1,24 @@ +.animated { + -webkit-animation-duration: 1s; + -moz-animation-duration: 1s; + -o-animation-duration: 1s; + animation-duration: 1s; + -webkit-animation-fill-mode: both; + -moz-animation-fill-mode: both; + -o-animation-fill-mode: both; + animation-fill-mode: both; + + &.infinite { + -webkit-animation-iteration-count: infinite; + -moz-animation-iteration-count: infinite; + -o-animation-iteration-count: infinite; + animation-iteration-count: infinite; + } + + &.hinge { + -webkit-animation-duration: 2s; + -moz-animation-duration: 2s; + -o-animation-duration: 2s; + animation-duration: 2s; + } +} diff --git a/media/less/vendor/animate/bounce.less b/media/less/vendor/animate/bounce.less new file mode 100644 index 0000000..f6e2e05 --- /dev/null +++ b/media/less/vendor/animate/bounce.less @@ -0,0 +1,26 @@ +@-webkit-keyframes bounce { + 0%, 20%, 50%, 80%, 100% {-webkit-transform: translateY(0);} + 40% {-webkit-transform: translateY(-30px);} + 60% {-webkit-transform: translateY(-15px);} +} + +@-moz-keyframes bounce { + 0%, 20%, 50%, 80%, 100% {-moz-transform: translateY(0);} + 40% {-moz-transform: translateY(-30px);} + 60% {-moz-transform: translateY(-15px);} +} + +@-o-keyframes bounce { + 0%, 20%, 50%, 80%, 100% {-o-transform: translateY(0);} + 40% {-o-transform: translateY(-30px);} + 60% {-o-transform: translateY(-15px);} +} +@keyframes bounce { + 0%, 20%, 50%, 80%, 100% {transform: translateY(0);} + 40% {transform: translateY(-30px);} + 60% {transform: translateY(-15px);} +} + +.bounce { + .animation-name(bounce); +} \ No newline at end of file diff --git a/media/less/vendor/animate/bounceIn.less b/media/less/vendor/animate/bounceIn.less new file mode 100644 index 0000000..f516364 --- /dev/null +++ b/media/less/vendor/animate/bounceIn.less @@ -0,0 +1,83 @@ +@-webkit-keyframes bounceIn { + 0% { + opacity: 0; + -webkit-transform: scale(.3); + } + + 50% { + opacity: 1; + -webkit-transform: scale(1.05); + } + + 70% { + -webkit-transform: scale(.9); + } + + 100% { + -webkit-transform: scale(1); + } +} + +@-moz-keyframes bounceIn { + 0% { + opacity: 0; + -moz-transform: scale(.3); + } + + 50% { + opacity: 1; + -moz-transform: scale(1.05); + } + + 70% { + -moz-transform: scale(.9); + } + + 100% { + -moz-transform: scale(1); + } +} + +@-o-keyframes bounceIn { + 0% { + opacity: 0; + -o-transform: scale(.3); + } + + 50% { + opacity: 1; + -o-transform: scale(1.05); + } + + 70% { + -o-transform: scale(.9); + } + + 100% { + -o-transform: scale(1); + } +} + +@keyframes bounceIn { + 0% { + opacity: 0; + transform: scale(.3); + } + + 50% { + opacity: 1; + transform: scale(1.05); + } + + 70% { + transform: scale(.9); + } + + 100% { + transform: scale(1); + } +} + +.bounceIn { + .animation-name(bounceIn); +} \ No newline at end of file diff --git a/media/less/vendor/animate/bounceInDown.less b/media/less/vendor/animate/bounceInDown.less new file mode 100644 index 0000000..62cd452 --- /dev/null +++ b/media/less/vendor/animate/bounceInDown.less @@ -0,0 +1,83 @@ +@-webkit-keyframes bounceInDown { + 0% { + opacity: 0; + -webkit-transform: translateY(-2000px); + } + + 60% { + opacity: 1; + -webkit-transform: translateY(30px); + } + + 80% { + -webkit-transform: translateY(-10px); + } + + 100% { + -webkit-transform: translateY(0); + } +} + +@-moz-keyframes bounceInDown { + 0% { + opacity: 0; + -moz-transform: translateY(-2000px); + } + + 60% { + opacity: 1; + -moz-transform: translateY(30px); + } + + 80% { + -moz-transform: translateY(-10px); + } + + 100% { + -moz-transform: translateY(0); + } +} + +@-o-keyframes bounceInDown { + 0% { + opacity: 0; + -o-transform: translateY(-2000px); + } + + 60% { + opacity: 1; + -o-transform: translateY(30px); + } + + 80% { + -o-transform: translateY(-10px); + } + + 100% { + -o-transform: translateY(0); + } +} + +@keyframes bounceInDown { + 0% { + opacity: 0; + transform: translateY(-2000px); + } + + 60% { + opacity: 1; + transform: translateY(30px); + } + + 80% { + transform: translateY(-10px); + } + + 100% { + transform: translateY(0); + } +} + +.bounceInDown { + .animation-name(bounceInDown); +} \ No newline at end of file diff --git a/media/less/vendor/animate/bounceInLeft.less b/media/less/vendor/animate/bounceInLeft.less new file mode 100644 index 0000000..9c268f9 --- /dev/null +++ b/media/less/vendor/animate/bounceInLeft.less @@ -0,0 +1,83 @@ +@-webkit-keyframes bounceInLeft { + 0% { + opacity: 0; + -webkit-transform: translateX(-2000px); + } + + 60% { + opacity: 1; + -webkit-transform: translateX(30px); + } + + 80% { + -webkit-transform: translateX(-10px); + } + + 100% { + -webkit-transform: translateX(0); + } +} + +@-moz-keyframes bounceInLeft { + 0% { + opacity: 0; + -moz-transform: translateX(-2000px); + } + + 60% { + opacity: 1; + -moz-transform: translateX(30px); + } + + 80% { + -moz-transform: translateX(-10px); + } + + 100% { + -moz-transform: translateX(0); + } +} + +@-o-keyframes bounceInLeft { + 0% { + opacity: 0; + -o-transform: translateX(-2000px); + } + + 60% { + opacity: 1; + -o-transform: translateX(30px); + } + + 80% { + -o-transform: translateX(-10px); + } + + 100% { + -o-transform: translateX(0); + } +} + +@keyframes bounceInLeft { + 0% { + opacity: 0; + transform: translateX(-2000px); + } + + 60% { + opacity: 1; + transform: translateX(30px); + } + + 80% { + transform: translateX(-10px); + } + + 100% { + transform: translateX(0); + } +} + +.bounceInLeft { + .animation-name(bounceInLeft); +} \ No newline at end of file diff --git a/media/less/vendor/animate/bounceInRight.less b/media/less/vendor/animate/bounceInRight.less new file mode 100644 index 0000000..daf0b36 --- /dev/null +++ b/media/less/vendor/animate/bounceInRight.less @@ -0,0 +1,83 @@ +@-webkit-keyframes bounceInRight { + 0% { + opacity: 0; + -webkit-transform: translateX(2000px); + } + + 60% { + opacity: 1; + -webkit-transform: translateX(-30px); + } + + 80% { + -webkit-transform: translateX(10px); + } + + 100% { + -webkit-transform: translateX(0); + } +} + +@-moz-keyframes bounceInRight { + 0% { + opacity: 0; + -moz-transform: translateX(2000px); + } + + 60% { + opacity: 1; + -moz-transform: translateX(-30px); + } + + 80% { + -moz-transform: translateX(10px); + } + + 100% { + -moz-transform: translateX(0); + } +} + +@-o-keyframes bounceInRight { + 0% { + opacity: 0; + -o-transform: translateX(2000px); + } + + 60% { + opacity: 1; + -o-transform: translateX(-30px); + } + + 80% { + -o-transform: translateX(10px); + } + + 100% { + -o-transform: translateX(0); + } +} + +@keyframes bounceInRight { + 0% { + opacity: 0; + transform: translateX(2000px); + } + + 60% { + opacity: 1; + transform: translateX(-30px); + } + + 80% { + transform: translateX(10px); + } + + 100% { + transform: translateX(0); + } +} + +.bounceInRight { + .animation-name(bounceInRight); +} \ No newline at end of file diff --git a/media/less/vendor/animate/bounceInUp.less b/media/less/vendor/animate/bounceInUp.less new file mode 100644 index 0000000..04b362f --- /dev/null +++ b/media/less/vendor/animate/bounceInUp.less @@ -0,0 +1,82 @@ +@-webkit-keyframes bounceInUp { + 0% { + opacity: 0; + -webkit-transform: translateY(2000px); + } + + 60% { + opacity: 1; + -webkit-transform: translateY(-30px); + } + + 80% { + -webkit-transform: translateY(10px); + } + + 100% { + -webkit-transform: translateY(0); + } +} +@-moz-keyframes bounceInUp { + 0% { + opacity: 0; + -moz-transform: translateY(2000px); + } + + 60% { + opacity: 1; + -moz-transform: translateY(-30px); + } + + 80% { + -moz-transform: translateY(10px); + } + + 100% { + -moz-transform: translateY(0); + } +} + +@-o-keyframes bounceInUp { + 0% { + opacity: 0; + -o-transform: translateY(2000px); + } + + 60% { + opacity: 1; + -o-transform: translateY(-30px); + } + + 80% { + -o-transform: translateY(10px); + } + + 100% { + -o-transform: translateY(0); + } +} + +@keyframes bounceInUp { + 0% { + opacity: 0; + transform: translateY(2000px); + } + + 60% { + opacity: 1; + transform: translateY(-30px); + } + + 80% { + transform: translateY(10px); + } + + 100% { + transform: translateY(0); + } +} + +.bounceInUp { + .animation-name(bounceInUp); +} \ No newline at end of file diff --git a/media/less/vendor/animate/bounceOut.less b/media/less/vendor/animate/bounceOut.less new file mode 100644 index 0000000..4d70b21 --- /dev/null +++ b/media/less/vendor/animate/bounceOut.less @@ -0,0 +1,83 @@ +@-webkit-keyframes bounceOut { + 0% { + -webkit-transform: scale(1); + } + + 25% { + -webkit-transform: scale(.95); + } + + 50% { + opacity: 1; + -webkit-transform: scale(1.1); + } + + 100% { + opacity: 0; + -webkit-transform: scale(.3); + } +} + +@-moz-keyframes bounceOut { + 0% { + -moz-transform: scale(1); + } + + 25% { + -moz-transform: scale(.95); + } + + 50% { + opacity: 1; + -moz-transform: scale(1.1); + } + + 100% { + opacity: 0; + -moz-transform: scale(.3); + } +} + +@-o-keyframes bounceOut { + 0% { + -o-transform: scale(1); + } + + 25% { + -o-transform: scale(.95); + } + + 50% { + opacity: 1; + -o-transform: scale(1.1); + } + + 100% { + opacity: 0; + -o-transform: scale(.3); + } +} + +@keyframes bounceOut { + 0% { + transform: scale(1); + } + + 25% { + transform: scale(.95); + } + + 50% { + opacity: 1; + transform: scale(1.1); + } + + 100% { + opacity: 0; + transform: scale(.3); + } +} + +.bounceOut { + .animation-name(bounceOut); +} \ No newline at end of file diff --git a/media/less/vendor/animate/bounceOutDown.less b/media/less/vendor/animate/bounceOutDown.less new file mode 100644 index 0000000..2d01d15 --- /dev/null +++ b/media/less/vendor/animate/bounceOutDown.less @@ -0,0 +1,67 @@ +@-webkit-keyframes bounceOutDown { + 0% { + -webkit-transform: translateY(0); + } + + 20% { + opacity: 1; + -webkit-transform: translateY(-20px); + } + + 100% { + opacity: 0; + -webkit-transform: translateY(2000px); + } +} + +@-moz-keyframes bounceOutDown { + 0% { + -moz-transform: translateY(0); + } + + 20% { + opacity: 1; + -moz-transform: translateY(-20px); + } + + 100% { + opacity: 0; + -moz-transform: translateY(2000px); + } +} + +@-o-keyframes bounceOutDown { + 0% { + -o-transform: translateY(0); + } + + 20% { + opacity: 1; + -o-transform: translateY(-20px); + } + + 100% { + opacity: 0; + -o-transform: translateY(2000px); + } +} + +@keyframes bounceOutDown { + 0% { + transform: translateY(0); + } + + 20% { + opacity: 1; + transform: translateY(-20px); + } + + 100% { + opacity: 0; + transform: translateY(2000px); + } +} + +.bounceOutDown { + .animation-name(bounceOutDown); +} \ No newline at end of file diff --git a/media/less/vendor/animate/bounceOutLeft.less b/media/less/vendor/animate/bounceOutLeft.less new file mode 100644 index 0000000..2e408ba --- /dev/null +++ b/media/less/vendor/animate/bounceOutLeft.less @@ -0,0 +1,67 @@ +@-webkit-keyframes bounceOutLeft { + 0% { + -webkit-transform: translateX(0); + } + + 20% { + opacity: 1; + -webkit-transform: translateX(20px); + } + + 100% { + opacity: 0; + -webkit-transform: translateX(-2000px); + } +} + +@-moz-keyframes bounceOutLeft { + 0% { + -moz-transform: translateX(0); + } + + 20% { + opacity: 1; + -moz-transform: translateX(20px); + } + + 100% { + opacity: 0; + -moz-transform: translateX(-2000px); + } +} + +@-o-keyframes bounceOutLeft { + 0% { + -o-transform: translateX(0); + } + + 20% { + opacity: 1; + -o-transform: translateX(20px); + } + + 100% { + opacity: 0; + -o-transform: translateX(-2000px); + } +} + +@keyframes bounceOutLeft { + 0% { + transform: translateX(0); + } + + 20% { + opacity: 1; + transform: translateX(20px); + } + + 100% { + opacity: 0; + transform: translateX(-2000px); + } +} + +.bounceOutLeft { + .animation-name(bounceOutLeft); +} \ No newline at end of file diff --git a/media/less/vendor/animate/bounceOutRight.less b/media/less/vendor/animate/bounceOutRight.less new file mode 100644 index 0000000..bc77c24 --- /dev/null +++ b/media/less/vendor/animate/bounceOutRight.less @@ -0,0 +1,67 @@ +@-webkit-keyframes bounceOutRight { + 0% { + -webkit-transform: translateX(0); + } + + 20% { + opacity: 1; + -webkit-transform: translateX(-20px); + } + + 100% { + opacity: 0; + -webkit-transform: translateX(2000px); + } +} + +@-moz-keyframes bounceOutRight { + 0% { + -moz-transform: translateX(0); + } + + 20% { + opacity: 1; + -moz-transform: translateX(-20px); + } + + 100% { + opacity: 0; + -moz-transform: translateX(2000px); + } +} + +@-o-keyframes bounceOutRight { + 0% { + -o-transform: translateX(0); + } + + 20% { + opacity: 1; + -o-transform: translateX(-20px); + } + + 100% { + opacity: 0; + -o-transform: translateX(2000px); + } +} + +@keyframes bounceOutRight { + 0% { + transform: translateX(0); + } + + 20% { + opacity: 1; + transform: translateX(-20px); + } + + 100% { + opacity: 0; + transform: translateX(2000px); + } +} + +.bounceOutRight { + .animation-name(bounceOutRight); +} \ No newline at end of file diff --git a/media/less/vendor/animate/bounceOutUp.less b/media/less/vendor/animate/bounceOutUp.less new file mode 100644 index 0000000..31fa892 --- /dev/null +++ b/media/less/vendor/animate/bounceOutUp.less @@ -0,0 +1,67 @@ +@-webkit-keyframes bounceOutUp { + 0% { + -webkit-transform: translateY(0); + } + + 20% { + opacity: 1; + -webkit-transform: translateY(20px); + } + + 100% { + opacity: 0; + -webkit-transform: translateY(-2000px); + } +} + +@-moz-keyframes bounceOutUp { + 0% { + -moz-transform: translateY(0); + } + + 20% { + opacity: 1; + -moz-transform: translateY(20px); + } + + 100% { + opacity: 0; + -moz-transform: translateY(-2000px); + } +} + +@-o-keyframes bounceOutUp { + 0% { + -o-transform: translateY(0); + } + + 20% { + opacity: 1; + -o-transform: translateY(20px); + } + + 100% { + opacity: 0; + -o-transform: translateY(-2000px); + } +} + +@keyframes bounceOutUp { + 0% { + transform: translateY(0); + } + + 20% { + opacity: 1; + transform: translateY(20px); + } + + 100% { + opacity: 0; + transform: translateY(-2000px); + } +} + +.bounceOutUp { + .animation-name(bounceOutUp); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeIn.less b/media/less/vendor/animate/fadeIn.less new file mode 100644 index 0000000..bb4c5f9 --- /dev/null +++ b/media/less/vendor/animate/fadeIn.less @@ -0,0 +1,23 @@ +@-webkit-keyframes fadeIn { + 0% {opacity: 0;} + 100% {opacity: 1;} +} + +@-moz-keyframes fadeIn { + 0% {opacity: 0;} + 100% {opacity: 1;} +} + +@-o-keyframes fadeIn { + 0% {opacity: 0;} + 100% {opacity: 1;} +} + +@keyframes fadeIn { + 0% {opacity: 0;} + 100% {opacity: 1;} +} + +.fadeIn { + .animation-name(fadeIn); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeInDown.less b/media/less/vendor/animate/fadeInDown.less new file mode 100644 index 0000000..3180869 --- /dev/null +++ b/media/less/vendor/animate/fadeInDown.less @@ -0,0 +1,51 @@ +@-webkit-keyframes fadeInDown { + 0% { + opacity: 0; + -webkit-transform: translateY(-20px); + } + + 100% { + opacity: 1; + -webkit-transform: translateY(0); + } +} + +@-moz-keyframes fadeInDown { + 0% { + opacity: 0; + -moz-transform: translateY(-20px); + } + + 100% { + opacity: 1; + -moz-transform: translateY(0); + } +} + +@-o-keyframes fadeInDown { + 0% { + opacity: 0; + -o-transform: translateY(-20px); + } + + 100% { + opacity: 1; + -o-transform: translateY(0); + } +} + +@keyframes fadeInDown { + 0% { + opacity: 0; + transform: translateY(-20px); + } + + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.fadeInDown { + .animation-name(fadeInDown); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeInDownBig.less b/media/less/vendor/animate/fadeInDownBig.less new file mode 100644 index 0000000..93e2f41 --- /dev/null +++ b/media/less/vendor/animate/fadeInDownBig.less @@ -0,0 +1,51 @@ +@-webkit-keyframes fadeInDownBig { + 0% { + opacity: 0; + -webkit-transform: translateY(-2000px); + } + + 100% { + opacity: 1; + -webkit-transform: translateY(0); + } +} + +@-moz-keyframes fadeInDownBig { + 0% { + opacity: 0; + -moz-transform: translateY(-2000px); + } + + 100% { + opacity: 1; + -moz-transform: translateY(0); + } +} + +@-o-keyframes fadeInDownBig { + 0% { + opacity: 0; + -o-transform: translateY(-2000px); + } + + 100% { + opacity: 1; + -o-transform: translateY(0); + } +} + +@keyframes fadeInDownBig { + 0% { + opacity: 0; + transform: translateY(-2000px); + } + + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.fadeInDownBig { + .animation-name(fadeInDownBig); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeInLeft.less b/media/less/vendor/animate/fadeInLeft.less new file mode 100644 index 0000000..125f036 --- /dev/null +++ b/media/less/vendor/animate/fadeInLeft.less @@ -0,0 +1,51 @@ +@-webkit-keyframes fadeInLeft { + 0% { + opacity: 0; + -webkit-transform: translateX(-20px); + } + + 100% { + opacity: 1; + -webkit-transform: translateX(0); + } +} + +@-moz-keyframes fadeInLeft { + 0% { + opacity: 0; + -moz-transform: translateX(-20px); + } + + 100% { + opacity: 1; + -moz-transform: translateX(0); + } +} + +@-o-keyframes fadeInLeft { + 0% { + opacity: 0; + -o-transform: translateX(-20px); + } + + 100% { + opacity: 1; + -o-transform: translateX(0); + } +} + +@keyframes fadeInLeft { + 0% { + opacity: 0; + transform: translateX(-20px); + } + + 100% { + opacity: 1; + transform: translateX(0); + } +} + +.fadeInLeft { + .animation-name(fadeInLeft); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeInLeftBig.less b/media/less/vendor/animate/fadeInLeftBig.less new file mode 100644 index 0000000..f3e1409 --- /dev/null +++ b/media/less/vendor/animate/fadeInLeftBig.less @@ -0,0 +1,48 @@ +@-webkit-keyframes fadeInLeftBig { + 0% { + opacity: 0; + -webkit-transform: translateX(-2000px); + } + + 100% { + opacity: 1; + -webkit-transform: translateX(0); + } +} +@-moz-keyframes fadeInLeftBig { + 0% { + opacity: 0; + -moz-transform: translateX(-2000px); + } + + 100% { + opacity: 1; + -moz-transform: translateX(0); + } +} +@-o-keyframes fadeInLeftBig { + 0% { + opacity: 0; + -o-transform: translateX(-2000px); + } + + 100% { + opacity: 1; + -o-transform: translateX(0); + } +} +@keyframes fadeInLeftBig { + 0% { + opacity: 0; + transform: translateX(-2000px); + } + + 100% { + opacity: 1; + transform: translateX(0); + } +} + +.fadeInLeftBig { + .animation-name(fadeInLeftBig); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeInRight.less b/media/less/vendor/animate/fadeInRight.less new file mode 100644 index 0000000..a28a83b --- /dev/null +++ b/media/less/vendor/animate/fadeInRight.less @@ -0,0 +1,51 @@ +@-webkit-keyframes fadeInRight { + 0% { + opacity: 0; + -webkit-transform: translateX(20px); + } + + 100% { + opacity: 1; + -webkit-transform: translateX(0); + } +} + +@-moz-keyframes fadeInRight { + 0% { + opacity: 0; + -moz-transform: translateX(20px); + } + + 100% { + opacity: 1; + -moz-transform: translateX(0); + } +} + +@-o-keyframes fadeInRight { + 0% { + opacity: 0; + -o-transform: translateX(20px); + } + + 100% { + opacity: 1; + -o-transform: translateX(0); + } +} + +@keyframes fadeInRight { + 0% { + opacity: 0; + transform: translateX(20px); + } + + 100% { + opacity: 1; + transform: translateX(0); + } +} + +.fadeInRight { + .animation-name(fadeInRight); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeInRightBig.less b/media/less/vendor/animate/fadeInRightBig.less new file mode 100644 index 0000000..884a339 --- /dev/null +++ b/media/less/vendor/animate/fadeInRightBig.less @@ -0,0 +1,51 @@ +@-webkit-keyframes fadeInRightBig { + 0% { + opacity: 0; + -webkit-transform: translateX(2000px); + } + + 100% { + opacity: 1; + -webkit-transform: translateX(0); + } +} + +@-moz-keyframes fadeInRightBig { + 0% { + opacity: 0; + -moz-transform: translateX(2000px); + } + + 100% { + opacity: 1; + -moz-transform: translateX(0); + } +} + +@-o-keyframes fadeInRightBig { + 0% { + opacity: 0; + -o-transform: translateX(2000px); + } + + 100% { + opacity: 1; + -o-transform: translateX(0); + } +} + +@keyframes fadeInRightBig { + 0% { + opacity: 0; + transform: translateX(2000px); + } + + 100% { + opacity: 1; + transform: translateX(0); + } +} + +.fadeInRightBig { + .animation-name(fadeInRightBig); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeInUp.less b/media/less/vendor/animate/fadeInUp.less new file mode 100644 index 0000000..b7f762a --- /dev/null +++ b/media/less/vendor/animate/fadeInUp.less @@ -0,0 +1,51 @@ +@-webkit-keyframes fadeInUp { + 0% { + opacity: 0; + -webkit-transform: translateY(20px); + } + + 100% { + opacity: 1; + -webkit-transform: translateY(0); + } +} + +@-moz-keyframes fadeInUp { + 0% { + opacity: 0; + -moz-transform: translateY(20px); + } + + 100% { + opacity: 1; + -moz-transform: translateY(0); + } +} + +@-o-keyframes fadeInUp { + 0% { + opacity: 0; + -o-transform: translateY(20px); + } + + 100% { + opacity: 1; + -o-transform: translateY(0); + } +} + +@keyframes fadeInUp { + 0% { + opacity: 0; + transform: translateY(20px); + } + + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.fadeInUp { + .animation-name(fadeInUp); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeInUpBig.less b/media/less/vendor/animate/fadeInUpBig.less new file mode 100644 index 0000000..c6e48fa --- /dev/null +++ b/media/less/vendor/animate/fadeInUpBig.less @@ -0,0 +1,51 @@ +@-webkit-keyframes fadeInUpBig { + 0% { + opacity: 0; + -webkit-transform: translateY(2000px); + } + + 100% { + opacity: 1; + -webkit-transform: translateY(0); + } +} + +@-moz-keyframes fadeInUpBig { + 0% { + opacity: 0; + -moz-transform: translateY(2000px); + } + + 100% { + opacity: 1; + -moz-transform: translateY(0); + } +} + +@-o-keyframes fadeInUpBig { + 0% { + opacity: 0; + -o-transform: translateY(2000px); + } + + 100% { + opacity: 1; + -o-transform: translateY(0); + } +} + +@keyframes fadeInUpBig { + 0% { + opacity: 0; + transform: translateY(2000px); + } + + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.fadeInUpBig { + .animation-name(fadeInUpBig); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeOut.less b/media/less/vendor/animate/fadeOut.less new file mode 100644 index 0000000..e0e1008 --- /dev/null +++ b/media/less/vendor/animate/fadeOut.less @@ -0,0 +1,23 @@ +@-webkit-keyframes fadeOut { + 0% {opacity: 1;} + 100% {opacity: 0;} +} + +@-moz-keyframes fadeOut { + 0% {opacity: 1;} + 100% {opacity: 0;} +} + +@-o-keyframes fadeOut { + 0% {opacity: 1;} + 100% {opacity: 0;} +} + +@keyframes fadeOut { + 0% {opacity: 1;} + 100% {opacity: 0;} +} + +.fadeOut { + .animation-name(fadeOut); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeOutDown.less b/media/less/vendor/animate/fadeOutDown.less new file mode 100644 index 0000000..3c68f1d --- /dev/null +++ b/media/less/vendor/animate/fadeOutDown.less @@ -0,0 +1,51 @@ +@-webkit-keyframes fadeOutDown { + 0% { + opacity: 1; + -webkit-transform: translateY(0); + } + + 100% { + opacity: 0; + -webkit-transform: translateY(20px); + } +} + +@-moz-keyframes fadeOutDown { + 0% { + opacity: 1; + -moz-transform: translateY(0); + } + + 100% { + opacity: 0; + -moz-transform: translateY(20px); + } +} + +@-o-keyframes fadeOutDown { + 0% { + opacity: 1; + -o-transform: translateY(0); + } + + 100% { + opacity: 0; + -o-transform: translateY(20px); + } +} + +@keyframes fadeOutDown { + 0% { + opacity: 1; + transform: translateY(0); + } + + 100% { + opacity: 0; + transform: translateY(20px); + } +} + +.fadeOutDown { + .animation-name(fadeOutDown); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeOutDownBig.less b/media/less/vendor/animate/fadeOutDownBig.less new file mode 100644 index 0000000..39e3768 --- /dev/null +++ b/media/less/vendor/animate/fadeOutDownBig.less @@ -0,0 +1,51 @@ +@-webkit-keyframes fadeOutDownBig { + 0% { + opacity: 1; + -webkit-transform: translateY(0); + } + + 100% { + opacity: 0; + -webkit-transform: translateY(2000px); + } +} + +@-moz-keyframes fadeOutDownBig { + 0% { + opacity: 1; + -moz-transform: translateY(0); + } + + 100% { + opacity: 0; + -moz-transform: translateY(2000px); + } +} + +@-o-keyframes fadeOutDownBig { + 0% { + opacity: 1; + -o-transform: translateY(0); + } + + 100% { + opacity: 0; + -o-transform: translateY(2000px); + } +} + +@keyframes fadeOutDownBig { + 0% { + opacity: 1; + transform: translateY(0); + } + + 100% { + opacity: 0; + transform: translateY(2000px); + } +} + +.fadeOutDownBig { + .animation-name(fadeOutDownBig); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeOutLeft.less b/media/less/vendor/animate/fadeOutLeft.less new file mode 100644 index 0000000..b34cd7b --- /dev/null +++ b/media/less/vendor/animate/fadeOutLeft.less @@ -0,0 +1,51 @@ +@-webkit-keyframes fadeOutLeft { + 0% { + opacity: 1; + -webkit-transform: translateX(0); + } + + 100% { + opacity: 0; + -webkit-transform: translateX(-20px); + } +} + +@-moz-keyframes fadeOutLeft { + 0% { + opacity: 1; + -moz-transform: translateX(0); + } + + 100% { + opacity: 0; + -moz-transform: translateX(-20px); + } +} + +@-o-keyframes fadeOutLeft { + 0% { + opacity: 1; + -o-transform: translateX(0); + } + + 100% { + opacity: 0; + -o-transform: translateX(-20px); + } +} + +@keyframes fadeOutLeft { + 0% { + opacity: 1; + transform: translateX(0); + } + + 100% { + opacity: 0; + transform: translateX(-20px); + } +} + +.fadeOutLeft { + .animation-name(fadeOutLeft); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeOutLeftBig.less b/media/less/vendor/animate/fadeOutLeftBig.less new file mode 100644 index 0000000..1da77de --- /dev/null +++ b/media/less/vendor/animate/fadeOutLeftBig.less @@ -0,0 +1,51 @@ +@-webkit-keyframes fadeOutLeftBig { + 0% { + opacity: 1; + -webkit-transform: translateX(0); + } + + 100% { + opacity: 0; + -webkit-transform: translateX(-2000px); + } +} + +@-moz-keyframes fadeOutLeftBig { + 0% { + opacity: 1; + -moz-transform: translateX(0); + } + + 100% { + opacity: 0; + -moz-transform: translateX(-2000px); + } +} + +@-o-keyframes fadeOutLeftBig { + 0% { + opacity: 1; + -o-transform: translateX(0); + } + + 100% { + opacity: 0; + -o-transform: translateX(-2000px); + } +} + +@keyframes fadeOutLeftBig { + 0% { + opacity: 1; + transform: translateX(0); + } + + 100% { + opacity: 0; + transform: translateX(-2000px); + } +} + +.fadeOutLeftBig { + .animation-name(fadeOutLeftBig); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeOutRight.less b/media/less/vendor/animate/fadeOutRight.less new file mode 100644 index 0000000..255a6be --- /dev/null +++ b/media/less/vendor/animate/fadeOutRight.less @@ -0,0 +1,51 @@ +@-webkit-keyframes fadeOutRight { + 0% { + opacity: 1; + -webkit-transform: translateX(0); + } + + 100% { + opacity: 0; + -webkit-transform: translateX(20px); + } +} + +@-moz-keyframes fadeOutRight { + 0% { + opacity: 1; + -moz-transform: translateX(0); + } + + 100% { + opacity: 0; + -moz-transform: translateX(20px); + } +} + +@-o-keyframes fadeOutRight { + 0% { + opacity: 1; + -o-transform: translateX(0); + } + + 100% { + opacity: 0; + -o-transform: translateX(20px); + } +} + +@keyframes fadeOutRight { + 0% { + opacity: 1; + transform: translateX(0); + } + + 100% { + opacity: 0; + transform: translateX(20px); + } +} + +.fadeOutRight { + .animation-name(fadeOutRight); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeOutRightBig.less b/media/less/vendor/animate/fadeOutRightBig.less new file mode 100644 index 0000000..e8beadb --- /dev/null +++ b/media/less/vendor/animate/fadeOutRightBig.less @@ -0,0 +1,48 @@ +@-webkit-keyframes fadeOutRightBig { + 0% { + opacity: 1; + -webkit-transform: translateX(0); + } + + 100% { + opacity: 0; + -webkit-transform: translateX(2000px); + } +} +@-moz-keyframes fadeOutRightBig { + 0% { + opacity: 1; + -moz-transform: translateX(0); + } + + 100% { + opacity: 0; + -moz-transform: translateX(2000px); + } +} +@-o-keyframes fadeOutRightBig { + 0% { + opacity: 1; + -o-transform: translateX(0); + } + + 100% { + opacity: 0; + -o-transform: translateX(2000px); + } +} +@keyframes fadeOutRightBig { + 0% { + opacity: 1; + transform: translateX(0); + } + + 100% { + opacity: 0; + transform: translateX(2000px); + } +} + +.fadeOutRightBig { + .animation-name(fadeOutRightBig); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeOutUp.less b/media/less/vendor/animate/fadeOutUp.less new file mode 100644 index 0000000..a561204 --- /dev/null +++ b/media/less/vendor/animate/fadeOutUp.less @@ -0,0 +1,48 @@ +@-webkit-keyframes fadeOutUp { + 0% { + opacity: 1; + -webkit-transform: translateY(0); + } + + 100% { + opacity: 0; + -webkit-transform: translateY(-20px); + } +} +@-moz-keyframes fadeOutUp { + 0% { + opacity: 1; + -moz-transform: translateY(0); + } + + 100% { + opacity: 0; + -moz-transform: translateY(-20px); + } +} +@-o-keyframes fadeOutUp { + 0% { + opacity: 1; + -o-transform: translateY(0); + } + + 100% { + opacity: 0; + -o-transform: translateY(-20px); + } +} +@keyframes fadeOutUp { + 0% { + opacity: 1; + transform: translateY(0); + } + + 100% { + opacity: 0; + transform: translateY(-20px); + } +} + +.fadeOutUp { + .animation-name(fadeOutUp); +} \ No newline at end of file diff --git a/media/less/vendor/animate/fadeOutUpBig.less b/media/less/vendor/animate/fadeOutUpBig.less new file mode 100644 index 0000000..278d778 --- /dev/null +++ b/media/less/vendor/animate/fadeOutUpBig.less @@ -0,0 +1,51 @@ +@-webkit-keyframes fadeOutUpBig { + 0% { + opacity: 1; + -webkit-transform: translateY(0); + } + + 100% { + opacity: 0; + -webkit-transform: translateY(-2000px); + } +} + +@-moz-keyframes fadeOutUpBig { + 0% { + opacity: 1; + -moz-transform: translateY(0); + } + + 100% { + opacity: 0; + -moz-transform: translateY(-2000px); + } +} + +@-o-keyframes fadeOutUpBig { + 0% { + opacity: 1; + -o-transform: translateY(0); + } + + 100% { + opacity: 0; + -o-transform: translateY(-2000px); + } +} + +@keyframes fadeOutUpBig { + 0% { + opacity: 1; + transform: translateY(0); + } + + 100% { + opacity: 0; + transform: translateY(-2000px); + } +} + +.fadeOutUpBig { + .animation-name(fadeOutUpBig); +} \ No newline at end of file diff --git a/media/less/vendor/animate/flash.less b/media/less/vendor/animate/flash.less new file mode 100644 index 0000000..9cd1571 --- /dev/null +++ b/media/less/vendor/animate/flash.less @@ -0,0 +1,23 @@ +@-webkit-keyframes flash { + 0%, 50%, 100% {opacity: 1;} + 25%, 75% {opacity: 0;} +} + +@-moz-keyframes flash { + 0%, 50%, 100% {opacity: 1;} + 25%, 75% {opacity: 0;} +} + +@-o-keyframes flash { + 0%, 50%, 100% {opacity: 1;} + 25%, 75% {opacity: 0;} +} + +@keyframes flash { + 0%, 50%, 100% {opacity: 1;} + 25%, 75% {opacity: 0;} +} + +.flash { + .animation-name(flash); +} \ No newline at end of file diff --git a/media/less/vendor/animate/flip.less b/media/less/vendor/animate/flip.less new file mode 100644 index 0000000..9fc3c11 --- /dev/null +++ b/media/less/vendor/animate/flip.less @@ -0,0 +1,93 @@ +@-webkit-keyframes flip { + 0% { + -webkit-transform: perspective(400px) translateZ(0) rotateY(0) scale(1); + -webkit-animation-timing-function: ease-out; + } + 40% { + -webkit-transform: perspective(400px) translateZ(150px) rotateY(170deg) scale(1); + -webkit-animation-timing-function: ease-out; + } + 50% { + -webkit-transform: perspective(400px) translateZ(150px) rotateY(190deg) scale(1); + -webkit-animation-timing-function: ease-in; + } + 80% { + -webkit-transform: perspective(400px) translateZ(0) rotateY(360deg) scale(.95); + -webkit-animation-timing-function: ease-in; + } + 100% { + -webkit-transform: perspective(400px) translateZ(0) rotateY(360deg) scale(1); + -webkit-animation-timing-function: ease-in; + } +} +@-moz-keyframes flip { + 0% { + -moz-transform: perspective(400px) translateZ(0) rotateY(0) scale(1); + -moz-animation-timing-function: ease-out; + } + 40% { + -moz-transform: perspective(400px) translateZ(150px) rotateY(170deg) scale(1); + -moz-animation-timing-function: ease-out; + } + 50% { + -moz-transform: perspective(400px) translateZ(150px) rotateY(190deg) scale(1); + -moz-animation-timing-function: ease-in; + } + 80% { + -moz-transform: perspective(400px) translateZ(0) rotateY(360deg) scale(.95); + -moz-animation-timing-function: ease-in; + } + 100% { + -moz-transform: perspective(400px) translateZ(0) rotateY(360deg) scale(1); + -moz-animation-timing-function: ease-in; + } +} +@-o-keyframes flip { + 0% { + -o-transform: perspective(400px) translateZ(0) rotateY(0) scale(1); + -o-animation-timing-function: ease-out; + } + 40% { + -o-transform: perspective(400px) translateZ(150px) rotateY(170deg) scale(1); + -o-animation-timing-function: ease-out; + } + 50% { + -o-transform: perspective(400px) translateZ(150px) rotateY(190deg) scale(1); + -o-animation-timing-function: ease-in; + } + 80% { + -o-transform: perspective(400px) translateZ(0) rotateY(360deg) scale(.95); + -o-animation-timing-function: ease-in; + } + 100% { + -o-transform: perspective(400px) translateZ(0) rotateY(360deg) scale(1); + -o-animation-timing-function: ease-in; + } +} +@keyframes flip { + 0% { + transform: perspective(400px) translateZ(0) rotateY(0) scale(1); + animation-timing-function: ease-out; + } + 40% { + transform: perspective(400px) translateZ(150px) rotateY(170deg) scale(1); + animation-timing-function: ease-out; + } + 50% { + transform: perspective(400px) translateZ(150px) rotateY(190deg) scale(1); + animation-timing-function: ease-in; + } + 80% { + transform: perspective(400px) translateZ(0) rotateY(360deg) scale(.95); + animation-timing-function: ease-in; + } + 100% { + transform: perspective(400px) translateZ(0) rotateY(360deg) scale(1); + animation-timing-function: ease-in; + } +} + +.animate.flip { + .backface-visibility(visible); + .animation-name(flip); +} diff --git a/media/less/vendor/animate/flipInX.less b/media/less/vendor/animate/flipInX.less new file mode 100644 index 0000000..1e717e1 --- /dev/null +++ b/media/less/vendor/animate/flipInX.less @@ -0,0 +1,81 @@ +@-webkit-keyframes flipInX { + 0% { + -webkit-transform: perspective(400px) rotateX(90deg); + opacity: 0; + } + + 40% { + -webkit-transform: perspective(400px) rotateX(-10deg); + } + + 70% { + -webkit-transform: perspective(400px) rotateX(10deg); + } + + 100% { + -webkit-transform: perspective(400px) rotateX(0deg); + opacity: 1; + } +} +@-moz-keyframes flipInX { + 0% { + -moz-transform: perspective(400px) rotateX(90deg); + opacity: 0; + } + + 40% { + -moz-transform: perspective(400px) rotateX(-10deg); + } + + 70% { + -moz-transform: perspective(400px) rotateX(10deg); + } + + 100% { + -moz-transform: perspective(400px) rotateX(0deg); + opacity: 1; + } +} +@-o-keyframes flipInX { + 0% { + -o-transform: perspective(400px) rotateX(90deg); + opacity: 0; + } + + 40% { + -o-transform: perspective(400px) rotateX(-10deg); + } + + 70% { + -o-transform: perspective(400px) rotateX(10deg); + } + + 100% { + -o-transform: perspective(400px) rotateX(0deg); + opacity: 1; + } +} +@keyframes flipInX { + 0% { + transform: perspective(400px) rotateX(90deg); + opacity: 0; + } + + 40% { + transform: perspective(400px) rotateX(-10deg); + } + + 70% { + transform: perspective(400px) rotateX(10deg); + } + + 100% { + transform: perspective(400px) rotateX(0deg); + opacity: 1; + } +} + +.flipInX { + .backface-visibility(visible); + .animation-name(flipInX); +} \ No newline at end of file diff --git a/media/less/vendor/animate/flipInY.less b/media/less/vendor/animate/flipInY.less new file mode 100644 index 0000000..0d19956 --- /dev/null +++ b/media/less/vendor/animate/flipInY.less @@ -0,0 +1,81 @@ +@-webkit-keyframes flipInY { + 0% { + -webkit-transform: perspective(400px) rotateY(90deg); + opacity: 0; + } + + 40% { + -webkit-transform: perspective(400px) rotateY(-10deg); + } + + 70% { + -webkit-transform: perspective(400px) rotateY(10deg); + } + + 100% { + -webkit-transform: perspective(400px) rotateY(0deg); + opacity: 1; + } +} +@-moz-keyframes flipInY { + 0% { + -moz-transform: perspective(400px) rotateY(90deg); + opacity: 0; + } + + 40% { + -moz-transform: perspective(400px) rotateY(-10deg); + } + + 70% { + -moz-transform: perspective(400px) rotateY(10deg); + } + + 100% { + -moz-transform: perspective(400px) rotateY(0deg); + opacity: 1; + } +} +@-o-keyframes flipInY { + 0% { + -o-transform: perspective(400px) rotateY(90deg); + opacity: 0; + } + + 40% { + -o-transform: perspective(400px) rotateY(-10deg); + } + + 70% { + -o-transform: perspective(400px) rotateY(10deg); + } + + 100% { + -o-transform: perspective(400px) rotateY(0deg); + opacity: 1; + } +} +@keyframes flipInY { + 0% { + transform: perspective(400px) rotateY(90deg); + opacity: 0; + } + + 40% { + transform: perspective(400px) rotateY(-10deg); + } + + 70% { + transform: perspective(400px) rotateY(10deg); + } + + 100% { + transform: perspective(400px) rotateY(0deg); + opacity: 1; + } +} + +.flipInY { + .backface-visibility(visible); + .animation-name(flipInY); +} \ No newline at end of file diff --git a/media/less/vendor/animate/flipOutX.less b/media/less/vendor/animate/flipOutX.less new file mode 100644 index 0000000..4086553 --- /dev/null +++ b/media/less/vendor/animate/flipOutX.less @@ -0,0 +1,48 @@ +@-webkit-keyframes flipOutX { + 0% { + -webkit-transform: perspective(400px) rotateX(0deg); + opacity: 1; + } + 100% { + -webkit-transform: perspective(400px) rotateX(90deg); + opacity: 0; + } +} + +@-moz-keyframes flipOutX { + 0% { + -moz-transform: perspective(400px) rotateX(0deg); + opacity: 1; + } + 100% { + -moz-transform: perspective(400px) rotateX(90deg); + opacity: 0; + } +} + +@-o-keyframes flipOutX { + 0% { + -o-transform: perspective(400px) rotateX(0deg); + opacity: 1; + } + 100% { + -o-transform: perspective(400px) rotateX(90deg); + opacity: 0; + } +} + +@keyframes flipOutX { + 0% { + transform: perspective(400px) rotateX(0deg); + opacity: 1; + } + 100% { + transform: perspective(400px) rotateX(90deg); + opacity: 0; + } +} + +.flipOutX { + .backface-visibility(visible); + .animation-name(flipOutX); +} \ No newline at end of file diff --git a/media/less/vendor/animate/flipOutY.less b/media/less/vendor/animate/flipOutY.less new file mode 100644 index 0000000..c3d1c69 --- /dev/null +++ b/media/less/vendor/animate/flipOutY.less @@ -0,0 +1,45 @@ +@-webkit-keyframes flipOutY { + 0% { + -webkit-transform: perspective(400px) rotateY(0deg); + opacity: 1; + } + 100% { + -webkit-transform: perspective(400px) rotateY(90deg); + opacity: 0; + } +} +@-moz-keyframes flipOutY { + 0% { + -moz-transform: perspective(400px) rotateY(0deg); + opacity: 1; + } + 100% { + -moz-transform: perspective(400px) rotateY(90deg); + opacity: 0; + } +} +@-o-keyframes flipOutY { + 0% { + -o-transform: perspective(400px) rotateY(0deg); + opacity: 1; + } + 100% { + -o-transform: perspective(400px) rotateY(90deg); + opacity: 0; + } +} +@keyframes flipOutY { + 0% { + transform: perspective(400px) rotateY(0deg); + opacity: 1; + } + 100% { + transform: perspective(400px) rotateY(90deg); + opacity: 0; + } +} + +.flipOutY { + .backface-visibility(visible); + .animation-name(flipOutY); +} \ No newline at end of file diff --git a/media/less/vendor/animate/hinge.less b/media/less/vendor/animate/hinge.less new file mode 100644 index 0000000..b0e6c44 --- /dev/null +++ b/media/less/vendor/animate/hinge.less @@ -0,0 +1,35 @@ +@-webkit-keyframes hinge { + 0% { -webkit-transform: rotate(0); -webkit-transform-origin: top left; -webkit-animation-timing-function: ease-in-out; } + 20%, 60% { -webkit-transform: rotate(80deg); -webkit-transform-origin: top left; -webkit-animation-timing-function: ease-in-out; } + 40% { -webkit-transform: rotate(60deg); -webkit-transform-origin: top left; -webkit-animation-timing-function: ease-in-out; } + 80% { -webkit-transform: rotate(60deg) translateY(0); opacity: 1; -webkit-transform-origin: top left; -webkit-animation-timing-function: ease-in-out; } + 100% { -webkit-transform: translateY(700px); opacity: 0; } +} + +@-moz-keyframes hinge { + 0% { -moz-transform: rotate(0); -moz-transform-origin: top left; -moz-animation-timing-function: ease-in-out; } + 20%, 60% { -moz-transform: rotate(80deg); -moz-transform-origin: top left; -moz-animation-timing-function: ease-in-out; } + 40% { -moz-transform: rotate(60deg); -moz-transform-origin: top left; -moz-animation-timing-function: ease-in-out; } + 80% { -moz-transform: rotate(60deg) translateY(0); opacity: 1; -moz-transform-origin: top left; -moz-animation-timing-function: ease-in-out; } + 100% { -moz-transform: translateY(700px); opacity: 0; } +} + +@-o-keyframes hinge { + 0% { -o-transform: rotate(0); -o-transform-origin: top left; -o-animation-timing-function: ease-in-out; } + 20%, 60% { -o-transform: rotate(80deg); -o-transform-origin: top left; -o-animation-timing-function: ease-in-out; } + 40% { -o-transform: rotate(60deg); -o-transform-origin: top left; -o-animation-timing-function: ease-in-out; } + 80% { -o-transform: rotate(60deg) translateY(0); opacity: 1; -o-transform-origin: top left; -o-animation-timing-function: ease-in-out; } + 100% { -o-transform: translateY(700px); opacity: 0; } +} + +@keyframes hinge { + 0% { transform: rotate(0); transform-origin: top left; animation-timing-function: ease-in-out; } + 20%, 60% { transform: rotate(80deg); transform-origin: top left; animation-timing-function: ease-in-out; } + 40% { transform: rotate(60deg); transform-origin: top left; animation-timing-function: ease-in-out; } + 80% { transform: rotate(60deg) translateY(0); opacity: 1; transform-origin: top left; animation-timing-function: ease-in-out; } + 100% { transform: translateY(700px); opacity: 0; } +} + +.hinge { + .animation-name(hinge); +} \ No newline at end of file diff --git a/media/less/vendor/animate/lightSpeedIn.less b/media/less/vendor/animate/lightSpeedIn.less new file mode 100644 index 0000000..e705c1a --- /dev/null +++ b/media/less/vendor/animate/lightSpeedIn.less @@ -0,0 +1,32 @@ +@-webkit-keyframes lightSpeedIn { + 0% { -webkit-transform: translateX(100%) skewX(-30deg); opacity: 0; } + 60% { -webkit-transform: translateX(-20%) skewX(30deg); opacity: 1; } + 80% { -webkit-transform: translateX(0%) skewX(-15deg); opacity: 1; } + 100% { -webkit-transform: translateX(0%) skewX(0deg); opacity: 1; } +} + +@-moz-keyframes lightSpeedIn { + 0% { -moz-transform: translateX(100%) skewX(-30deg); opacity: 0; } + 60% { -moz-transform: translateX(-20%) skewX(30deg); opacity: 1; } + 80% { -moz-transform: translateX(0%) skewX(-15deg); opacity: 1; } + 100% { -moz-transform: translateX(0%) skewX(0deg); opacity: 1; } +} + +@-o-keyframes lightSpeedIn { + 0% { -o-transform: translateX(100%) skewX(-30deg); opacity: 0; } + 60% { -o-transform: translateX(-20%) skewX(30deg); opacity: 1; } + 80% { -o-transform: translateX(0%) skewX(-15deg); opacity: 1; } + 100% { -o-transform: translateX(0%) skewX(0deg); opacity: 1; } +} + +@keyframes lightSpeedIn { + 0% { transform: translateX(100%) skewX(-30deg); opacity: 0; } + 60% { transform: translateX(-20%) skewX(30deg); opacity: 1; } + 80% { transform: translateX(0%) skewX(-15deg); opacity: 1; } + 100% { transform: translateX(0%) skewX(0deg); opacity: 1; } +} + +.lightSpeedIn { + .animation-name(lightSpeedIn); + .animation-timing-function(ease-out); +} \ No newline at end of file diff --git a/media/less/vendor/animate/lightSpeedOut.less b/media/less/vendor/animate/lightSpeedOut.less new file mode 100644 index 0000000..627b701 --- /dev/null +++ b/media/less/vendor/animate/lightSpeedOut.less @@ -0,0 +1,24 @@ +@-webkit-keyframes lightSpeedOut { + 0% { -webkit-transform: translateX(0%) skewX(0deg); opacity: 1; } + 100% { -webkit-transform: translateX(100%) skewX(-30deg); opacity: 0; } +} + +@-moz-keyframes lightSpeedOut { + 0% { -moz-transform: translateX(0%) skewX(0deg); opacity: 1; } + 100% { -moz-transform: translateX(100%) skewX(-30deg); opacity: 0; } +} + +@-o-keyframes lightSpeedOut { + 0% { -o-transform: translateX(0%) skewX(0deg); opacity: 1; } + 100% { -o-transform: translateX(100%) skewX(-30deg); opacity: 0; } +} + +@keyframes lightSpeedOut { + 0% { transform: translateX(0%) skewX(0deg); opacity: 1; } + 100% { transform: translateX(100%) skewX(-30deg); opacity: 0; } +} + +.lightSpeedOut { + .animation-name(lightSpeedOut); + .animation-timing-function(ease-in); +} \ No newline at end of file diff --git a/media/less/vendor/animate/mixins.less b/media/less/vendor/animate/mixins.less new file mode 100644 index 0000000..b158533 --- /dev/null +++ b/media/less/vendor/animate/mixins.less @@ -0,0 +1,140 @@ + +.animation(@arguments) { + -webkit-animation: @arguments; + -moz-animation: @arguments; + -ms-animation: @arguments; + -o-animation: @arguments; + animation: @arguments; +} + +.animation-delay(@delay) { + -webkit-animation-delay: @delay; + -moz-animation-delay: @delay; + -ms-animation-delay: @delay; + -o-animation-delay: @delay; + animation-delay: @delay; +} + +.animation-duration(@duration) { + -webkit-animation-duration: @duration; + -moz-animation-duration: @duration; + -ms-animation-duration: @duration; + -o-animation-duration: @duration; + animation-duration: @duration; +} + +.animation-iteration-count(@num) { + -webkit-animation-iteration-count: @num; + -moz-animation-iteration-count: @num; + -ms-animation-iteration-count: @num; + -o-animation-iteration-count: @num; + animation-iteration-count: @num; +} + +.animation-fill-mode(@mode) { + -webkit-animation-fill-mode: @mode; + -moz-animation-fill-mode: @mode; + -ms-animation-fill-mode: @mode; + -o-animation-fill-mode: @mode; + animation-fill-mode: @mode; +} + +.animation-name(@name) { + -webkit-animation-name: @name; + -moz-animation-name: @name; + -ms-animation-name: @name; + -o-animation-name: @name; + animation-name: @name; +} + +.backface-visibility(@visibility) { + -webkit-backface-visibility: @visibility !important; + -moz-backface-visibility: @visibility !important; + -ms-backface-visibility: @visibility !important; + -o-backface-visibility: @visibility !important; + backface-visibility: @visibility !important; +} + +.animation-timing-function(@name) { + -webkit-animation-timing-function: @name; + -moz-animation-timing-function: @name; + -ms-animation-timing-function: @name; + -o-animation-timing-function: @name; + animation-timing-function: @name; +} + +.transform-origin(@origin) { + -webkit-transform-origin: @origin; + -moz-transform-origin: @origin; + -ms-transform-origin: @origin; + -o-transform-origin: @origin; + transform-origin: @origin; +} + + +// Transitions +.transition(@transition) { + -webkit-transition: @transition; + -moz-transition: @transition; + -ms-transition: @transition; + -o-transition: @transition; + transition: @transition; +} + +.transition-transform(@duration, @func) { + -webkit-transition: -webkit-transform @duration @func; + -moz-transition: -moz-transform @duration @func; + -ms-transition: -ms-transform @duration @func; + -o-transition: -o-transform @duration @func; + transition: transform @duration @func; +} + +.transition-delay(@delay) { + -webkit-transition-delay: @delay; + -moz-transition-delay: @delay; + -ms-transition-delay: @delay; + -o-transition-delay: @delay; + transition-delay: @delay; +} + +.transition-duration(@duration) { + -webkit-transition-duration: @duration; + -moz-transition-duration: @duration; + -ms-transition-duration: @duration; + -o-transition-duration: @duration; + transition-duration: @duration; +} + +.transform-rotate(@rotate) { + -webkit-transform: rotate(@rotate); + -moz-transform: rotate(@rotate); + -ms-transform: rotate(@rotate); + -o-transform: rotate(@rotate); + transform: rotate(@rotate); +} + +.transform-scale(@ratio) { + -webkit-transform: scale(@ratio); + -moz-transform: scale(@ratio); + -ms-transform: scale(@ratio); + -o-transform: scale(@ratio); + transform: scale(@ratio); +} + +.transform-translateX(@x) { + -webkit-transform: translateX(@x); + -moz-transform: translateX(@x); + -ms-transform: translateX(@x); + -o-transform: translateX(@x); + transform: translateX(@x); +} + +.transform-translateY(@y) { + -webkit-transform: translateY(@y); + -moz-transform: translateY(@y); + -ms-transform: translateY(@y); + -o-transform: translateY(@y); + transform: translateY(@y); +} + + diff --git a/media/less/vendor/animate/pulse.less b/media/less/vendor/animate/pulse.less new file mode 100644 index 0000000..cff84b6 --- /dev/null +++ b/media/less/vendor/animate/pulse.less @@ -0,0 +1,26 @@ +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ + +@-webkit-keyframes pulse { + 0% { -webkit-transform: scale(1); } + 50% { -webkit-transform: scale(1.1); } + 100% { -webkit-transform: scale(1); } +} +@-moz-keyframes pulse { + 0% { -moz-transform: scale(1); } + 50% { -moz-transform: scale(1.1); } + 100% { -moz-transform: scale(1); } +} +@-o-keyframes pulse { + 0% { -o-transform: scale(1); } + 50% { -o-transform: scale(1.1); } + 100% { -o-transform: scale(1); } +} +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); } +} + +.pulse { + .animation-name(pulse); +} \ No newline at end of file diff --git a/media/less/vendor/animate/rollIn.less b/media/less/vendor/animate/rollIn.less new file mode 100644 index 0000000..adf0d47 --- /dev/null +++ b/media/less/vendor/animate/rollIn.less @@ -0,0 +1,25 @@ +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ + +@-webkit-keyframes rollIn { + 0% { opacity: 0; -webkit-transform: translateX(-100%) rotate(-120deg); } + 100% { opacity: 1; -webkit-transform: translateX(0px) rotate(0deg); } +} + +@-moz-keyframes rollIn { + 0% { opacity: 0; -moz-transform: translateX(-100%) rotate(-120deg); } + 100% { opacity: 1; -moz-transform: translateX(0px) rotate(0deg); } +} + +@-o-keyframes rollIn { + 0% { opacity: 0; -o-transform: translateX(-100%) rotate(-120deg); } + 100% { opacity: 1; -o-transform: translateX(0px) rotate(0deg); } +} + +@keyframes rollIn { + 0% { opacity: 0; transform: translateX(-100%) rotate(-120deg); } + 100% { opacity: 1; transform: translateX(0px) rotate(0deg); } +} + +.rollIn { + .animation-name(rollIn); +} \ No newline at end of file diff --git a/media/less/vendor/animate/rollOut.less b/media/less/vendor/animate/rollOut.less new file mode 100644 index 0000000..b2970db --- /dev/null +++ b/media/less/vendor/animate/rollOut.less @@ -0,0 +1,53 @@ +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ + +@-webkit-keyframes rollOut { + 0% { + opacity: 1; + -webkit-transform: translateX(0px) rotate(0deg); + } + + 100% { + opacity: 0; + -webkit-transform: translateX(100%) rotate(120deg); + } +} + +@-moz-keyframes rollOut { + 0% { + opacity: 1; + -moz-transform: translateX(0px) rotate(0deg); + } + + 100% { + opacity: 0; + -moz-transform: translateX(100%) rotate(120deg); + } +} + +@-o-keyframes rollOut { + 0% { + opacity: 1; + -o-transform: translateX(0px) rotate(0deg); + } + + 100% { + opacity: 0; + -o-transform: translateX(100%) rotate(120deg); + } +} + +@keyframes rollOut { + 0% { + opacity: 1; + transform: translateX(0px) rotate(0deg); + } + + 100% { + opacity: 0; + transform: translateX(100%) rotate(120deg); + } +} + +.rollOut { + .animation-name(rollOut); +} \ No newline at end of file diff --git a/media/less/vendor/animate/rotateIn.less b/media/less/vendor/animate/rotateIn.less new file mode 100644 index 0000000..3ae1aa0 --- /dev/null +++ b/media/less/vendor/animate/rotateIn.less @@ -0,0 +1,56 @@ +@-webkit-keyframes rotateIn { + 0% { + -webkit-transform-origin: center center; + -webkit-transform: rotate(-200deg); + opacity: 0; + } + + 100% { + -webkit-transform-origin: center center; + -webkit-transform: rotate(0); + opacity: 1; + } +} +@-moz-keyframes rotateIn { + 0% { + -moz-transform-origin: center center; + -moz-transform: rotate(-200deg); + opacity: 0; + } + + 100% { + -moz-transform-origin: center center; + -moz-transform: rotate(0); + opacity: 1; + } +} +@-o-keyframes rotateIn { + 0% { + -o-transform-origin: center center; + -o-transform: rotate(-200deg); + opacity: 0; + } + + 100% { + -o-transform-origin: center center; + -o-transform: rotate(0); + opacity: 1; + } +} +@keyframes rotateIn { + 0% { + transform-origin: center center; + transform: rotate(-200deg); + opacity: 0; + } + + 100% { + transform-origin: center center; + transform: rotate(0); + opacity: 1; + } +} + +.rotateIn { + .animation-name(rotateIn); +} \ No newline at end of file diff --git a/media/less/vendor/animate/rotateInDownLeft.less b/media/less/vendor/animate/rotateInDownLeft.less new file mode 100644 index 0000000..1152a33 --- /dev/null +++ b/media/less/vendor/animate/rotateInDownLeft.less @@ -0,0 +1,59 @@ +@-webkit-keyframes rotateInDownLeft { + 0% { + -webkit-transform-origin: left bottom; + -webkit-transform: rotate(-90deg); + opacity: 0; + } + + 100% { + -webkit-transform-origin: left bottom; + -webkit-transform: rotate(0); + opacity: 1; + } +} + +@-moz-keyframes rotateInDownLeft { + 0% { + -moz-transform-origin: left bottom; + -moz-transform: rotate(-90deg); + opacity: 0; + } + + 100% { + -moz-transform-origin: left bottom; + -moz-transform: rotate(0); + opacity: 1; + } +} + +@-o-keyframes rotateInDownLeft { + 0% { + -o-transform-origin: left bottom; + -o-transform: rotate(-90deg); + opacity: 0; + } + + 100% { + -o-transform-origin: left bottom; + -o-transform: rotate(0); + opacity: 1; + } +} + +@keyframes rotateInDownLeft { + 0% { + transform-origin: left bottom; + transform: rotate(-90deg); + opacity: 0; + } + + 100% { + transform-origin: left bottom; + transform: rotate(0); + opacity: 1; + } +} + +.rotateInDownLeft { + .animation-name(rotateInDownLeft); +} \ No newline at end of file diff --git a/media/less/vendor/animate/rotateInDownRight.less b/media/less/vendor/animate/rotateInDownRight.less new file mode 100644 index 0000000..8655bcf --- /dev/null +++ b/media/less/vendor/animate/rotateInDownRight.less @@ -0,0 +1,59 @@ +@-webkit-keyframes rotateInDownRight { + 0% { + -webkit-transform-origin: right bottom; + -webkit-transform: rotate(90deg); + opacity: 0; + } + + 100% { + -webkit-transform-origin: right bottom; + -webkit-transform: rotate(0); + opacity: 1; + } +} + +@-moz-keyframes rotateInDownRight { + 0% { + -moz-transform-origin: right bottom; + -moz-transform: rotate(90deg); + opacity: 0; + } + + 100% { + -moz-transform-origin: right bottom; + -moz-transform: rotate(0); + opacity: 1; + } +} + +@-o-keyframes rotateInDownRight { + 0% { + -o-transform-origin: right bottom; + -o-transform: rotate(90deg); + opacity: 0; + } + + 100% { + -o-transform-origin: right bottom; + -o-transform: rotate(0); + opacity: 1; + } +} + +@keyframes rotateInDownRight { + 0% { + transform-origin: right bottom; + transform: rotate(90deg); + opacity: 0; + } + + 100% { + transform-origin: right bottom; + transform: rotate(0); + opacity: 1; + } +} + +.rotateInDownRight { + .animation-name(rotateInDownRight); +} \ No newline at end of file diff --git a/media/less/vendor/animate/rotateInUpLeft.less b/media/less/vendor/animate/rotateInUpLeft.less new file mode 100644 index 0000000..6f36344 --- /dev/null +++ b/media/less/vendor/animate/rotateInUpLeft.less @@ -0,0 +1,59 @@ +@-webkit-keyframes rotateInUpLeft { + 0% { + -webkit-transform-origin: left bottom; + -webkit-transform: rotate(90deg); + opacity: 0; + } + + 100% { + -webkit-transform-origin: left bottom; + -webkit-transform: rotate(0); + opacity: 1; + } +} + +@-moz-keyframes rotateInUpLeft { + 0% { + -moz-transform-origin: left bottom; + -moz-transform: rotate(90deg); + opacity: 0; + } + + 100% { + -moz-transform-origin: left bottom; + -moz-transform: rotate(0); + opacity: 1; + } +} + +@-o-keyframes rotateInUpLeft { + 0% { + -o-transform-origin: left bottom; + -o-transform: rotate(90deg); + opacity: 0; + } + + 100% { + -o-transform-origin: left bottom; + -o-transform: rotate(0); + opacity: 1; + } +} + +@keyframes rotateInUpLeft { + 0% { + transform-origin: left bottom; + transform: rotate(90deg); + opacity: 0; + } + + 100% { + transform-origin: left bottom; + transform: rotate(0); + opacity: 1; + } +} + +.rotateInUpLeft { + .animation-name(rotateInUpLeft); +} \ No newline at end of file diff --git a/media/less/vendor/animate/rotateInUpRight.less b/media/less/vendor/animate/rotateInUpRight.less new file mode 100644 index 0000000..5472891 --- /dev/null +++ b/media/less/vendor/animate/rotateInUpRight.less @@ -0,0 +1,59 @@ +@-webkit-keyframes rotateInUpRight { + 0% { + -webkit-transform-origin: right bottom; + -webkit-transform: rotate(-90deg); + opacity: 0; + } + + 100% { + -webkit-transform-origin: right bottom; + -webkit-transform: rotate(0); + opacity: 1; + } +} + +@-moz-keyframes rotateInUpRight { + 0% { + -moz-transform-origin: right bottom; + -moz-transform: rotate(-90deg); + opacity: 0; + } + + 100% { + -moz-transform-origin: right bottom; + -moz-transform: rotate(0); + opacity: 1; + } +} + +@-o-keyframes rotateInUpRight { + 0% { + -o-transform-origin: right bottom; + -o-transform: rotate(-90deg); + opacity: 0; + } + + 100% { + -o-transform-origin: right bottom; + -o-transform: rotate(0); + opacity: 1; + } +} + +@keyframes rotateInUpRight { + 0% { + transform-origin: right bottom; + transform: rotate(-90deg); + opacity: 0; + } + + 100% { + transform-origin: right bottom; + transform: rotate(0); + opacity: 1; + } +} + +.rotateInUpRight { + .animation-name(rotateInUpRight); +} \ No newline at end of file diff --git a/media/less/vendor/animate/rotateOut.less b/media/less/vendor/animate/rotateOut.less new file mode 100644 index 0000000..2d6372e --- /dev/null +++ b/media/less/vendor/animate/rotateOut.less @@ -0,0 +1,59 @@ +@-webkit-keyframes rotateOut { + 0% { + -webkit-transform-origin: center center; + -webkit-transform: rotate(0); + opacity: 1; + } + + 100% { + -webkit-transform-origin: center center; + -webkit-transform: rotate(200deg); + opacity: 0; + } +} + +@-moz-keyframes rotateOut { + 0% { + -moz-transform-origin: center center; + -moz-transform: rotate(0); + opacity: 1; + } + + 100% { + -moz-transform-origin: center center; + -moz-transform: rotate(200deg); + opacity: 0; + } +} + +@-o-keyframes rotateOut { + 0% { + -o-transform-origin: center center; + -o-transform: rotate(0); + opacity: 1; + } + + 100% { + -o-transform-origin: center center; + -o-transform: rotate(200deg); + opacity: 0; + } +} + +@keyframes rotateOut { + 0% { + transform-origin: center center; + transform: rotate(0); + opacity: 1; + } + + 100% { + transform-origin: center center; + transform: rotate(200deg); + opacity: 0; + } +} + +.rotateOut { + .animation-name(rotateOut); +} \ No newline at end of file diff --git a/media/less/vendor/animate/rotateOutDownLeft.less b/media/less/vendor/animate/rotateOutDownLeft.less new file mode 100644 index 0000000..cb706d7 --- /dev/null +++ b/media/less/vendor/animate/rotateOutDownLeft.less @@ -0,0 +1,59 @@ +@-webkit-keyframes rotateOutDownLeft { + 0% { + -webkit-transform-origin: left bottom; + -webkit-transform: rotate(0); + opacity: 1; + } + + 100% { + -webkit-transform-origin: left bottom; + -webkit-transform: rotate(90deg); + opacity: 0; + } +} + +@-moz-keyframes rotateOutDownLeft { + 0% { + -moz-transform-origin: left bottom; + -moz-transform: rotate(0); + opacity: 1; + } + + 100% { + -moz-transform-origin: left bottom; + -moz-transform: rotate(90deg); + opacity: 0; + } +} + +@-o-keyframes rotateOutDownLeft { + 0% { + -o-transform-origin: left bottom; + -o-transform: rotate(0); + opacity: 1; + } + + 100% { + -o-transform-origin: left bottom; + -o-transform: rotate(90deg); + opacity: 0; + } +} + +@keyframes rotateOutDownLeft { + 0% { + transform-origin: left bottom; + transform: rotate(0); + opacity: 1; + } + + 100% { + transform-origin: left bottom; + transform: rotate(90deg); + opacity: 0; + } +} + +.rotateOutDownLeft { + .animation-name(rotateOutDownLeft); +} \ No newline at end of file diff --git a/media/less/vendor/animate/rotateOutDownRight.less b/media/less/vendor/animate/rotateOutDownRight.less new file mode 100644 index 0000000..64b50b3 --- /dev/null +++ b/media/less/vendor/animate/rotateOutDownRight.less @@ -0,0 +1,59 @@ +@-webkit-keyframes rotateOutDownRight { + 0% { + -webkit-transform-origin: right bottom; + -webkit-transform: rotate(0); + opacity: 1; + } + + 100% { + -webkit-transform-origin: right bottom; + -webkit-transform: rotate(-90deg); + opacity: 0; + } +} + +@-moz-keyframes rotateOutDownRight { + 0% { + -moz-transform-origin: right bottom; + -moz-transform: rotate(0); + opacity: 1; + } + + 100% { + -moz-transform-origin: right bottom; + -moz-transform: rotate(-90deg); + opacity: 0; + } +} + +@-o-keyframes rotateOutDownRight { + 0% { + -o-transform-origin: right bottom; + -o-transform: rotate(0); + opacity: 1; + } + + 100% { + -o-transform-origin: right bottom; + -o-transform: rotate(-90deg); + opacity: 0; + } +} + +@keyframes rotateOutDownRight { + 0% { + transform-origin: right bottom; + transform: rotate(0); + opacity: 1; + } + + 100% { + transform-origin: right bottom; + transform: rotate(-90deg); + opacity: 0; + } +} + +.rotateOutDownRight { + .animation-name(rotateOutDownRight); +} \ No newline at end of file diff --git a/media/less/vendor/animate/rotateOutUpLeft.less b/media/less/vendor/animate/rotateOutUpLeft.less new file mode 100644 index 0000000..51e8393 --- /dev/null +++ b/media/less/vendor/animate/rotateOutUpLeft.less @@ -0,0 +1,59 @@ +@-webkit-keyframes rotateOutUpLeft { + 0% { + -webkit-transform-origin: left bottom; + -webkit-transform: rotate(0); + opacity: 1; + } + + 100% { + -webkit-transform-origin: left bottom; + -webkit-transform: rotate(-90deg); + opacity: 0; + } +} + +@-moz-keyframes rotateOutUpLeft { + 0% { + -moz-transform-origin: left bottom; + -moz-transform: rotate(0); + opacity: 1; + } + + 100% { + -moz-transform-origin: left bottom; + -moz-transform: rotate(-90deg); + opacity: 0; + } +} + +@-o-keyframes rotateOutUpLeft { + 0% { + -o-transform-origin: left bottom; + -o-transform: rotate(0); + opacity: 1; + } + + 100% { + -o-transform-origin: left bottom; + -o-transform: rotate(-90deg); + opacity: 0; + } +} + +@keyframes rotateOutUpLeft { + 0% { + transform-origin: left bottom; + transform: rotate(0); + opacity: 1; + } + + 100% { + -transform-origin: left bottom; + -transform: rotate(-90deg); + opacity: 0; + } +} + +.rotateOutUpLeft { + .animation-name(rotateOutUpLeft); +} \ No newline at end of file diff --git a/media/less/vendor/animate/rotateOutUpRight.less b/media/less/vendor/animate/rotateOutUpRight.less new file mode 100644 index 0000000..5f63f89 --- /dev/null +++ b/media/less/vendor/animate/rotateOutUpRight.less @@ -0,0 +1,59 @@ +@-webkit-keyframes rotateOutUpRight { + 0% { + -webkit-transform-origin: right bottom; + -webkit-transform: rotate(0); + opacity: 1; + } + + 100% { + -webkit-transform-origin: right bottom; + -webkit-transform: rotate(90deg); + opacity: 0; + } +} + +@-moz-keyframes rotateOutUpRight { + 0% { + -moz-transform-origin: right bottom; + -moz-transform: rotate(0); + opacity: 1; + } + + 100% { + -moz-transform-origin: right bottom; + -moz-transform: rotate(90deg); + opacity: 0; + } +} + +@-o-keyframes rotateOutUpRight { + 0% { + -o-transform-origin: right bottom; + -o-transform: rotate(0); + opacity: 1; + } + + 100% { + -o-transform-origin: right bottom; + -o-transform: rotate(90deg); + opacity: 0; + } +} + +@keyframes rotateOutUpRight { + 0% { + transform-origin: right bottom; + transform: rotate(0); + opacity: 1; + } + + 100% { + transform-origin: right bottom; + transform: rotate(90deg); + opacity: 0; + } +} + +.rotateOutUpRight { + .animation-name(rotateOutUpRight); +} \ No newline at end of file diff --git a/media/less/vendor/animate/shake.less b/media/less/vendor/animate/shake.less new file mode 100644 index 0000000..daf4d96 --- /dev/null +++ b/media/less/vendor/animate/shake.less @@ -0,0 +1,27 @@ +@-webkit-keyframes shake { + 0%, 100% {-webkit-transform: translateX(0);} + 10%, 30%, 50%, 70%, 90% {-webkit-transform: translateX(-10px);} + 20%, 40%, 60%, 80% {-webkit-transform: translateX(10px);} +} + +@-moz-keyframes shake { + 0%, 100% {-moz-transform: translateX(0);} + 10%, 30%, 50%, 70%, 90% {-moz-transform: translateX(-10px);} + 20%, 40%, 60%, 80% {-moz-transform: translateX(10px);} +} + +@-o-keyframes shake { + 0%, 100% {-o-transform: translateX(0);} + 10%, 30%, 50%, 70%, 90% {-o-transform: translateX(-10px);} + 20%, 40%, 60%, 80% {-o-transform: translateX(10px);} +} + +@keyframes shake { + 0%, 100% {transform: translateX(0);} + 10%, 30%, 50%, 70%, 90% {transform: translateX(-10px);} + 20%, 40%, 60%, 80% {transform: translateX(10px);} +} + +.shake { + .animation-name(shake); +} \ No newline at end of file diff --git a/media/less/vendor/animate/swing.less b/media/less/vendor/animate/swing.less new file mode 100644 index 0000000..4fc88f8 --- /dev/null +++ b/media/less/vendor/animate/swing.less @@ -0,0 +1,37 @@ +@-webkit-keyframes swing { + 20%, 40%, 60%, 80%, 100% { -webkit-transform-origin: top center; } + 20% { -webkit-transform: rotate(15deg); } + 40% { -webkit-transform: rotate(-10deg); } + 60% { -webkit-transform: rotate(5deg); } + 80% { -webkit-transform: rotate(-5deg); } + 100% { -webkit-transform: rotate(0deg); } +} + +@-moz-keyframes swing { + 20% { -moz-transform: rotate(15deg); } + 40% { -moz-transform: rotate(-10deg); } + 60% { -moz-transform: rotate(5deg); } + 80% { -moz-transform: rotate(-5deg); } + 100% { -moz-transform: rotate(0deg); } +} + +@-o-keyframes swing { + 20% { -o-transform: rotate(15deg); } + 40% { -o-transform: rotate(-10deg); } + 60% { -o-transform: rotate(5deg); } + 80% { -o-transform: rotate(-5deg); } + 100% { -o-transform: rotate(0deg); } +} + +@keyframes swing { + 20% { transform: rotate(15deg); } + 40% { transform: rotate(-10deg); } + 60% { transform: rotate(5deg); } + 80% { transform: rotate(-5deg); } + 100% { transform: rotate(0deg); } +} + +.swing { + .transform-origin(top center); + .animation-name(swing); +} \ No newline at end of file diff --git a/media/less/vendor/animate/tada.less b/media/less/vendor/animate/tada.less new file mode 100644 index 0000000..1839c25 --- /dev/null +++ b/media/less/vendor/animate/tada.less @@ -0,0 +1,35 @@ +@-webkit-keyframes tada { + 0% {-webkit-transform: scale(1);} + 10%, 20% {-webkit-transform: scale(0.9) rotate(-3deg);} + 30%, 50%, 70%, 90% {-webkit-transform: scale(1.1) rotate(3deg);} + 40%, 60%, 80% {-webkit-transform: scale(1.1) rotate(-3deg);} + 100% {-webkit-transform: scale(1) rotate(0);} +} + +@-moz-keyframes tada { + 0% {-moz-transform: scale(1);} + 10%, 20% {-moz-transform: scale(0.9) rotate(-3deg);} + 30%, 50%, 70%, 90% {-moz-transform: scale(1.1) rotate(3deg);} + 40%, 60%, 80% {-moz-transform: scale(1.1) rotate(-3deg);} + 100% {-moz-transform: scale(1) rotate(0);} +} + +@-o-keyframes tada { + 0% {-o-transform: scale(1);} + 10%, 20% {-o-transform: scale(0.9) rotate(-3deg);} + 30%, 50%, 70%, 90% {-o-transform: scale(1.1) rotate(3deg);} + 40%, 60%, 80% {-o-transform: scale(1.1) rotate(-3deg);} + 100% {-o-transform: scale(1) rotate(0);} +} + +@keyframes tada { + 0% {transform: scale(1);} + 10%, 20% {transform: scale(0.9) rotate(-3deg);} + 30%, 50%, 70%, 90% {transform: scale(1.1) rotate(3deg);} + 40%, 60%, 80% {transform: scale(1.1) rotate(-3deg);} + 100% {transform: scale(1) rotate(0);} +} + +.tada { + .animation-name(tada); +} \ No newline at end of file diff --git a/media/less/vendor/animate/wobble.less b/media/less/vendor/animate/wobble.less new file mode 100644 index 0000000..b8589ec --- /dev/null +++ b/media/less/vendor/animate/wobble.less @@ -0,0 +1,45 @@ +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ + +@-webkit-keyframes wobble { + 0% { -webkit-transform: translateX(0%); } + 15% { -webkit-transform: translateX(-25%) rotate(-5deg); } + 30% { -webkit-transform: translateX(20%) rotate(3deg); } + 45% { -webkit-transform: translateX(-15%) rotate(-3deg); } + 60% { -webkit-transform: translateX(10%) rotate(2deg); } + 75% { -webkit-transform: translateX(-5%) rotate(-1deg); } + 100% { -webkit-transform: translateX(0%); } +} + +@-moz-keyframes wobble { + 0% { -moz-transform: translateX(0%); } + 15% { -moz-transform: translateX(-25%) rotate(-5deg); } + 30% { -moz-transform: translateX(20%) rotate(3deg); } + 45% { -moz-transform: translateX(-15%) rotate(-3deg); } + 60% { -moz-transform: translateX(10%) rotate(2deg); } + 75% { -moz-transform: translateX(-5%) rotate(-1deg); } + 100% { -moz-transform: translateX(0%); } +} + +@-o-keyframes wobble { + 0% { -o-transform: translateX(0%); } + 15% { -o-transform: translateX(-25%) rotate(-5deg); } + 30% { -o-transform: translateX(20%) rotate(3deg); } + 45% { -o-transform: translateX(-15%) rotate(-3deg); } + 60% { -o-transform: translateX(10%) rotate(2deg); } + 75% { -o-transform: translateX(-5%) rotate(-1deg); } + 100% { -o-transform: translateX(0%); } +} + +@keyframes wobble { + 0% { transform: translateX(0%); } + 15% { transform: translateX(-25%) rotate(-5deg); } + 30% { transform: translateX(20%) rotate(3deg); } + 45% { transform: translateX(-15%) rotate(-3deg); } + 60% { transform: translateX(10%) rotate(2deg); } + 75% { transform: translateX(-5%) rotate(-1deg); } + 100% { transform: translateX(0%); } +} + +.wobble { + .animation-name(wobble); +} \ No newline at end of file diff --git a/media/less/vendor/bootstrap/alerts.less b/media/less/vendor/bootstrap/alerts.less new file mode 100644 index 0000000..df070b8 --- /dev/null +++ b/media/less/vendor/bootstrap/alerts.less @@ -0,0 +1,68 @@ +// +// Alerts +// -------------------------------------------------- + + +// Base styles +// ------------------------- + +.alert { + padding: @alert-padding; + margin-bottom: @line-height-computed; + border: 1px solid transparent; + border-radius: @alert-border-radius; + + // Headings for larger alerts + h4 { + margin-top: 0; + // Specified for the h4 to prevent conflicts of changing @headings-color + color: inherit; + } + // Provide class for links that match alerts + .alert-link { + font-weight: @alert-link-font-weight; + } + + // Improve alignment and spacing of inner content + > p, + > ul { + margin-bottom: 0; + } + > p + p { + margin-top: 5px; + } +} + +// Dismissible alerts +// +// Expand the right padding and account for the close button's positioning. + +.alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0. +.alert-dismissible { + padding-right: (@alert-padding + 20); + + // Adjust close link position + .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; + } +} + +// Alternate styles +// +// Generate contextual modifier classes for colorizing the alert. + +.alert-success { + .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text); +} +.alert-info { + .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text); +} +.alert-warning { + .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text); +} +.alert-danger { + .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text); +} diff --git a/media/less/vendor/bootstrap/badges.less b/media/less/vendor/bootstrap/badges.less new file mode 100644 index 0000000..b27c405 --- /dev/null +++ b/media/less/vendor/bootstrap/badges.less @@ -0,0 +1,61 @@ +// +// Badges +// -------------------------------------------------- + + +// Base class +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: @font-size-small; + font-weight: @badge-font-weight; + color: @badge-color; + line-height: @badge-line-height; + vertical-align: baseline; + white-space: nowrap; + text-align: center; + background-color: @badge-bg; + border-radius: @badge-border-radius; + + // Empty badges collapse automatically (not available in IE8) + &:empty { + display: none; + } + + // Quick fix for badges in buttons + .btn & { + position: relative; + top: -1px; + } + .btn-xs & { + top: 0; + padding: 1px 5px; + } + + // Hover state, but only for links + a& { + &:hover, + &:focus { + color: @badge-link-hover-color; + text-decoration: none; + cursor: pointer; + } + } + + // Account for badges in navs + .list-group-item.active > &, + .nav-pills > .active > a > & { + color: @badge-active-color; + background-color: @badge-active-bg; + } + .list-group-item > & { + float: right; + } + .list-group-item > & + & { + margin-right: 5px; + } + .nav-pills > li > a > & { + margin-left: 3px; + } +} diff --git a/media/less/vendor/bootstrap/bootstrap.less b/media/less/vendor/bootstrap/bootstrap.less new file mode 100644 index 0000000..181b85f --- /dev/null +++ b/media/less/vendor/bootstrap/bootstrap.less @@ -0,0 +1,50 @@ +// Core variables and mixins +@import "variables.less"; +@import "mixins.less"; + +// Reset and dependencies +@import "normalize.less"; +// @import "print.less"; +// @import "glyphicons.less"; + +// Core CSS +@import "scaffolding.less"; +@import "type.less"; +// @import "code.less"; +@import "grid.less"; +// @import "tables.less"; +@import "forms.less"; +@import "buttons.less"; + +// Components +@import "component-animations.less"; +@import "dropdowns.less"; +@import "button-groups.less"; +@import "input-groups.less"; +// @import "navs.less"; +// @import "navbar.less"; +// @import "breadcrumbs.less"; +// @import "pagination.less"; +// @import "pager.less"; +@import "labels.less"; +@import "badges.less"; +// @import "jumbotron.less"; +@import "thumbnails.less"; +@import "alerts.less"; +@import "progress-bars.less"; +@import "media.less"; +// @import "list-group.less"; +// @import "panels.less"; +@import "responsive-embed.less"; +// @import "wells.less"; +@import "close.less"; + +// Components w/ JavaScript +@import "modals.less"; +@import "tooltip.less"; +@import "popovers.less"; +// @import "carousel.less"; + +// Utility classes +@import "utilities.less"; +@import "responsive-utilities.less"; \ No newline at end of file diff --git a/media/less/vendor/bootstrap/breadcrumbs.less b/media/less/vendor/bootstrap/breadcrumbs.less new file mode 100644 index 0000000..cb01d50 --- /dev/null +++ b/media/less/vendor/bootstrap/breadcrumbs.less @@ -0,0 +1,26 @@ +// +// Breadcrumbs +// -------------------------------------------------- + + +.breadcrumb { + padding: @breadcrumb-padding-vertical @breadcrumb-padding-horizontal; + margin-bottom: @line-height-computed; + list-style: none; + background-color: @breadcrumb-bg; + border-radius: @border-radius-base; + + > li { + display: inline-block; + + + li:before { + content: "@{breadcrumb-separator}\00a0"; // Unicode space added since inline-block means non-collapsing white-space + padding: 0 5px; + color: @breadcrumb-color; + } + } + + > .active { + color: @breadcrumb-active-color; + } +} diff --git a/media/less/vendor/bootstrap/button-groups.less b/media/less/vendor/bootstrap/button-groups.less new file mode 100644 index 0000000..f84febb --- /dev/null +++ b/media/less/vendor/bootstrap/button-groups.less @@ -0,0 +1,243 @@ +// +// Button groups +// -------------------------------------------------- + +// Make the div behave like a button +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; // match .btn alignment given font-size hack above + > .btn { + position: relative; + float: left; + // Bring the "active" button to the front + &:hover, + &:focus, + &:active, + &.active { + z-index: 2; + } + } +} + +// Prevent double borders when buttons are next to each other +.btn-group { + .btn + .btn, + .btn + .btn-group, + .btn-group + .btn, + .btn-group + .btn-group { + margin-left: -1px; + } +} + +// Optional: Group multiple button groups together for a toolbar +.btn-toolbar { + margin-left: -5px; // Offset the first child's margin + &:extend(.clearfix all); + + .btn-group, + .input-group { + float: left; + } + > .btn, + > .btn-group, + > .input-group { + margin-left: 5px; + } +} + +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} + +// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match +.btn-group > .btn:first-child { + margin-left: 0; + &:not(:last-child):not(.dropdown-toggle) { + .border-right-radius(0); + } +} +// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + .border-left-radius(0); +} + +// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group) +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child { + > .btn:last-child, + > .dropdown-toggle { + .border-right-radius(0); + } +} +.btn-group > .btn-group:last-child > .btn:first-child { + .border-left-radius(0); +} + +// On active and open, don't show outline +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + + +// Sizing +// +// Remix the default button sizing classes into new ones for easier manipulation. + +.btn-group-xs > .btn { &:extend(.btn-xs); } +.btn-group-sm > .btn { &:extend(.btn-sm); } +.btn-group-lg > .btn { &:extend(.btn-lg); } + + +// Split button dropdowns +// ---------------------- + +// Give the line between buttons some depth +.btn-group > .btn + .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-left: 12px; + padding-right: 12px; +} + +// The clickable button for toggling the menu +// Remove the gradient and set the same inset shadow as the :active state +.btn-group.open .dropdown-toggle { + .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + + // Show no shadow for `.btn-link` since it has no other button styles. + &.btn-link { + .box-shadow(none); + } +} + + +// Reposition the caret +.btn .caret { + margin-left: 0; +} +// Carets in other button sizes +.btn-lg .caret { + border-width: @caret-width-large @caret-width-large 0; + border-bottom-width: 0; +} +// Upside down carets for .dropup +.dropup .btn-lg .caret { + border-width: 0 @caret-width-large @caret-width-large; +} + + +// Vertical button groups +// ---------------------- + +.btn-group-vertical { + > .btn, + > .btn-group, + > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; + } + + // Clear floats so dropdown menus can be properly placed + > .btn-group { + &:extend(.clearfix all); + > .btn { + float: none; + } + } + + > .btn + .btn, + > .btn + .btn-group, + > .btn-group + .btn, + > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; + } +} + +.btn-group-vertical > .btn { + &:not(:first-child):not(:last-child) { + border-radius: 0; + } + &:first-child:not(:last-child) { + border-top-right-radius: @border-radius-base; + .border-bottom-radius(0); + } + &:last-child:not(:first-child) { + border-bottom-left-radius: @border-radius-base; + .border-top-radius(0); + } +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) { + > .btn:last-child, + > .dropdown-toggle { + .border-bottom-radius(0); + } +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + .border-top-radius(0); +} + + +// Justified button groups +// ---------------------- + +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; + > .btn, + > .btn-group { + float: none; + display: table-cell; + width: 1%; + } + > .btn-group .btn { + width: 100%; + } + + > .btn-group .dropdown-menu { + left: auto; + } +} + + +// Checkbox and radio options +// +// In order to support the browser's form validation feedback, powered by the +// `required` attribute, we have to "hide" the inputs via `clip`. We cannot use +// `display: none;` or `visibility: hidden;` as that also hides the popover. +// Simply visually hiding the inputs via `opacity` would leave them clickable in +// certain cases which is prevented by using `clip` and `pointer-events`. +// This way, we ensure a DOM element is visible to position the popover from. +// +// See https://github.com/twbs/bootstrap/pull/12794 and +// https://github.com/twbs/bootstrap/pull/14559 for more information. + +[data-toggle="buttons"] { + > .btn, + > .btn-group > .btn { + input[type="radio"], + input[type="checkbox"] { + position: absolute; + clip: rect(0,0,0,0); + pointer-events: none; + } + } +} diff --git a/media/less/vendor/bootstrap/buttons.less b/media/less/vendor/bootstrap/buttons.less new file mode 100644 index 0000000..40553c6 --- /dev/null +++ b/media/less/vendor/bootstrap/buttons.less @@ -0,0 +1,160 @@ +// +// Buttons +// -------------------------------------------------- + + +// Base styles +// -------------------------------------------------- + +.btn { + display: inline-block; + margin-bottom: 0; // For input.btn + font-weight: @btn-font-weight; + text-align: center; + vertical-align: middle; + touch-action: manipulation; + cursor: pointer; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid transparent; + white-space: nowrap; + .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @border-radius-base); + .user-select(none); + + &, + &:active, + &.active { + &:focus, + &.focus { + .tab-focus(); + } + } + + &:hover, + &:focus, + &.focus { + color: @btn-default-color; + text-decoration: none; + } + + &:active, + &.active { + outline: 0; + background-image: none; + .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + } + + &.disabled, + &[disabled], + fieldset[disabled] & { + cursor: @cursor-disabled; + pointer-events: none; // Future-proof disabling of clicks + .opacity(.65); + .box-shadow(none); + } +} + + +// Alternate buttons +// -------------------------------------------------- + +.btn-default { + .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border); +} +.btn-primary { + .button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border); +} +// Success appears as green +.btn-success { + .button-variant(@btn-success-color; @btn-success-bg; @btn-success-border); +} +// Info appears as blue-green +.btn-info { + .button-variant(@btn-info-color; @btn-info-bg; @btn-info-border); +} +// Warning appears as orange +.btn-warning { + .button-variant(@btn-warning-color; @btn-warning-bg; @btn-warning-border); +} +// Danger and error appear as red +.btn-danger { + .button-variant(@btn-danger-color; @btn-danger-bg; @btn-danger-border); +} + + +// Link buttons +// ------------------------- + +// Make a button look and behave like a link +.btn-link { + color: @link-color; + font-weight: normal; + border-radius: 0; + + &, + &:active, + &.active, + &[disabled], + fieldset[disabled] & { + background-color: transparent; + .box-shadow(none); + } + &, + &:hover, + &:focus, + &:active { + border-color: transparent; + } + &:hover, + &:focus { + color: @link-hover-color; + text-decoration: underline; + background-color: transparent; + } + &[disabled], + fieldset[disabled] & { + &:hover, + &:focus { + color: @btn-link-disabled-color; + text-decoration: none; + } + } +} + + +// Button Sizes +// -------------------------------------------------- + +.btn-lg { + // line-height: ensure even-numbered height of button next to large input + .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large); +} +.btn-sm { + // line-height: ensure proper height of button next to small input + .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small); +} +.btn-xs { + .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @border-radius-small); +} + + +// Block button +// -------------------------------------------------- + +.btn-block { + display: block; + width: 100%; +} + +// Vertically space out multiple block buttons +.btn-block + .btn-block { + margin-top: 5px; +} + +// Specificity overrides +input[type="submit"], +input[type="reset"], +input[type="button"] { + &.btn-block { + width: 100%; + } +} diff --git a/media/less/vendor/bootstrap/carousel.less b/media/less/vendor/bootstrap/carousel.less new file mode 100644 index 0000000..a28e397 --- /dev/null +++ b/media/less/vendor/bootstrap/carousel.less @@ -0,0 +1,268 @@ +// +// Carousel +// -------------------------------------------------- + + +// Wrapper for the slide container and indicators +.carousel { + position: relative; +} + +.carousel-inner { + position: relative; + overflow: hidden; + width: 100%; + + > .item { + display: none; + position: relative; + .transition(.6s ease-in-out left); + + // Account for jankitude on images + > img, + > a > img { + &:extend(.img-responsive); + line-height: 1; + } + + // WebKit CSS3 transforms for supported devices + @media all and (transform-3d), (-webkit-transform-3d) { + .transition-transform(~'0.6s ease-in-out'); + .backface-visibility(~'hidden'); + .perspective(1000); + + &.next, + &.active.right { + .translate3d(100%, 0, 0); + left: 0; + } + &.prev, + &.active.left { + .translate3d(-100%, 0, 0); + left: 0; + } + &.next.left, + &.prev.right, + &.active { + .translate3d(0, 0, 0); + left: 0; + } + } + } + + > .active, + > .next, + > .prev { + display: block; + } + + > .active { + left: 0; + } + + > .next, + > .prev { + position: absolute; + top: 0; + width: 100%; + } + + > .next { + left: 100%; + } + > .prev { + left: -100%; + } + > .next.left, + > .prev.right { + left: 0; + } + + > .active.left { + left: -100%; + } + > .active.right { + left: 100%; + } + +} + +// Left/right controls for nav +// --------------------------- + +.carousel-control { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: @carousel-control-width; + .opacity(@carousel-control-opacity); + font-size: @carousel-control-font-size; + color: @carousel-control-color; + text-align: center; + text-shadow: @carousel-text-shadow; + // We can't have this transition here because WebKit cancels the carousel + // animation if you trip this while in the middle of another animation. + + // Set gradients for backgrounds + &.left { + #gradient > .horizontal(@start-color: rgba(0,0,0,.5); @end-color: rgba(0,0,0,.0001)); + } + &.right { + left: auto; + right: 0; + #gradient > .horizontal(@start-color: rgba(0,0,0,.0001); @end-color: rgba(0,0,0,.5)); + } + + // Hover/focus state + &:hover, + &:focus { + outline: 0; + color: @carousel-control-color; + text-decoration: none; + .opacity(.9); + } + + // Toggles + .icon-prev, + .icon-next, + .glyphicon-chevron-left, + .glyphicon-chevron-right { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; + } + .icon-prev, + .glyphicon-chevron-left { + left: 50%; + margin-left: -10px; + } + .icon-next, + .glyphicon-chevron-right { + right: 50%; + margin-right: -10px; + } + .icon-prev, + .icon-next { + width: 20px; + height: 20px; + margin-top: -10px; + line-height: 1; + font-family: serif; + } + + + .icon-prev { + &:before { + content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039) + } + } + .icon-next { + &:before { + content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A) + } + } +} + +// Optional indicator pips +// +// Add an unordered list with the following class and add a list item for each +// slide your carousel holds. + +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + margin-left: -30%; + padding-left: 0; + list-style: none; + text-align: center; + + li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + border: 1px solid @carousel-indicator-border-color; + border-radius: 10px; + cursor: pointer; + + // IE8-9 hack for event handling + // + // Internet Explorer 8-9 does not support clicks on elements without a set + // `background-color`. We cannot use `filter` since that's not viewed as a + // background color by the browser. Thus, a hack is needed. + // + // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we + // set alpha transparency for the best results possible. + background-color: #000 \9; // IE8 + background-color: rgba(0,0,0,0); // IE9 + } + .active { + margin: 0; + width: 12px; + height: 12px; + background-color: @carousel-indicator-active-bg; + } +} + +// Optional captions +// ----------------------------- +// Hidden by default for smaller viewports +.carousel-caption { + position: absolute; + left: 15%; + right: 15%; + bottom: 20px; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: @carousel-caption-color; + text-align: center; + text-shadow: @carousel-text-shadow; + & .btn { + text-shadow: none; // No shadow for button elements in carousel-caption + } +} + + +// Scale up controls for tablets and up +@media screen and (min-width: @screen-sm-min) { + + // Scale up the controls a smidge + .carousel-control { + .glyphicon-chevron-left, + .glyphicon-chevron-right, + .icon-prev, + .icon-next { + width: 30px; + height: 30px; + margin-top: -15px; + font-size: 30px; + } + .glyphicon-chevron-left, + .icon-prev { + margin-left: -15px; + } + .glyphicon-chevron-right, + .icon-next { + margin-right: -15px; + } + } + + // Show and left align the captions + .carousel-caption { + left: 20%; + right: 20%; + padding-bottom: 30px; + } + + // Move up the indicators + .carousel-indicators { + bottom: 20px; + } +} diff --git a/media/less/vendor/bootstrap/close.less b/media/less/vendor/bootstrap/close.less new file mode 100644 index 0000000..9b4e74f --- /dev/null +++ b/media/less/vendor/bootstrap/close.less @@ -0,0 +1,33 @@ +// +// Close icons +// -------------------------------------------------- + + +.close { + float: right; + font-size: (@font-size-base * 1.5); + font-weight: @close-font-weight; + line-height: 1; + color: @close-color; + text-shadow: @close-text-shadow; + .opacity(.2); + + &:hover, + &:focus { + color: @close-color; + text-decoration: none; + cursor: pointer; + .opacity(.5); + } + + // Additional properties for button version + // iOS requires the button element instead of an anchor tag. + // If you want the anchor version, it requires `href="#"`. + button& { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; + } +} diff --git a/media/less/vendor/bootstrap/code.less b/media/less/vendor/bootstrap/code.less new file mode 100644 index 0000000..a08b4d4 --- /dev/null +++ b/media/less/vendor/bootstrap/code.less @@ -0,0 +1,69 @@ +// +// Code (inline and block) +// -------------------------------------------------- + + +// Inline and block code styles +code, +kbd, +pre, +samp { + font-family: @font-family-monospace; +} + +// Inline code +code { + padding: 2px 4px; + font-size: 90%; + color: @code-color; + background-color: @code-bg; + border-radius: @border-radius-base; +} + +// User input typically entered via keyboard +kbd { + padding: 2px 4px; + font-size: 90%; + color: @kbd-color; + background-color: @kbd-bg; + border-radius: @border-radius-small; + box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); + + kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + box-shadow: none; + } +} + +// Blocks of code +pre { + display: block; + padding: ((@line-height-computed - 1) / 2); + margin: 0 0 (@line-height-computed / 2); + font-size: (@font-size-base - 1); // 14px to 13px + line-height: @line-height-base; + word-break: break-all; + word-wrap: break-word; + color: @pre-color; + background-color: @pre-bg; + border: 1px solid @pre-border-color; + border-radius: @border-radius-base; + + // Account for some code outputs that place code tags in pre tags + code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; + } +} + +// Enable scrollable blocks of code +.pre-scrollable { + max-height: @pre-scrollable-max-height; + overflow-y: scroll; +} diff --git a/media/less/vendor/bootstrap/component-animations.less b/media/less/vendor/bootstrap/component-animations.less new file mode 100644 index 0000000..967715d --- /dev/null +++ b/media/less/vendor/bootstrap/component-animations.less @@ -0,0 +1,34 @@ +// +// Component animations +// -------------------------------------------------- + +// Heads up! +// +// We don't use the `.opacity()` mixin here since it causes a bug with text +// fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552. + +.fade { + opacity: 0; + .transition(opacity .15s linear); + &.in { + opacity: 1; + } +} + +.collapse { + display: none; + visibility: hidden; + + &.in { display: block; visibility: visible; } + tr&.in { display: table-row; } + tbody&.in { display: table-row-group; } +} + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + .transition-property(~"height, visibility"); + .transition-duration(.35s); + .transition-timing-function(ease); +} diff --git a/media/less/vendor/bootstrap/dropdowns.less b/media/less/vendor/bootstrap/dropdowns.less new file mode 100644 index 0000000..1e9b1e8 --- /dev/null +++ b/media/less/vendor/bootstrap/dropdowns.less @@ -0,0 +1,213 @@ +// +// Dropdown menus +// -------------------------------------------------- + + +// Dropdown arrow/caret +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: @caret-width-base solid; + border-right: @caret-width-base solid transparent; + border-left: @caret-width-base solid transparent; +} + +// The dropdown wrapper (div) +.dropdown { + position: relative; +} + +// Prevent the focus on the dropdown toggle when closing dropdowns +.dropdown-toggle:focus { + outline: 0; +} + +// The dropdown menu (ul) +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: @zindex-dropdown; + display: none; // none by default, but block on "open" of the menu + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; // override default ul + list-style: none; + font-size: @font-size-base; + text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) + background-color: @dropdown-bg; + border: 1px solid @dropdown-fallback-border; // IE8 fallback + border: 1px solid @dropdown-border; + border-radius: @border-radius-base; + .box-shadow(0 6px 12px rgba(0,0,0,.175)); + background-clip: padding-box; + + // Aligns the dropdown menu to right + // + // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]` + &.pull-right { + right: 0; + left: auto; + } + + // Dividers (basically an hr) within the dropdown + .divider { + .nav-divider(@dropdown-divider-bg); + } + + // Links within the dropdown menu + > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: @line-height-base; + color: @dropdown-link-color; + white-space: nowrap; // prevent links from randomly breaking onto new lines + } +} + +// Hover/Focus state +.dropdown-menu > li > a { + &:hover, + &:focus { + text-decoration: none; + color: @dropdown-link-hover-color; + background-color: @dropdown-link-hover-bg; + } +} + +// Active state +.dropdown-menu > .active > a { + &, + &:hover, + &:focus { + color: @dropdown-link-active-color; + text-decoration: none; + outline: 0; + background-color: @dropdown-link-active-bg; + } +} + +// Disabled state +// +// Gray out text and ensure the hover/focus state remains gray + +.dropdown-menu > .disabled > a { + &, + &:hover, + &:focus { + color: @dropdown-link-disabled-color; + } + + // Nuke hover/focus effects + &:hover, + &:focus { + text-decoration: none; + background-color: transparent; + background-image: none; // Remove CSS gradient + .reset-filter(); + cursor: @cursor-disabled; + } +} + +// Open state for the dropdown +.open { + // Show the menu + > .dropdown-menu { + display: block; + } + + // Remove the outline when :focus is triggered + > a { + outline: 0; + } +} + +// Menu positioning +// +// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown +// menu with the parent. +.dropdown-menu-right { + left: auto; // Reset the default from `.dropdown-menu` + right: 0; +} +// With v3, we enabled auto-flipping if you have a dropdown within a right +// aligned nav component. To enable the undoing of that, we provide an override +// to restore the default dropdown menu alignment. +// +// This is only for left-aligning a dropdown menu within a `.navbar-right` or +// `.pull-right` nav component. +.dropdown-menu-left { + left: 0; + right: auto; +} + +// Dropdown section headers +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: @font-size-small; + line-height: @line-height-base; + color: @dropdown-header-color; + white-space: nowrap; // as with > li > a +} + +// Backdrop to catch body clicks on mobile, etc. +.dropdown-backdrop { + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: (@zindex-dropdown - 10); +} + +// Right aligned dropdowns +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +// Allow for dropdowns to go bottom up (aka, dropup-menu) +// +// Just add .dropup after the standard .dropdown class and you're set, bro. +// TODO: abstract this so that the navbar fixed styles are not placed here? + +.dropup, +.navbar-fixed-bottom .dropdown { + // Reverse the caret + .caret { + border-top: 0; + border-bottom: @caret-width-base solid; + content: ""; + } + // Different positioning for bottom up menu + .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 2px; + } +} + + +// Component alignment +// +// Reiterate per navbar.less and the modified component alignment there. + +@media (min-width: @grid-float-breakpoint) { + .navbar-right { + .dropdown-menu { + .dropdown-menu-right(); + } + // Necessary for overrides of the default right aligned menu. + // Will remove come v4 in all likelihood. + .dropdown-menu-left { + .dropdown-menu-left(); + } + } +} diff --git a/media/less/vendor/bootstrap/forms.less b/media/less/vendor/bootstrap/forms.less new file mode 100644 index 0000000..085d9d4 --- /dev/null +++ b/media/less/vendor/bootstrap/forms.less @@ -0,0 +1,557 @@ +// +// Forms +// -------------------------------------------------- + + +// Normalize non-controls +// +// Restyle and baseline non-control form elements. + +fieldset { + padding: 0; + margin: 0; + border: 0; + // Chrome and Firefox set a `min-width: min-content;` on fieldsets, + // so we reset that to ensure it behaves more like a standard block element. + // See https://github.com/twbs/bootstrap/issues/12359. + min-width: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: @line-height-computed; + font-size: (@font-size-base * 1.5); + line-height: inherit; + color: @legend-color; + border: 0; + border-bottom: 1px solid @legend-border-color; +} + +label { + display: inline-block; + max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141) + margin-bottom: 5px; + font-weight: bold; +} + + +// Normalize form controls +// +// While most of our form styles require extra classes, some basic normalization +// is required to ensure optimum display with or without those classes to better +// address browser inconsistencies. + +// Override content-box in Normalize (* isn't specific enough) +input[type="search"] { + .box-sizing(border-box); +} + +// Position radios and checkboxes better +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; // IE8-9 + line-height: normal; +} + +// Set the height of file controls to match text inputs +input[type="file"] { + display: block; +} + +// Make range inputs behave like textual form controls +input[type="range"] { + display: block; + width: 100%; +} + +// Make multiple select elements height not fixed +select[multiple], +select[size] { + height: auto; +} + +// Focus for file, radio, and checkbox +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + .tab-focus(); +} + +// Adjust output element +output { + display: block; + padding-top: (@padding-base-vertical + 1); + font-size: @font-size-base; + line-height: @line-height-base; + color: @input-color; +} + + +// Common form controls +// +// Shared size and type resets for form controls. Apply `.form-control` to any +// of the following form controls: +// +// select +// textarea +// input[type="text"] +// input[type="password"] +// input[type="datetime"] +// input[type="datetime-local"] +// input[type="date"] +// input[type="month"] +// input[type="time"] +// input[type="week"] +// input[type="number"] +// input[type="email"] +// input[type="url"] +// input[type="search"] +// input[type="tel"] +// input[type="color"] + +.form-control { + display: block; + width: 100%; + height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border) + padding: @padding-base-vertical @padding-base-horizontal; + font-size: @font-size-base; + line-height: @line-height-base; + color: @input-color; + background-color: @input-bg; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid @input-border; + border-radius: @input-border-radius; + .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); + .transition(~"border-color ease-in-out .15s, box-shadow ease-in-out .15s"); + + // Customize the `:focus` state to imitate native WebKit styles. + .form-control-focus(); + + // Placeholder + .placeholder(); + + // Disabled and read-only inputs + // + // HTML5 says that controls under a fieldset > legend:first-child won't be + // disabled if the fieldset is disabled. Due to implementation difficulty, we + // don't honor that edge case; we style them as disabled anyway. + &[disabled], + &[readonly], + fieldset[disabled] & { + cursor: @cursor-disabled; + background-color: @input-bg-disabled; + opacity: 1; // iOS fix for unreadable disabled content + } + + // Reset height for `textarea`s + textarea& { + height: auto; + } +} + + +// Search inputs in iOS +// +// This overrides the extra rounded corners on search inputs in iOS so that our +// `.form-control` class can properly style them. Note that this cannot simply +// be added to `.form-control` as it's not specific enough. For details, see +// https://github.com/twbs/bootstrap/issues/11586. + +input[type="search"] { + -webkit-appearance: none; +} + + +// Special styles for iOS temporal inputs +// +// In Mobile Safari, setting `display: block` on temporal inputs causes the +// text within the input to become vertically misaligned. As a workaround, we +// set a pixel line-height that matches the given height of the input, but only +// for Safari. + +@media screen and (-webkit-min-device-pixel-ratio: 0) { + input[type="date"], + input[type="time"], + input[type="datetime-local"], + input[type="month"] { + line-height: @input-height-base; + } + input[type="date"].input-sm, + input[type="time"].input-sm, + input[type="datetime-local"].input-sm, + input[type="month"].input-sm { + line-height: @input-height-small; + } + input[type="date"].input-lg, + input[type="time"].input-lg, + input[type="datetime-local"].input-lg, + input[type="month"].input-lg { + line-height: @input-height-large; + } +} + + +// Form groups +// +// Designed to help with the organization and spacing of vertical forms. For +// horizontal forms, use the predefined grid classes. + +.form-group { + margin-bottom: 15px; +} + + +// Checkboxes and radios +// +// Indent the labels to position radios/checkboxes as hanging controls. + +.radio, +.checkbox { + position: relative; + display: block; + margin-top: 10px; + margin-bottom: 10px; + + label { + min-height: @line-height-computed; // Ensure the input doesn't jump when there is no text + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + cursor: pointer; + } +} +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + position: absolute; + margin-left: -20px; + margin-top: 4px \9; +} + +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing +} + +// Radios and checkboxes on same line +.radio-inline, +.checkbox-inline { + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + vertical-align: middle; + font-weight: normal; + cursor: pointer; +} +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; // space out consecutive inline controls +} + +// Apply same disabled cursor tweak as for inputs +// Some special care is needed because