candy 2.0 + plugins

This commit is contained in:
Jörg Thalheim 2015-07-22 01:01:23 +02:00
parent 775acd247c
commit c4ebf7ba80
94 changed files with 12042 additions and 14477 deletions

View File

@ -1,8 +0,0 @@
docs
example/.htaccess
.DS_Store
._*
.idea
.ndproj/Data
.ndproj/Menu.txt
node_modules

30
content/static/candy/CONTRIBUTING.md Normal file → Executable file
View File

@ -7,6 +7,8 @@
## Learn & listen
[![Gitter chat](https://badges.gitter.im/candy-chat.png)](https://gitter.im/candy-chat)
* [Mailing list](http://groups.google.com/group/candy-chat)
* yes, non-gmail users can signup as well
* [FAQ](https://github.com/candy-chat/candy/wiki/Frequently-Asked-Questions)
@ -21,8 +23,7 @@ A few hopefully helpful hints to contributing to Candy
#### Using vagrant
1. [Fork](https://help.github.com/articles/fork-a-repo) Candy
2. [Install Vagrant](http://vagrantup.com/)
3. Follow instructions [for Candy Vagrant](https://github.com/candy-chat/vagrant)
4. Change the remote in the `candy` and `candy-plugins` repos: `git remote set-url origin git://github.com/YOURNAME/candy` (or candy-plugins)
3. Run `vagrant up`.
5. Create a branch based on the `dev` branch (`git checkout -B my-awesome-feature`)
6. Run `grunt watch` to automatically run jshint (syntax checker) and the build of `candy.bundle.js` and `candy.min.js` while developing.
7. Make your changes, fix eventual *jshint* errors & push them back to your fork
@ -34,13 +35,22 @@ Please note that you should have a working XMPP server to test your changes (the
1. [Fork](https://help.github.com/articles/fork-a-repo) Candy
2. Clone your fork
2. Checkout out `dev` branch (`git checkout dev`) & Update git submodules `git submodule update --init`
3. Install [Node.js](http://nodejs.org/)
4. Install [Grunt](http://gruntjs.com/) (`npm install -g grunt-cli`)
5. Install npm dependencies (`npm install` in candy root directory)
6. Create a branch based on the `dev` branch (`git checkout -B my-awesome-feature`)
7. Run `grunt watch` to automatically run jshint (syntax checker) and the build of `candy.bundle.js` and `candy.min.js` while developing.
8. Make your changes, fix eventual *jshint* errors & push them back to your fork
9. Create a [pull request](https://help.github.com/articles/using-pull-requests)
3. Checkout out `dev` branch (`git checkout dev`)
4. Install [Node.js](http://nodejs.org/)
5. Install [Grunt](http://gruntjs.com/) (`npm install -g grunt-cli`)
6. Install [Bower](http://bower.io/) (`npm install -g bower`)
7. Install npm dependencies (`npm install` in candy root directory)
8. Install bower dependencies (`bower install` in candy root directory)
9. Create a branch based on the `dev` branch (`git checkout -B my-awesome-feature`)
10. Run `grunt watch` to automatically run jshint (syntax checker) and the build of `candy.bundle.js` and `candy.min.js` while developing.
11. Make your changes, fix eventual *jshint* errors & push them back to your fork
12. Create a [pull request](https://help.github.com/articles/using-pull-requests)
In case you have any questions, don't hesitate to ask on the [Mailing list](http://groups.google.com/group/candy-chat).
### Running tests
* Tests are run using [Intern](http://theintern.io).
* `grunt` and `grunt watch` will each run unit tests in Chrome on Linux (for fast feedback).
* `grunt test` will run both unit and integration tests in a variety of environments. Tests are run using Selenium Standalone and Phantom.JS while developing, and on Sauce Labs in CI or using `grunt test`.
* If you don't want to use the Vagrant box to run Selenium/PhantomJS, set `CANDY_VAGRANT='false'` to run tests.

View File

@ -1,13 +1,14 @@
'use strict';
var localInternConfig = process.env.CANDY_VAGRANT === 'false' ? 'tests/intern.local' : 'tests/intern.vagrant';
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
jshint: {
all: ['Gruntfile.js', './src/**/*.js'],
all: ['Gruntfile.js', './src/**/*.js', './tests/**/*.js'],
options: {
jshintrc: "./.jshintrc",
reporter: require('jshint-stylish')
@ -30,8 +31,11 @@ module.exports = function(grunt) {
'src/candy.js', 'src/core.js', 'src/view.js',
'src/util.js', 'src/core/action.js',
'src/core/chatRoom.js', 'src/core/chatRoster.js',
'src/core/chatUser.js', 'src/core/event.js',
'src/view/observer.js', 'src/view/pane.js',
'src/core/chatUser.js', 'src/core/contact.js',
'src/core/event.js', 'src/view/observer.js',
'src/view/pane/chat.js', 'src/view/pane/message.js',
'src/view/pane/privateRoom.js', 'src/view/pane/room.js',
'src/view/pane/roster.js', 'src/view/pane/window.js',
'src/view/template.js', 'src/view/translation.js'
]
},
@ -53,14 +57,15 @@ module.exports = function(grunt) {
},
libs: {
files: {
'libs/libs.bundle.js': [
'libs/strophejs/strophe.js',
'libs/strophejs-plugins/muc/strophe.muc.js',
'libs/strophejs-plugins/disco/strophe.disco.js',
'libs/strophejs-plugins/caps/strophe.caps.jsonly.js',
'libs/mustache.js/mustache.js',
'libs/jquery-i18n/jquery.i18n.js',
'libs/dateformat/dateFormat.js'
'libs.bundle.js': [
'bower_components/strophe/strophe.js',
'bower_components/strophejs-plugins/muc/strophe.muc.js',
'bower_components/strophejs-plugins/roster/strophe.roster.js',
'bower_components/strophejs-plugins/disco/strophe.disco.js',
'bower_components/strophejs-plugins/caps/strophe.caps.jsonly.js',
'bower_components/mustache/mustache.js',
'bower_components/jquery-i18n/jquery.i18n.js',
'vendor_libs/dateformat/dateFormat.js'
]
},
options: {
@ -73,23 +78,38 @@ module.exports = function(grunt) {
},
'libs-min': {
files: {
'libs/libs.min.js': ['libs/libs.bundle.js']
'libs.min.js': ['libs.bundle.js']
}
}
},
watch: {
clear: {
files: ['src/*.js', 'src/**/*.js', 'tests/**/*.js'],
tasks: ['clear']
},
grunt: {
files: ['Gruntfile.js']
},
bundle: {
files: ['src/*.js', 'src/**/*.js'],
tasks: ['jshint', 'uglify:bundle', 'uglify:min', 'notify:bundle']
files: ['src/**/*.js'],
tasks: ['todo:src', 'jshint', 'uglify:bundle', 'uglify:min', 'notify:bundle', 'intern:unit']
},
libs: {
files: ['libs/*/**/*.js'],
files: ['bower_components/*/**/*.js', 'vendor_libs/*/**/*.js'],
tasks: ['uglify:libs', 'uglify:libs-min', 'notify:libs']
},
tests: {
files: ['tests/candy/unit/**/*.js'],
tasks: ['todo:tests', 'jshint', 'intern:unit']
},
functional_tests: {
files: ['tests/candy/functional/**/*.js'],
tasks: ['todo:tests', 'jshint', 'intern:functional']
}
},
natural_docs: {
all: {
bin: process.env.NATURALDOCS_DIR + '/NaturalDocs',
bin: process.env.NATURALDOCS_DIR ? process.env.NATURALDOCS_DIR + '/NaturalDocs' : 'naturaldocs',
flags: ['-r'],
inputs: ['./src'],
output: './docs',
@ -98,7 +118,7 @@ module.exports = function(grunt) {
},
clean: {
bundle: ['./candy.bundle.js', './candy.bundle.map', './candy.min.js'],
libs: ['./libs/libs.bundle.js', './libs/libs.bundle.map', './libs/libs.min.js'],
libs: ['./libs.bundle.js', './libs.bundle.map', './libs.min.js'],
docs: ['./docs']
},
mkdir: {
@ -129,6 +149,41 @@ module.exports = function(grunt) {
message: 'JsHint & bundling done'
}
}
},
intern: {
all: {
options: {
runType: 'runner',
config: 'tests/intern'
}
},
unit: {
options: {
runType: 'runner',
config: localInternConfig,
functionalSuites: []
}
},
functional: {
options: {
runType: 'runner',
config: localInternConfig,
suites: []
}
}
},
coveralls: {
options: {
force: true // prevent from failing CI build if coveralls is down etc.
},
all: {
src: 'lcov.info',
}
},
todo: {
options: {},
src: ['src/**/*.js'],
tests: ['tests/**/*.js']
}
});
@ -140,10 +195,16 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-mkdir');
grunt.loadNpmTasks('grunt-notify');
grunt.loadNpmTasks('grunt-sync-pkg');
grunt.loadNpmTasks('intern');
grunt.loadNpmTasks('grunt-clear');
grunt.loadNpmTasks('grunt-coveralls');
grunt.loadNpmTasks('grunt-todo');
grunt.registerTask('test', ['intern:all']);
grunt.registerTask('ci', ['todo', 'jshint', 'build', 'intern:all', 'coveralls:all', 'docs']);
grunt.registerTask('build', ['uglify:libs', 'uglify:libs-min', 'uglify:bundle', 'uglify:min']);
grunt.registerTask('default', [
'jshint', 'uglify:libs', 'uglify:libs-min',
'uglify:bundle', 'uglify:min', 'notify:default'
'jshint', 'build', 'notify:default', 'intern:unit'
]);
grunt.registerTask('docs', ['mkdir:docs', 'natural_docs', 'notify:docs']);
};
};

View File

@ -1,6 +1,9 @@
Candy — a JavaScript-based multi-user chat client
==================================================
[![Build Status](https://travis-ci.org/candy-chat/candy.png?branch=dev)](https://travis-ci.org/candy-chat/candy)
[![Coverage Status](https://coveralls.io/repos/candy-chat/candy/badge.png?branch=dev)](https://coveralls.io/r/candy-chat/candy)
Visit the official project page: http://candy-chat.github.io/candy
Features
@ -11,7 +14,7 @@ Features
- 100% well-documented JavaScript source code
- Built for Jabber (XMPP), using famous technologies
- Used and approved in a productive environment with up to 400 concurrent users
- Works with all major web browsers including IE7
- Works with all major web browsers including IE9
Plugins
-------
@ -20,6 +23,6 @@ If you wish to add new functionality (to your candy installation) or contribute
Support & Community
-------------------
Take a look at our [FAQ](https://github.com/candy-chat/candy/wiki/Frequently-Asked-Questions). If it doesn't solve your questions, you're welcome to join our [Mailinglist on Google Groups](http://groups.google.com/group/candy-chat).
You don't need to have a Gmail account for it.
You don't need to have a Gmail account for it.
[![githalytics.com alpha](https://cruel-carlota.pagodabox.com/a41a8075608abeaf99db685d7ef29cf6 "githalytics.com")](http://githalytics.com/candy-chat/candy)

20
content/static/candy/Vagrantfile vendored Normal file
View File

@ -0,0 +1,20 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "ubuntu/trusty64"
config.vm.network :forwarded_port, guest: 80, host: 8080
config.vm.network :forwarded_port, guest: 5280, host: 5280
config.vm.network :forwarded_port, guest: 4444, host: 4444
config.vm.network :private_network, ip: '192.168.88.4'
config.vm.provision :shell, :path => "devbox/provisioning.sh"
config.vm.provider "virtualbox" do |v|
v.name = "candy"
v.customize ["modifyvm", :id, "--memory", 768]
end
end

View File

@ -29,5 +29,12 @@
"bower_components",
"test",
"tests"
]
}
],
"dependencies": {
"jquery": "~1.10.2",
"strophe": "1.1.3",
"strophejs-plugins": "benlangfeld/strophejs-plugins#30fb089457addc37e01d69c3536dee868a90a9ad",
"mustache": "0.3.0",
"jquery-i18n": "1.1.1"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,59 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Candy - Chats are not dead yet</title>
<link rel="shortcut icon" href="res/img/favicon.png" type="image/gif" />
<link rel="stylesheet" type="text/css" href="res/default.css" />
<meta charset="utf-8">
<title>Candy - Chats are not dead yet</title>
<link rel="shortcut icon" href="res/img/favicon.png" type="image/gif" />
<link rel="stylesheet" type="text/css" href="res/default.css" />
<script type="text/javascript" src="libs/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript" src="libs/libs.min.js"></script>
<script type="text/javascript" src="candy.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
function rid() {
return "" + Math.ceil(99999999 * Math.random());
}
var nick = "Candy-" + rid();
var m;
if ((m = window.location.search.match(/nick=([^\&]+)/))) {
nick = decodeURIComponent(m[1]);
}
Candy.init('https://www.c3d2.de/http-bind/', {
core: {
// only set this to true if developing / debugging errors
debug: false,
// autojoin is a *required* parameter if you don't have a plugin (e.g. roomPanel) for it
// true
// -> fetch info from server (NOTE: does only work with openfire server)
// ['test@conference.example.com']
// -> array of rooms to join after connecting
resource: rid() + "-" + rid() + "-" + rid(),
autojoin: ["c3d2@chat.c3d2.de"]
},
view: { assets: 'res/' }
});
<script type="text/javascript" src="jquery/1.10.2/jquery.min.js"></script>
/*Candy.Core.connect("candy@jabber.c3d2.de", "yummy", nick);*/
Candy.Core.connect("anon.jabber.c3d2.de", null, nick);
<script type="text/javascript" src="libs.min.js"></script>
<script type="text/javascript" src="candy.min.js"></script>
/**
* Thanks for trying Candy!
*
* If you need more information, please see here:
* - Setup instructions & config params: http://candy-chat.github.io/candy/#setup
* - FAQ & more: https://github.com/candy-chat/candy/wiki
*
* Mailinglist for questions:
* - http://groups.google.com/group/candy-chat
*
* Github issues for bugs:
* - https://github.com/candy-chat/candy/issues
*/
});
</script>
<script type="text/javascript" src="plugins/timeago/candy.js"></script>
<link rel="stylesheet" type="text/css" href="plugins/timeago/candy.css" />
<script type="text/javascript" src="plugins/inline-images/candy.js"></script>
<link rel="stylesheet" type="text/css" href="plugins/inline-images/candy.css" />
<script type="text/javascript" src="plugins/inline-videos/candy.js"></script>
<script type="text/javascript" src="plugins/typingnotifications/typingnotifications.js"></script>
<link rel="stylesheet" type="text/css" href="plugins/typingnotifications/typingnotifications.css" />
<script type="text/javascript" src="plugins/namecomplete/candy.js"></script>
<link rel="stylesheet" type="text/css" href="plugins/namecomplete/candy.css" />
<script type="text/javascript" src="plugins/notifications/candy.js"></script>
<script type="text/javascript" src="plugins/notifyme/candy.js"></script>
<link rel="stylesheet" type="text/css" href="plugins/notifyme/candy.css" />
<script type="text/javascript" src="plugins/medoes/candy.js"></script>
<link rel="stylesheet" type="text/css" href="plugins/medoes/candy.css" />
<script type="text/javascript">
$(document).ready(function() {
function rid() {
return "" + Math.ceil(99999999 * Math.random());
}
var nick = "Candy-" + rid();
var m;
if ((m = window.location.search.match(/nick=([^\&]+)/))) {
nick = decodeURIComponent(m[1]);
}
Candy.init('https://www.c3d2.de/http-bind/', {
core: {
// only set this to true if developing / debugging errors
debug: false,
// autojoin is a *required* parameter if you don't have a plugin (e.g. roomPanel) for it
// true
// -> fetch info from server (NOTE: does only work with openfire server)
// ['test@conference.example.com']
// -> array of rooms to join after connecting
resource: rid() + "-" + rid() + "-" + rid(),
autojoin: ["c3d2@chat.c3d2.de"]
},
view: { assets: 'res/' }
});
CandyShop.Timeago.init();
CandyShop.NotifyMe.init();
CandyShop.InlineImages.init();
CandyShop.InlineVideos.init();
CandyShop.TypingNotifications.init();
CandyShop.NameComplete.init();
CandyShop.Notifications.init();
CandyShop.NotifyMe.init();
CandyShop.MeDoes.init();
Candy.Core.connect("anon.jabber.c3d2.de", null, nick);
});
</script>
</head>
<body>
<div id="candy"></div>
<div id="candy"></div>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

3
content/static/candy/libs.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -40,14 +40,21 @@
"homepage": "http://candy-chat.github.io/candy/",
"devDependencies": {
"grunt": "^0.4.5",
"grunt-contrib-clean": "~0.5.0",
"grunt-clear": "^0.2.1",
"grunt-contrib-clean": "^0.5.0",
"grunt-contrib-jshint": "^0.10.0",
"grunt-contrib-uglify": "^0.4.0",
"grunt-contrib-watch": "^0.6.1",
"grunt-mkdir": "~0.1.1",
"grunt-natural-docs": "~0.1.1",
"grunt-coveralls": "^0.3.0",
"grunt-mkdir": "^0.1.1",
"grunt-natural-docs": "^0.1.1",
"grunt-notify": "^0.3.0",
"grunt-sync-pkg": "~0.1.2",
"jshint-stylish": "^0.2.0"
"grunt-sync-pkg": "^0.1.2",
"intern": "^2.0.1",
"jshint-stylish": "^0.2.0",
"sinon": "^1.10.3",
"sinon-chai": "^2.5.0",
"lolex": "^1.2.0",
"grunt-todo": "~0.4.0"
}
}

View File

@ -0,0 +1,7 @@
.DS_Store
._*
.idea
.*.sw*
bower_components
node_modules
lcov.info

View File

@ -0,0 +1,65 @@
'use strict';
var localInternConfig = process.env.CANDY_VAGRANT === 'false' ? 'tests/intern.local' : 'tests/intern.vagrant';
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
jshint: {
all: ['Gruntfile.js', '**/*.js', '!node_modules/**', '!bower_components/**'],
options: {
jshintrc: "./.jshintrc",
reporter: require('jshint-stylish')
}
},
watch: {
grunt: {
files: ['Gruntfile.js']
},
clear: {
files: ['**/*.js'],
tasks: ['clear', 'todo', 'jshint', 'intern:unit']
}
},
intern: {
all: {
options: {
runType: 'runner',
config: 'tests/intern'
}
},
unit: {
options: {
runType: 'runner',
config: localInternConfig,
functionalSuites: []
}
}
},
coveralls: {
options: {
force: true // prevent from failing CI build if coveralls is down etc.
},
all: {
src: 'lcov.info',
}
},
todo: {
options: {},
all: ['**/*.js', '!node_modules/**', '!bower_components/**']
}
});
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('intern');
grunt.loadNpmTasks('grunt-clear');
grunt.loadNpmTasks('grunt-coveralls');
grunt.loadNpmTasks('grunt-todo');
grunt.registerTask('test', ['intern:unit']);
grunt.registerTask('ci', ['todo', 'jshint', 'intern:all', 'coveralls:all']);
grunt.registerTask('default', ['jshint', 'intern:unit']);
};

View File

@ -0,0 +1,7 @@
Copyright (c) 2014 Misc
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.

View File

@ -0,0 +1,63 @@
# Candy Plugins
[![Build Status](https://travis-ci.org/candy-chat/candy-plugins.png)](https://travis-ci.org/candy-chat/candy-plugins)
[![Coverage Status](https://coveralls.io/repos/candy-chat/candy-plugins/badge.png)](https://coveralls.io/r/candy-chat/candy-plugins)
This is the official plugin repository for [Candy](http://candy-chat.github.com/candy), a JavaScript based multi-user chat client.
## List of available plugins
* __Auto-Join Invites__ - Automatically joins any and all incoming MUC invites.
* __available-rooms__ - A plugin to show & join public rooms.
* __Chat Recall__ - Saves the last {x} messages to scroll through with up and down arrows, similar to terminal/cmd.
* __Clearchat__ - Clears chat window on click or if typing `/clear`
* __Colors__ - Send and receive colored messages.
* __Colors XHTML__ - Send and receive colored messages formatted with XHTML.
* __Create Room__ - Creates a clickable UI for creating and joining rooms.
* __Emphasis__ - basic text formatting via textile, BBcode, or xhtml
* __Fullscreen Display__ - Shows incoming messages to specified users starting with @ + username + : as large as the browser's content area, overlaying everything else.
* __Inline Images__ - If a user posts a URL to an image, that image gets rendered directly inside of Candy.
* __Inline Videos__ - If a user posts a URL to youtube video, it embeds the youtube video iframe into Candy.
* __join__ A plugin that allows to type `/join room [password]` to join a room.
* __jQuery-Ui__ - jQuery UI lightness theme
* __Left Tabs__ - Moves the tabs to the left side and uses a bit of Bootstrap3-friendly theme elements.
* __Modify Role__ - Adds **add moderator** and **remove moderator** context menu links.
* __Me Does__ - special formatting for /me messages
* __Namecomplete__ - Autocompletes names of users within room
* __Nickchange__ - Enable your users to change the nick using a toolbar icon
* __Notifications__ - OS Notifications in webkit
* __Notifyme__ - Notifies yourself in case one does use your nickname in a message
* __Refocus__ - This plugin puts the focus on the entry box if the user clicks somewhere in the message list.
* __Remove Ignore__ - Removes the option to ignore/unignore a user from the roster.
* __Replies__ - Highlight any message that contains "@my_username"
* __MUC Room Bar__ - Adds a bar to the top of the message pane that displays the room topic and allows moderators to click-to-edit.
* __Room Panel__ - Provides a list of rooms available to join.
* __Static Lobby__ - Creates a static lobby UI and pulls in a global roster. Allows you to invite people from global roster to other MUCs you are participating in.
* __Sticky Subject__ - Retains the subject of the room underneath the tab itself.
* __Timeago__ - Replaces the exact time/date with fuzzy timestamps like "2 minutes ago".
* __Typing Notifications__ - Displays a user's typing notification status above the text entry form.
## Contributing
Please submit a pull request with your plugin or your changes to a plugin. We'll gladly merge it.
After a successful merge of a pull request, we will give you **push access** to this repository. You can then update your plugin on your own. If you update other plugins, please consider creating a pull request in order to inform the original plugin owner.
When contributing, please make sure that your code is of **high quality** and similar to other code in this repository. Also please submit a **screenshot** and a **README.md**.
1. [Setup the Vagrant environment from Candy core](https://github.com/candy-chat/candy/blob/dev/CONTRIBUTING.md)
2. Install [Node.js](http://nodejs.org/)
3. Install [Grunt](http://gruntjs.com/) (`npm install -g grunt-cli`)
4. Install [Bower](http://bower.io/) (`npm install -g bower`)
5. Install npm dependencies (`npm install` in candy-plugins root directory)
6. Install bower dependencies (`bower install` in candy-plugins root directory)
7. Run `grunt watch` to automatically run jshint (syntax checker) and the tests while developing.
### Running tests
* Tests are run using [Intern](http://theintern.io).
* `grunt` and `grunt watch` will each run unit tests in Chrome on Linux (for fast feedback).
* `grunt test` will run both unit and integration tests in a variety of environments. Tests are run using Selenium Standalone and Phantom.JS while developing, and on Sauce Labs in CI or using `grunt test`.
* If you don't want to use the Vagrant box to run Selenium/PhantomJS, set `CANDY_VAGRANT='false'` to run tests.
## Support & Community
Take a look at our [FAQ](https://github.com/candy-chat/candy/wiki/Frequently-Asked-Questions). If it doesn't solve your questions, you're welcome to join our [Mailinglist on Google Groups](http://groups.google.com/group/candy-chat).
You don't need to have a Gmail account for it.

View File

@ -0,0 +1,34 @@
{
"name": "candy-shop",
"version": "1.0.0",
"homepage": "http://candy-chat.github.io/candy/",
"authors": [
"Michael Weibel <michael.weibel@gmail.com>",
"Patrick Stadler <patrick.stadler@gmail.com>"
],
"description": "Multi-user XMPP web client plugins",
"main": [
],
"keywords": [
"xmpp",
"muc",
"multi-user",
"websocket",
"bosh",
"chat"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "git://github.com/candy-chat/candy-plugins.git"
},
"ignore": [
"node_modules",
"bower_components",
"tests"
],
"dependencies": {
"candy": "1.7.0",
"jquery": "~1.10.2"
}
}

View File

@ -0,0 +1,24 @@
# Inline Images
If a user posts a URL to an image, that image gets rendered directly inside of Candy.
![Inline Images](screenshot.png)
## Usage
Include the JavaScript and CSS files:
```HTML
<script type="text/javascript" src="candyshop/inline-images/candy.js"></script>
<link rel="stylesheet" type="text/css" href="candyshop/inline-images/candy.css" />
```
To enable the Inline Images plugin, just add one of the ´init´ methods to your bootstrap:
```JavaScript
// init with default settings:
CandyShop.InlineImages.init();
// customized initialization:
CandyShop.InlineImages.initWithFileExtensions(['png','jpg']); // only recognize PNG and JPG files as image
CandyShop.InlineImages.initWithMaxImageSize(150); // resize images to a maximum edge size of 150px
CandyShop.InlineImages.initWithFileExtensionsAndMaxImageSize(['png','jpg'], 150); // combination of the above examples
```

View File

@ -0,0 +1,13 @@
.inlineimages-link {
text-decoration:none;
position:relative;
display:inline-block;
}
.inlineimages-link:hover:before {
content: url('overlay.png');
position: absolute;
top: 5px;
left: 5px;
opacity: .8;
}

View File

@ -0,0 +1,199 @@
/*
* inline-images
* @version 1.0
* @author Manuel Alabor (manuel@alabor.me)
* @author Jonatan Männchen <jonatan@maennchen.ch>
*
* If a user posts a URL to an image, that image gets rendered directly
* inside of Candy.
*/
/* global Candy, jQuery, Image */
var CandyShop = (function(self) { return self; }(CandyShop || {}));
CandyShop.InlineImages = (function(self, Candy, $) {
var _options = {
fileExtensions: ['png','jpg','jpeg','gif']
, maxImageSize: 100
, noInlineSizing: false
};
/** Function: init
* Initializes the inline-images plugin with the default settings.
*/
self.init = function(options) {
// Apply the supplied options to the defaults specified
$.extend(true, _options, options);
$(Candy).on('candy:view.message.before-show', handleBeforeShow);
$(Candy).on('candy:view.message.after-show', handleOnShow);
};
/** Function: initWithFileExtensions
* Initializes the inline-images plugin with the possibility to pass an
* array with all the file extensions you want to display as image.
*
* Parameters:
* (String array) fileExtensions - Array with extensions (jpg, png, ...)
*/
self.initWithFileExtensions = function(fileExtensions) {
_options.fileExtensions = fileExtensions;
self.init();
};
/** Function: initWithMaxImageSize
* Initializes the inline-images plugin with the possibility to pass the
* maximum image size for displayed images.
*
* Parameters:
* (int) maxImageSize - Maximum edge size for images
*/
self.initWithMaxImageSize = function(maxImageSize) {
_options.maxImageSize = maxImageSize;
self.init();
};
/** Function: initWithFileExtensionsAndMaxImageSize
* Initializes the inline-images plugin with the possibility to pass an
* array with all the file extensions you want to display as image and
* the maximum image size for displayed images.
*
* Parameters:
* (String array) fileExtensions - Array with extensions (jpg, png, ...)
* (int) maxImageSize - Maximum edge size for images
*/
self.initWithFileExtensionsAndMaxImageSize = function(fileExtensions, maxImageSize) {
_options.fileExtensions = fileExtensions;
_options.maxImageSize = maxImageSize;
self.init();
};
/** Function: handleBeforeShow
* Handles the beforeShow event of a message.
*
* Paramteres:
* (Object) args - {roomJid, element, nick, message}
*
* Returns:
* (String)
*/
var handleBeforeShow = function(e, args) {
args.message = replaceLinksWithLoaders(args.message);
if (args.xhtmlMessage) {
args.xhtmlMessage = replaceLinksWithLoaders(args.xhtmlMessage);
}
return true;
};
/** Function replaceLinksWithLoaders
* Replaces anchor tags with image loader elements where applicable
*
* Parameters:
* (String) message
*
* Returns:
* (String) the replaced message
*/
var replaceLinksWithLoaders = function(message) {
var dummyContainer = document.createElement('div');
dummyContainer.innerHTML = message;
$(dummyContainer).find('a').each(function(index, anchor) {
if (anchorHasMatchingFileExtension(anchor)) {
anchor.innerHTML = buildImageLoaderSource(anchor.href);
}
});
return dummyContainer.innerHTML;
};
/** Function anchorHasMatchingFileExtension
* Identifies whether or not an anchor tag links to a file with one of the matching extensions we're looking for
*
* Parameters:
* (Element) element
*
* Returns:
* (true, false)
*/
var anchorHasMatchingFileExtension = function(element) {
var dotPosition = element.pathname.lastIndexOf(".");
if(dotPosition > -1) {
if(_options.fileExtensions.indexOf(element.pathname.substr(dotPosition+1)) != -1) {
return true;
}
}
return false;
};
/** Function: handleOnShow
* Each time a message gets displayed, this method checks for possible
* image loaders (created by buildImageLoaderSource).
* If there is one, the image "behind" the loader gets loaded in the
* background. As soon as the image is loaded, the image loader gets
* replaced by proper scaled image.
*
* Parameters:
* (Array) args
*/
var handleOnShow = function(e, args) {
$('.inlineimages-loader').each(function(index, element) {
$(element).removeClass('inlineimages-loader');
var url = $(element).attr('longdesc');
var imageLoader = new Image();
$(imageLoader).load(function() {
var origWidth = this.width;
var origHeight = this.height;
if(origWidth > _options.maxImageSize || origHeight > _options.maxImageSize) {
var ratio = Math.min(_options.maxImageSize / origWidth, _options.maxImageSize / origHeight);
var width = Math.round(ratio * origWidth);
var height = Math.round(ratio * origHeight);
}
$(element).replaceWith(buildImageSource(url, width, height))
});
imageLoader.src = url;
});
};
/** Function: buildImageLoaderSource
* Returns a loader indicator. The handleOnShow method fullfills afterwards
* the effective image loading.
*
* Parameters:
* (String) url - image url
*
* Returns:
* (String)
*/
var buildImageLoaderSource = function(url) {
return '<img class="inlineimages-loader" longdesc="' + url + '" src="ui/candy-plugins/inline-images/spinner.gif" />';
};
/** Function: buildImageSource
* Returns HTML source to show a URL as an image.
*
* Parameters:
* (String) url - image url
*
* Returns:
* (String)
*/
var buildImageSource = function(url, width, height) {
if (_options.noInlineSizing) {
return '<img src="' + url + '" />';
} else {
return '<img src="' + url + '" width="' + width + '" height="' + height + '"/>';
}
};
return self;
}(CandyShop.InlineImages || {}, Candy, jQuery));

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,19 @@
# Inline Videos Plugin
If a user posts a URL to a youtube video, that video gets rendered directly inside of Candy.
## Usage
Include the JavaScript file:
```HTML
<script type="text/javascript" src="path_to_plugins/inline-videos/candy.js"></script>
```
Call its `init()` method after Candy has been initialized:
```JavaScript
Candy.init('/http-bind/');
CandyShop.InlineVideos.init();
Candy.Core.connect();
```

View File

@ -0,0 +1,42 @@
/** File: candy.js
* Candy - Chats are not dead yet.
*
* Authors
* - Jonatan Männchen <jonatan.maennchen@amiadogroup.com>
*/
/* global Candy, jQuery */
var CandyShop = (function(self) { return self; }(CandyShop || {}));
/** Class: InlineVideos
* If a user posts a URL to a video, that video gets rendered directly
* inside of Candy.
*/
CandyShop.InlineVideos = (function(self, Candy, $) {
/** Function: init
* Initializes the inline-videos plugin with the default settings.
*
* Parameters:
* (Object) options - An options packet to apply to this plugin
*/
self.init = function() {
// add a listener to these events
$(Candy).on('candy:view.message.before-show', self.handleBeforeShow);
};
/** Function: handleBeforeShow
* Handles the beforeShow event of a message.
*
* Parameters:
* (String) message - the message to process
*
* Returns:
* (String)
*/
self.handleBeforeShow = function(e, args) {
args.message = args.message.replace(/\>(https?:\/\/w{0,3}\.?youtube.com\/watch\?v=([^\s^&]*)([^\s]*))\<\/a\>/i, '>$1<br /><iframe width="300" height="200" src="//www.youtube.com/embed/$2" frameborder="0" allowfullscreen></iframe></a><br />');
};
return self;
}(CandyShop.InlineVideos || {}, Candy, jQuery));

View File

@ -0,0 +1,10 @@
#Candy jQuery UI lightness Theme plugin
This plugin replaces the default theme with the jQuery UI lightness Theme. (http://jqueryui.com/)
##Usage
To enable jQuery UI lightness Theme you have to include its stylesheet:
```html
<link rel="stylesheet" type="text/css" href="candy/plugins/jquery-ui/ui-lightness/css/ui-lightness.css" />
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

@ -0,0 +1,115 @@
html, body {
font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;
}
div#candy {
background-color: #EEE;
}
div#candy div#chat-pane ul#chat-tabs {
background-image: url('../images/ui-bg_gloss-wave_35_f6a828_500x100.png');
background-repeat: repeat-x;
background-color: #F6A828;
}
div#candy div#chat-pane ul#chat-tabs li {
border-right: 1px solid #CCC;
}
div#candy div#chat-pane ul#chat-tabs li a {
color: #1C94C4;
}
div#candy div#chat-pane ul#chat-tabs li.active a {
color: #E78F08;
}
div#candy div#chat-pane ul#chat-tabs li.roomtype-chat small.unread {
background-color: #F6A828;
}
div#candy div#chat-pane ul#chat-tabs li.roomtype-groupchat small.unread {
background-color: #F6A828;
}
div#candy div#chat-pane ul#chat-toolbar {
background-color: #EEE;
border-top: 1px solid #DDD;
}
div#candy div#chat-pane div#chat-rooms.rooms div.room-pane form.message-form input.submit {
padding: 2px 5px 5px 5px;
background-color: #F6F6F6;
border: 1px solid #CCC;
border-radius: 4px;
color: #1C94C4;
}
div#candy div#chat-pane div#chat-rooms.rooms div.room-pane form.message-form input.submit:hover {
background-color: #FDF9E1;
border: 1px solid #FBCB09;
color: #E78F08;
}
div#candy div#chat-pane div#chat-rooms.rooms div.room-pane div.message-form-wrapper {
border-top: 1px solid #DDD;
}
div#candy div#chat-pane div#chat-rooms.rooms div.room-pane div.message-pane-wrapper dl.message-pane dd span.label a.name {
color: #888;
}
div#candy div#chat-pane div#chat-rooms.rooms div.room-pane div.message-pane-wrapper dl.message-pane dd.adminmessage {
color: #000;
}
div#candy div#chat-pane div#chat-rooms.rooms div.room-pane div.message-pane-wrapper dl.message-pane dd.subject {
color: #E78F08;
}
div#candy div#chat-pane div#chat-rooms.rooms div.room-pane div.roster-pane div.user {
background-color: #F8F8F8;
border: 1px solid #CCC;
border-radius: 4px;
color: #1C94C4;
}
div#candy div#chat-pane div#chat-rooms.rooms div.room-pane div.roster-pane div.user:hover {
cursor: pointer;
background-color: #FDF9E1;
border: 1px solid #FBCB09;
color: #E78F08;
}
div#candy div#chat-pane div#chat-rooms.rooms div.room-pane div.roster-pane ul li.context {
background-image: url('../images/action/menu.png');
}
div#candy div#chat-pane div#chat-rooms.rooms div.room-pane div.roster-pane ul li.context:hover {
background-image: url('../images/action/menu-hover.png');
background-color: #F6A828;
}
#context-menu {
position: absolute;
z-index: 10;
display: none;
padding: 15px 10px;
margin: 8px -28px -8px -12px;
background: url('../images/context-arrows.gif') no-repeat left bottom;
}
#context-menu ul {
background-color: #F8F8F8;
border: 1px solid #CCC;
border-radius: 4px;
color: #1C94C4;
}
#context-menu li:hover {
background-color: #FDF9E1 !important;
color: #E78F08;
}
#chat-modal {
background: url('../images/modal-bg.png');
color: #000;
}
#chat-modal a#admin-message-cancel.close {
color: #000;
}
#chat-modal a#admin-message-cancel.close:hover {
background-color: #F6A828;
color: #FFF;
}
#chat-modal-overlay {
background-image: url('../images/overlay.png');
filter:alpha(opacity=50);
opacity: 0.5;
-moz-opacity:0.5;
}
#tooltip {
background: url('../images/tooltip-arrows.gif') no-repeat left bottom;
}
#tooltip div {
background-color: #F6A828;
color: #FFF;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,31 @@
# /me Does
Format /me messages, implementing XEP-0245
## Usage
Include the JavaScript file:
```HTML
<script type="text/javascript" src="candyshop/me-does/candy.js"></script>
```
Call its `init()` method after Candy has been initialized:
```javascript
Candy.init('/http-bind/', {});
// enable /me handling
CandyShop.MeDoes.init();
Candy.Core.connect();
```
Now all messages starting with '/me 'will use infoMessage formatting.
```
/me takes screenshot
```
![Color Picker](me-does-screenshot.png)
**Please note**: As `me-does` reroutes message output, it's call to `init()` should happen after the `init()` of most other plugins, including, `inline-images`.

View File

@ -0,0 +1,17 @@
var CandyShop = (function(self) { return self; }(CandyShop || {}));
CandyShop.MeDoes = (function(self, Candy, $) {
self.init = function() {
$(Candy).on("candy:view.message.before-show", function(e, args) {
if (args && args.message && args.message.match(/^\/me /i)) {
var message = args.message.match(/^\/([^\s]+)(?:\s+(.*))?$/m)[2];
Candy.View.Pane.Chat.infoMessage(args.roomJid, null, '<span><strong>' + args.name + '</strong> ' + message + '</span>');
return false;
}
});
};
return self;
}(CandyShop.MeDoes || {}, Candy, jQuery));

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,29 @@
# Modify role
Adds **add moderator** and **remove moderator** privilege links to context menu.
![Modify role screenshot](screenshot.png)
## Usage
To enable *Modify role* you have to include its JavaScript code and stylesheet:
```HTML
<script type="text/javascript" src="candyshop/modify-role/candy.js"></script>
<link rel="stylesheet" type="text/css" href="candyshop/modify-role/candy.css" />
```
Call its `init()` method after Candy has been initialized:
```JavaScript
Candy.init('/http-bind/');
// enable ModifyRole plugin
CandyShop.ModifyRole.init();
Candy.Core.connect();
```
## Credits
Thanks to [famfamfam silk icons](http://www.famfamfam.com/lab/icons/silk/) for the icons.
## License
MIT

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,6 @@
#context-menu .add-moderator {
background-image: url(add-moderator.png);
}
#context-menu .remove-moderator {
background-image: url(remove-moderator.png);
}

View File

@ -0,0 +1,97 @@
/** File: candy.js
* Plugin for modifying roles. Currently implemented: op & deop
*
* Authors:
* - Michael Weibel <michael.weibel@gmail.com>
*
* License: MIT
*
* Copyright:
* (c) 2014 Michael Weibel. All rights reserved.
*/
/* global Candy, jQuery, Strophe, $iq */
var CandyShop = (function(self) { return self; }(CandyShop || {}));
/** Class: CandyShop.ModifyRole
* Remove the ignore option in the roster
*/
CandyShop.ModifyRole = (function(self, Candy, $) {
var modifyRole = function modifyRole(role, roomJid, user) {
var conn = Candy.Core.getConnection(),
nick = user.getNick(),
iq = $iq({
'to': Candy.Util.escapeJid(roomJid),
'type': 'set'
});
iq.c('query', {'xmlns': Strophe.NS.MUC_ADMIN})
.c('item', {'nick': nick, 'role': role});
conn.sendIQ(iq.tree());
};
var applyTranslations = function applyTranslations() {
var addModeratorActionLabel = {
'en' : 'Grant moderator status',
'de' : 'Moderator status geben'
};
var removeModeratorActionLabel = {
'en' : 'Remove moderator status',
'de' : 'Moderator status nehmen'
};
$.each(addModeratorActionLabel, function(k, v) {
if(Candy.View.Translation[k]) {
Candy.View.Translation[k].addModeratorActionLabel = v;
}
});
$.each(removeModeratorActionLabel, function(k, v) {
if(Candy.View.Translation[k]) {
Candy.View.Translation[k].removeModeratorActionLabel = v;
}
});
};
var isOwnerOrAdmin = function(user) {
return ['owner', 'admin'].indexOf(user.getAffiliation()) !== -1;
};
var isModerator = function(user) {
return user.getRole() === 'moderator';
};
/** Function: init
* Initializes the plugin by adding an event which modifies
* the contextmenu links.
*/
self.init = function init() {
applyTranslations();
$(Candy).bind('candy:view.roster.context-menu', function(e, args) {
args.menulinks.addModerator = {
requiredPermission: function(user, me) {
return me.getNick() !== user.getNick() && isOwnerOrAdmin(me) && !isOwnerOrAdmin(user) && !isModerator(user);
},
'class' : 'add-moderator',
'label' : $.i18n._('addModeratorActionLabel'),
'callback' : function(e, roomJid, user) {
modifyRole('moderator', roomJid, user);
}
};
args.menulinks.removeModerator = {
requiredPermission: function(user, me) {
return me.getNick() !== user.getNick() && isOwnerOrAdmin(me) && !isOwnerOrAdmin(user) && isModerator(user);
},
'class' : 'remove-moderator',
'label' : $.i18n._('removeModeratorActionLabel'),
'callback' : function(e, roomJid, user) {
modifyRole('participant', roomJid, user);
}
};
});
};
return self;
}(CandyShop.ModifyRole || {}, Candy, jQuery));

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,26 @@
# MUC Room Bar
A plugin for Candy Chat to enable a room bar that displays the room topic and allows moderators to edit it with a click, as well as adds a button to pop up a GUI for inviting users to a MUC.
## Dependencies
Depends on `CandyShop.StaticLobby` for its `Invite` object's `Send` method.
With LeftTabs plugin:
![MUC Room Bar with LeftTabs Plugin](screenshot-left.png)
Without LeftTabs plugin:
![MUC Room Bar without LeftTabs Plugin](screenshot-normal.png)
## Usage
Include the JavaScript and CSS files for the plugin:
```HTML
<script type="text/javascript" src="candyshop/mucroombar/mucroombar.js"></script>
<link rel="stylesheet" type="text/css" href="candyshop/mucroombar/mucroombar.css" />
```
Also be sure to include [Twitter Typeahead](https://github.com/twitter/typeahead.js)'s packaged JS file (includes Bloodhound).
To enable this plugin, add its `init` method after you `init` Candy:
```JavaScript
CandyShop.RoomBar.init();
```

View File

@ -0,0 +1,81 @@
.roombar {
background-color: #1763b0;
border-bottom: 1px solid #e7e7e7;
height: 30px;
padding: 2px 3px;
width: 100%;
z-index: 2;
overflow: hidden;
margin-top: -30px;
}
.roombar .topic {
color: white;
cursor: pointer;
float: left;
font-weight: 1.5em;
font-size: 1.2em;
height: 100%;
padding-left: 5px;
width: 100%;
}
.roombar input[type="text"] {
background-color: rgba(255,255,255,0.1);
border: 1px solid rgba(0,0,0,0.1);
bottom: 1px;
font-weight: 100;
letter-spacing: 2px;
padding: 0 3px;
position: relative;
width: 100%;
}
.message-pane-wrapper {
padding-top: 30px;
margin-bottom: -30px;
padding-bottom: 30px;
}
#chat-rooms .roster-wrapper .pane-heading .invite-users {
float: right;
}
.tt-dropdown-menu {
background: white;
width: 100%;
}
#invite-users-muc input:disabled {
display: none;
}
#invite-users-muc input {
background: white !important;
}
.tagholder {
max-height: 150px;
overflow-y: scroll;
}
.input-tag {
background-color: rgba(23, 99, 176, 0.5);
border: 1px solid rgba(23, 99, 176, 0.69);
border-radius: 6px;
color: white;
cursor: default;
display: block;
margin: 2px;
padding: 2px;
text-shadow: 1px 1px 1px rgba(0,0,0,0.2);
white-space: nowrap;
}
.input-tag .close-input-tag {
padding-left: 10px;
}
.tt-cursor {
background-color: rgba(23,90,176,0.2);
}

View File

@ -0,0 +1,211 @@
/** File: mucroombar.js
* Candy Plugin Auto-Join Incoming MUC Invites
* Author: Melissa Adamaitis <madamei@mojolingo.com>
* Dependency: CandyShop.StaticLobby
*/
var CandyShop = (function(self) { return self; }(CandyShop || {}));
CandyShop.RoomBar = (function(self, Candy, $) {
/** Object: about
*
* Contains:
* (String) name - Candy Plugin Add MUC Management Bar
* (Float) version - Candy Plugin Add MUC Management Bar
*/
self.about = {
name: 'Candy Plugin Add MUC Management Bar',
version: '1.0'
};
/**
* Initializes the RoomBar plugin with the default settings.
*/
self.init = function() {
// Add a room bar when the room is first created.
$(Candy).on('candy:view.room.after-show', function(ev, obj) {
CandyShop.RoomBar.addRoomBar(obj);
CandyShop.RoomBar.appendInviteUsersButton(obj.roomJid);
return undefined;
});
// Change the topic in the roombar when it is changed.
$(Candy).on('candy:view.room.after-subject-change', function(ev, obj) {
CandyShop.RoomBar.showTopic(obj.subject, obj.element);
});
// Remove the now-useless "Change Subject" menu item
$(Candy).on('candy:view.roster.context-menu', function (ev, obj) {
delete obj.menulinks.subject;
});
};
self.addRoomBar = function(obj){
if($('div.room-pane.roomtype-groupchat[data-roomjid="' + obj.roomJid + '"] .message-pane-wrapper .roombar').length === 0) {
var roombarHtml = self.Template.roombar;
$('div.room-pane.roomtype-groupchat[data-roomjid="' + obj.roomJid + '"] .message-pane-wrapper').prepend(roombarHtml);
}
$('#' + obj.element.context.id + ' .message-pane-wrapper .roombar .topic').click(function() {
self.updateRoomTopic(obj.roomJid, obj.element.context.id, $(this).html());
});
};
self.showTopic = function(topic, element) {
$(element).find(' .message-pane-wrapper .roombar .topic').html(topic);
};
self.updateRoomTopic = function(roomJid, elementId, currentTopic) {
// If we're a room moderator, be able to edit the room topic.
if(Candy.Core.getRoom(roomJid) !== null && Candy.Core.getRoom(roomJid).user !== null && Candy.Core.getRoom(roomJid).user.getRole() === 'moderator') {
// If there isn't an active input for room topic already, create input interface.
if($('#' + elementId + ' .message-pane-wrapper .roombar .topic input').length === 0) {
// Replace topic with an input field
if(currentTopic === ' ') { currentTopic = ''; }
var fieldHtml = '<input type="text" value="' + currentTopic + '" />';
$('#' + elementId + ' .message-pane-wrapper .roombar .topic').html(fieldHtml);
// Add focus to the new element.
$('#' + elementId + ' .message-pane-wrapper .roombar .topic input').focus();
// Set listener for on return press or lose focus.
$('#' + elementId + ' .message-pane-wrapper .roombar .topic input').blur(function() {
if(currentTopic !== $(this).val()) {
CandyShop.RoomBar.sendNewTopic(roomJid, $(this).val());
} else {
$('#' + elementId + ' .message-pane-wrapper .roombar .topic').html(currentTopic);
}
});
$('#' + elementId + ' .message-pane-wrapper .roombar .topic input').keypress(function(ev) {
var keycode = (ev.keyCode ? ev.keyCode : ev.which);
if(keycode === 13) {
if(currentTopic !== $(this).val()) {
CandyShop.RoomBar.sendNewTopic(roomJid, $(this).val());
} else {
$('#' + elementId + ' .message-pane-wrapper .roombar .topic').html(currentTopic);
}
}
});
}
}
};
self.appendInviteUsersButton = function(roomJid) {
var paneHeading = $('#chat-rooms > div.roomtype-groupchat[data-roomjid="' + roomJid + '"] .roster-wrapper .pane-heading');
if ($(paneHeading).find('.invite-users').length === 0) {
var html = self.Template.inviteButton;
$(paneHeading).append(html);
$(paneHeading).find('.invite-users').click(function() {
// Pop up a modal with an invite-users dialogue.
Candy.View.Pane.Chat.Modal.show(Mustache.to_html(self.Template.inviteModal, {
roomjid: roomJid
}), true, false);
self.centerModal(true);
// Bloodhound suggestion engine
var bhUsers = new Bloodhound({
name: 'users',
local: $.map(Candy.Core.getRoster().items, function(item) {
return { name: item.getName(), jid: item.getJid() };
}),
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.name);
},
queryTokenizer: Bloodhound.tokenizers.whitespace
});
bhUsers.initialize();
// Typeahead UI
$('#users-input').typeahead({
itemValue: 'jid',
itemText: 'name',
hint: true,
highlight: true,
minLength: 1
},{
name: 'users',
displayKey: 'name',
source: bhUsers.ttAdapter()
});
// Add a new place for tags to go
$('#users-input').before(self.Template.tagholder);
// Bind the selection event for typeahead.
$('#users-input').bind('typeahead:selected', function(ev, suggestion) {
// Append the tag
if ($('.tagholder .input-tag[data-userjid="' + suggestion.jid + '"]').length === 0) {
$('.tagholder').append(Mustache.to_html(self.Template.tag, {
userjid: suggestion.jid,
username: suggestion.name
}));
}
$('#users-input').val('');
self.centerModal();
$('.tagholder').scrollTop($('.tagholder').height());
// Add remove button click handler
$('.tagholder .input-tag .close-input-tag').click(function() {
$(this).parent().remove();
});
});
// Form submission handler
$('#invite-users-muc').submit(function(ev) {
ev.preventDefault();
// Get all of the users chosen.
var userTags = $('.tagholder .input-tag');
// Send them invites.
for (var i = 0; i < userTags.length; i++) {
CandyShop.StaticLobby.Invite.Send($(userTags[i]).attr('data-userjid'), roomJid);
$('.tagholder .input-tag[data-userjid="' + $(userTags[i]).attr('data-userjid') + '"]').remove();
}
Candy.View.Pane.Chat.Modal.hide();
return false;
});
});
}
};
self.centerModal = function(first) {
// Center the modal better
var windowHeight = $(window).height(),
windowWidth = $(window).width(),
objectHeight = $('#chat-modal').outerHeight(),
objectWidth = $('#chat-modal').outerWidth(),
newTop = (windowHeight / 2) - (objectHeight / 2),
newLeft = (windowWidth / 2) + (objectWidth / 2);
if (first) {
$('#chat-modal').css({
left: newLeft,
top: newTop
});
} else {
$('#chat-modal').animate({
left: newLeft,
top: newTop
}, 'fast');
}
};
// Display the set topic modal and add submit handler.
self.sendNewTopic = function(roomJid, topic) {
if(topic === '') { topic = ' '; }
// Even though it does the exact same thing, Candy.View.Pane.Room.setSubject(roomJid, topic) was not sending the stanza out.
Candy.Core.getConnection().muc.setTopic(Candy.Util.escapeJid(roomJid), topic);
};
self.Template = {
tagholder: '<div class="tagholder"></div>',
tag: '<span class="input-tag" data-userjid={{userjid}}>{{username}}<span class="close-input-tag">x</span></span>',
roombar: '<div class="roombar"><div class="topic"></div></div>',
inviteButton: '<button class="invite-users btn btn-default btn-sm">Invite Users</button>',
inviteModal: '<h4>Invite Users</h4><form id="invite-users-muc" data-roomjid={{roomjid}}><div class="form-group">' +
'<input type="text" name="bhUsers" class="tm-input form-control" ' +
'id="users-input"/></div><button class="btn btn-default" type="submit">Send Invitations</button></form>'
};
return self;
}(CandyShop.RoomBar || {}, Candy, jQuery));

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -0,0 +1,31 @@
# Name completion plugin
This plugin will complete the names of users in the room when a specified key is pressed.
### Usage
<script type="text/javascript" src="path_to_plugins/namecomplete/candy.js"></script>
<link rel="stylesheet" type="text/css" href="path_to_plugins/namecomplete/candy.css" />
...
CandyShop.NameComplete.init();
### Configuration options
`nameIdentifier` - String - The identifier to look for in a string. Defaults to `'@'`
`completeKeyCode` - Integer - The key code of the key to use. Defaults to `9` (tab)
### Example configurations
// complete the name when the user types +nick and hits the right arrow
// +troymcc -> +troymccabe
CandyShop.NameComplete.init({
nameIdentifier: '+',
completeKeyCode: '39'
});
// complete the name when the user types -nick and hits the up arrow
// +troymcc ^ +troymccabe
CandyShop.NameComplete.init({
nameIdentifier: '-',
completeKeyCode: '38'
});

View File

@ -0,0 +1,7 @@
#context-menu li.selected {
background-color: #ccc;
}
#context-menu li.candy-namecomplete-option {
padding: 3px 5px;
}

View File

@ -0,0 +1,260 @@
/** File: candy.js
* Candy - Chats are not dead yet.
*
* Authors:
* - Troy McCabe <troy.mccabe@geeksquad.com>
* - Ben Klang <bklang@mojolingo.com>
*
* Copyright:
* (c) 2012 Geek Squad. All rights reserved.
* (c) 2014 Power Home Remodeling Group. All rights reserved.
*/
/* global document, Candy, jQuery */
var CandyShop = (function(self) { return self; }(CandyShop || {}));
/** Class: CandyShop.NameComplete
* Allows for completion of a name in the roster
*/
CandyShop.NameComplete = (function(self, Candy, $) {
/** Object: _options
* Options:
* (String) nameIdentifier - Prefix to append to a name to look for. '@' now looks for '@NICK', '' looks for 'NICK', etc. Defaults to '@'
* (Integer) completeKeyCode - Which key to use to complete
*/
var _options = {
nameIdentifier: '@',
completeKeyCode: 9
};
/** Array: _nicks
* An array of nicks to complete from
* Populated after 'candy:core.presence'
*/
var _nicks = [];
/** String: _selector
* The selector for the visible message box
*/
var _selector = 'input[name="message"]:visible';
/** Boolean:_autocompleteStarted
* Keeps track of whether we're in the middle of autocompleting a name
*/
var _autocompleteStarted = false;
/** Function: init
* Initialize the NameComplete plugin
* Show options for auto completion of names
*
* Parameters:
* (Object) options - Options to apply to this plugin
*/
self.init = function(options) {
// apply the supplied options to the defaults specified
$.extend(true, _options, options);
// listen for keydown when autocomplete options exist
$(document).on('keypress', _selector, function(e) {
if (e.which === _options.nameIdentifier.charCodeAt()) {
_autocompleteStarted = true;
}
if (_autocompleteStarted) {
// update the list of nicks to grab
self.populateNicks();
// set up the vars for this method
// break it on spaces, and get the last word in the string
var field = $(this);
var msgParts = field.val().split(' ');
var lastWord = new RegExp( "^" + msgParts[msgParts.length - 1] + String.fromCharCode(e.which), "i");
var matches = [];
// go through each of the nicks and compare it
$(_nicks).each(function(index, item) {
// if we have results
if (item.match(lastWord) !== null) {
matches.push(item);
}
});
// if we only have one match, no need to show the picker, just replace it
// else show the picker of the name matches
if (matches.length === 1) {
self.replaceName(matches[0]);
// Since the name will be autocompleted, throw away the last character
e.preventDefault();
} else if (matches.length > 1) {
self.showPicker(matches, field);
}
}
});
};
/** Function: keyDown
* The listener for keydown in the menu
*/
self.keyDown = function(e) {
// get the menu and the content element
var menu = $('#context-menu');
var content = menu.find('ul');
var selected = content.find('li.selected');
if(menu.css('display') === 'none') {
$(document).unbind('keydown', self.keyDown);
return;
}
// switch the key code
switch (e.which) {
// up arrow
case 38:
// down arrow
case 40:
var newEl;
if (e.which === 38) {
// move the selected thing up
newEl = selected.prev();
} else {
// move the selected thing down
newEl = selected.next();
}
// Prevent going off either end of the list
if ($(newEl).length > 0) {
selected.removeClass('selected');
newEl.addClass('selected');
}
// don't perform any key actions
e.preventDefault();
break;
// esc key
case 27:
// delete Key
case 8:
case 46:
self.endAutocomplete();
break;
// the key code for completion
case _options.completeKeyCode:
case 13:
// get the text of the selected item
var val = content.find('li.selected').text();
// replace the last item with the selected item
self.replaceName(val);
// don't perform any key actions
e.preventDefault();
break;
}
};
/** Function: endAutocomplete
* Disables autocomplete mode, hiding the context menu
*/
self.endAutocomplete = function() {
_autocompleteStarted = false;
$(_selector).unbind('keydown', self.keyDown);
$('#context-menu').hide();
};
/** Function: selectOnClick
* The listener for click on decision in the menu
*
* Parameters:
* (Event) e - The click event
*/
self.selectOnClick = function(e) {
self.replaceName($(e.currentTarget).text());
$(_selector).focus();
e.preventDefault();
};
/** Function: populateNicks
* Populate the collection of nicks to autocomplete from
*/
self.populateNicks = function() {
// clear the nick collection
_nicks = [];
// grab the roster in the current room
var room = Candy.Core.getRoom(Candy.View.getCurrent().roomJid);
if (room !== null) {
var roster = room.getRoster().getAll();
// iterate and add the nicks to the collection
$.each(roster, function(index, item) {
_nicks.push(_options.nameIdentifier + item.getNick());
});
}
};
/** Function: replaceName
*
*/
self.replaceName = function(replaceText) {
// get the parts of the message
var $msgBox = $(_selector);
var msgParts = $msgBox.val().split(' ');
// If the name is the first word, add a colon to the end
if (msgParts.length === 1) {
replaceText += ": ";
} else {
replaceText += " ";
}
// replace the last part with the item
msgParts[msgParts.length - 1] = replaceText;
// put the string back together on spaces
$msgBox.val(msgParts.join(' '));
self.endAutocomplete();
};
/** Function: showPicker
* Show the picker for the list of names that match
*/
self.showPicker = function(matches, elem) {
// get the element
elem = $(elem);
// get the necessary items
var pos = elem.offset(),
menu = $('#context-menu'),
content = $('ul', menu),
i;
// clear the content if needed
content.empty();
// add the matches to the list
for(i = 0; i < matches.length; i++) {
content.append('<li class="candy-namecomplete-option">' + matches[i] + '</li>');
}
// select the first item
$(content.find('li')[0]).addClass('selected');
content.find('li').click(self.selectOnClick);
// bind the keydown to move around the menu
$(_selector).bind('keydown', self.keyDown);
var posLeft = elem.val().length * 7,
posTop = Candy.Util.getPosTopAccordingToWindowBounds(menu, pos.top);
// show it
menu.css({'left': posLeft, 'top': posTop.px, backgroundPosition: posLeft.backgroundPositionAlignment + ' ' + posTop.backgroundPositionAlignment});
menu.fadeIn('fast');
return true;
};
return self;
}(CandyShop.NameComplete || {}, Candy, jQuery));

View File

@ -0,0 +1,29 @@
# Notifications
Send HTML5 Notifications when a message is received and the window is not in focus. This only works with webkit browsers.
## Usage
To enable *Notifications* you have to include its JavaScript code and stylesheet:
```HTML
<script type="text/javascript" src="candyshop/notifications/candy.js"></script>
```
Call its `init()` method after Candy has been initialized:
```JavaScript
Candy.init('/http-bind/');
CandyShop.Notifications.init();
Candy.Core.connect();
```
It is possible to configure the Plugin.
```JavaScript
CandyShop.Notifications.init({
notifyNormalMessage: false, // Send a notification for every message. Defaults to false
notifyPersonalMessage: true, // Send a notification if the user is mentioned. (Requires NotfiyMe Plugin) Defaults to true
closeTime: 3000 // Close notification after X milliseconds. Zero means it doesn't close automaticly. Defaults to 3000
});
```

View File

@ -0,0 +1,111 @@
/*
* HTML5 Notifications
* @version 1.0
* @author Jonatan Männchen <jonatan@maennchen.ch>
* @author Melissa Adamaitis <madamei@mojolingo.com>
*
* Notify user if new messages come in.
*/
var CandyShop = (function(self) { return self; }(CandyShop || {}));
CandyShop.Notifications = (function(self, Candy, $) {
/** Object: _options
* Options for this plugin's operation
*
* Options:
* (Boolean) notifyNormalMessage - Notification on normalmessage. Defaults to false
* (Boolean) notifyPersonalMessage - Notification for private messages. Defaults to true
* (Boolean) notifyMention - Notification for mentions. Defaults to true
* (Integer) closeTime - Time until closing the Notification. (0 = Don't close) Defaults to 3000
* (String) title - Title to be used in notification popup. Set to null to use the contact's name.
* (String) icon - Path to use for image/icon for notification popup.
*/
var _options = {
notifyNormalMessage: false,
notifyPersonalMessage: true,
notifyMention: true,
closeTime: 3000,
title: null,
icon: window.location.origin + '/' + Candy.View.getOptions().assets + '/img/favicon.png'
};
/** Function: init
* Initializes the notifications plugin.
*
* Parameters:
* (Object) options - The options to apply to this plugin
*
* @return void
*/
self.init = function(options) {
// apply the supplied options to the defaults specified
$.extend(true, _options, options);
// Just init if notifications are supported
if (window.Notification) {
// Setup Permissions (has to be kicked on with some user-events)
jQuery(document).one('click keydown', self.setupPermissions);
// Add Listener for Notifications
$(Candy).on('candy:view.message.notify', self.handleNotification);
}
};
/** Function: checkPermissions
* Check if the plugin has permission to send notifications.
*
* @return boid
*/
self.setupPermissions = function() {
// Check if permissions is given
if (window.Notification !== 0) { // 0 is PERMISSION_ALLOWED
// Request for it
window.Notification.requestPermission();
}
};
/** Function: handleNotification
* Descriptions
*
* Parameters:
* (Array) args
*
* @return void
*/
self.handleNotification = function(e, args) {
// Check if window has focus, so no notification needed
if (!document.hasFocus()) {
if(_options.notifyNormalMessage ||
(self.mentionsMe(args.message) && _options.notifyMention) ||
(_options.notifyPersonalMessage && Candy.View.Pane.Chat.rooms[args.roomJid].type === 'chat')) {
// Create the notification.
var title = !_options.title ? args.name : _options.title ,
notification = new window.Notification(title, {
icon: _options.icon,
body: args.message
});
// Close it after 3 Seconds
if(_options.closeTime) {
window.setTimeout(function() { notification.close(); }, _options.closeTime);
}
}
}
};
self.mentionsMe = function(message) {
var message = message.toLowerCase(),
nick = Candy.Core.getUser().getNick().toLowerCase(),
cid = Strophe.getNodeFromJid(Candy.Core.getUser().getJid()).toLowerCase(),
jid = Candy.Core.getUser().getJid().toLowerCase();
if (message.indexOf(nick) === -1 &&
message.indexOf(cid) === -1 &&
message.indexOf(jid) === -1) {
return false;
}
return true;
};
return self;
}(CandyShop.Notifications || {}, Candy, jQuery));

View File

@ -0,0 +1,32 @@
# Notify me plugin
This plugin will notify users when their names are mentioned and prefixed with a specific token
### Usage
<script type="text/javascript" src="path_to_plugins/notifyme/candy.js"></script>
<link rel="stylesheet" type="text/css" href="path_to_plugins/notifyme/candy.css" />
...
CandyShop.NotifyMe.init();
### Configuration options
`nameIdentifier` - String - The identifier to look for in a string. Defaults to `'@'`
`playSound` - Boolean - Whether to play a sound when the username is mentioned. Defaults to `true`
`highlightInRoom` - Boolean - Whether to highlight the username when it is mentioned. Defaults to `true`
`normalizeNickname` - Boolean - Whether to normalize the casing of the nickname to the way you entered it. Otherwise, leave the casing as the sender wrote it. Defaults to `true`
### Example configurations
// Highlight my name when it's prefixed with a '+'
CandyShop.NotifyMe.init({
nameIdentifier: '+',
playSound: false
});
// Highlight and play a sound if my name is prefixed with a '-'
CandyShop.NotifyMe.init({
nameIdentifier: '-'
});

View File

@ -0,0 +1,3 @@
.candy-notifyme-highlight {
background: #FFFF00;
}

View File

@ -0,0 +1,96 @@
/** File: candy.js
* Candy - Chats are not dead yet.
*
* Authors:
* - Troy McCabe <troy.mccabe@geeksquad.com>
*
* Copyright:
* (c) 2012 Geek Squad. All rights reserved.
*/
/* global Candy, jQuery */
var CandyShop = (function(self) { return self; }(CandyShop || {}));
/** Class: CandyShop.NotifyMe
* Notifies with a sound and highlights the text in the chat when a nick is called out
*/
CandyShop.NotifyMe = (function(self, Candy, $) {
/** Object: _options
* Options for this plugin's operation
*
* Options:
* (String) nameIdentifier - Prefix to append to a name to look for. '@' now looks for '@NICK', '' looks for 'NICK', etc. Defaults to '@'
* (Boolean) playSound - Whether to play a sound when identified. Defaults to true
* (Boolean) highlightInRoom - Whether to highlight the name in the room. Defaults to true
* (Boolean) normalizeNickname - Whether to normalize the casing of the nickname to the way you entered it. Otherwise, leave the casing as the sender wrote it. Defaults to true
*/
var _options = {
nameIdentifier: '@',
playSound: true,
highlightInRoom: true,
normalizeNickname: true
};
var _getNick = function() {
return Candy.Core.getUser().getNick();
};
var _getSearchTerm = function() {
// make it what is searched
// search for <identifier>name in the whole message
return _options.nameIdentifier + _getNick();
};
/** Function: init
* Initialize the NotifyMe plugin
* Bind to beforeShow, play sound and higlight if specified
*
* Parameters:
* (Object) options - The options to apply to this plugin
*/
self.init = function(options) {
// apply the supplied options to the defaults specified
$.extend(true, _options, options);
// bind to the beforeShow event
$(Candy).on('candy:view.message.before-show', function(e, args) {
var searchRegExp = new RegExp('^(.*)(\s?' + _getSearchTerm() + ')', 'ig');
// if it's in the message and it's not from me, do stuff
// I wouldn't want to say 'just do @{MY_NICK} to get my attention' and have it knock...
if (searchRegExp.test(args.message) && args.name != _getNick()) {
// play the sound if specified
if (_options.playSound) {
Candy.View.Pane.Chat.Toolbar.playSound();
}
// Save that I'm mentioned in args
args.forMe = true;
}
return args.message;
});
// bind to the beforeShow event
$(Candy).on('candy:view.message.before-render', function(e, args) {
var searchTerm = _getSearchTerm();
var searchMatch = new RegExp('^(.*)(\s?' + searchTerm + ')', 'ig').exec(args.templateData.message);
// if it's in the message and it's not from me, do stuff
// I wouldn't want to say 'just do @{MY_NICK} to get my attention' and have it knock...
if (searchMatch != null && args.templateData.name != _getNick()) {
// highlight if specified
if (_options.highlightInRoom) {
var displayNickName = searchTerm;
if (!_options.normalizeNickname) {
displayNickName = searchMatch[2];
}
args.templateData.message = args.templateData.message.replace(searchMatch[2], '<span class="candy-notifyme-highlight">' + displayNickName + '</span>');
}
}
});
};
return self;
}(CandyShop.NotifyMe || {}, Candy, jQuery));

View File

@ -0,0 +1,49 @@
{
"name": "candy-shop",
"version": "1.0.0",
"description": "Multi-user XMPP web client plugins",
"directories": {},
"scripts": {
"test": "grunt ci"
},
"repository": {
"type": "git",
"url": "git://github.com/candy-chat/candy-plugins.git"
},
"keywords": [
"xmpp",
"muc",
"multi-user",
"websocket",
"bosh",
"chat"
],
"contributors": [
{
"name": "Michael Weibel",
"email": "michael.weibel@gmail.com"
},
{
"name": "Patrick Stadler",
"email": "patrick.stadler@gmail.com",
"url": "http://pstadler.sh"
}
],
"license": "MIT",
"bugs": {
"url": "https://github.com/candy-chat/candy-plugins/issues"
},
"homepage": "http://candy-chat.github.io/candy/",
"devDependencies": {
"grunt": "^0.4.5",
"grunt-contrib-jshint": "^0.10.0",
"grunt-contrib-watch": "^0.6.1",
"grunt-coveralls": "^0.3.0",
"intern": "^2.0.1",
"jshint-stylish": "^0.2.0",
"sinon": "git+https://github.com/cjohansen/Sinon.JS.git",
"sinon-chai": "^2.5.0",
"grunt-todo": "~0.4.0",
"grunt-clear": "~0.2.1"
}
}

View File

@ -0,0 +1,20 @@
#Candy Timeago plugin
This plugin replaces the exact time/date with 'fuzzy timestamps' (e.g. 'less than a minute ago', '2 minutes ago', 'about an hour ago'). The timestamps update dynamically. All the heavy lifting is done by Ryan McGeary's excellent jQuery Timeago plugin (http://timeago.yarp.com/).
##Usage
To enable Timeago include it's JavaScript code and CSS file (after the main Candy script and CSS):
```html
<script type="text/javascript" src="candyshop/timeago/candy.js"></script>
<link rel="stylesheet" type="text/css" href="candyshop/timeago/candy.css" />
```
Then call its init() method after Candy has been initialized:
```html
Candy.init('/http-bind/');
CandyShop.Timeago.init();
Candy.Core.connect();
```

View File

@ -0,0 +1,3 @@
.message-pane li abbr {
border-bottom: none;
}

View File

@ -0,0 +1,192 @@
/*
* candy-timeago-plugin
* @version 0.1 (2011-07-15)
* @author David Devlin (dave.devlin@gmail.com)
*
* Integrates the jQuery Timeago plugin (http://timeago.yarp.com/) with Candy.
*/
/* global document, Candy, jQuery */
var CandyShop = (function(self) { return self; }(CandyShop || {}));
CandyShop.Timeago = (function(self, Candy, $) {
self.init = function() {
Candy.View.Template.Chat.adminMessage = '<li><small><abbr title="{{time}}">{{time}}</abbr></small><div class="adminmessage"><span class="label">{{sender}}</span><span class="spacer">▸</span>{{subject}} {{message}}</div></li>';
Candy.View.Template.Chat.infoMessage = '<li><small><abbr title="{{time}}">{{time}}</abbr></small><div class="infomessage"><span class="spacer">•</span>{{subject}} {{message}}</div></li>';
Candy.View.Template.Room.subject = '<li><small><abbr title="{{time}}">{{time}}</abbr></small><div class="subject"><span class="label">{{roomName}}</span><span class="spacer">▸</span>{{_roomSubject}} {{subject}}</div></li>';
Candy.View.Template.Message.item = '<li><small><abbr title="{{time}}">{{time}}</abbr></small><div><a class="label" href="#" class="name">{{displayName}}</a><span class="spacer">▸</span>{{{message}}}</div></li>';
Candy.Util.localizedTime = function(dateTime) {
if (dateTime === undefined) {
return undefined;
}
var date = Candy.Util.iso8601toDate(dateTime);
return date.format($.i18n._('isoDateTime'));
};
var applyTimeago = function(e, args) {
var $elem = args.element ? $('abbr', args.element) : $('abbr');
$elem.timeago();
};
$(Candy).on('candy:view.message.after-show', applyTimeago);
$(Candy).on('candy:view.room.after-subject-change', applyTimeago);
// the following handlers run timeago() on all <abbr> tags
$(Candy).on('candy:core.presence.room', applyTimeago);
$(Candy).on('candy:view.chat.admin-message', applyTimeago);
};
return self;
}(CandyShop.Timeago || {}, Candy, jQuery));
/*
* timeago: a jQuery plugin, version: 0.9.3 (2011-01-21)
* @requires jQuery v1.2.3 or later
*
* Timeago is a jQuery plugin that makes it easy to support automatically
* updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
*
* For usage and examples, visit:
* http://timeago.yarp.com/
*
* Licensed under the MIT:
* http://www.opensource.org/licenses/mit-license.php
*
* Copyright (c) 2008-2011, Ryan McGeary (ryanonjavascript -[at]- mcgeary [*dot*] org)
*/
(function($) {
$.timeago = function(timestamp) {
if (timestamp instanceof Date) {
return inWords(timestamp);
} else if (typeof timestamp === "string") {
return inWords($.timeago.parse(timestamp));
} else {
return inWords($.timeago.datetime(timestamp));
}
};
var $t = $.timeago;
$.extend($.timeago, {
settings: {
refreshMillis: 60000,
allowFuture: false,
strings: {
prefixAgo: null,
prefixFromNow: null,
suffixAgo: "ago",
suffixFromNow: "from now",
seconds: "less than a minute",
minute: "about a minute",
minutes: "%d minutes",
hour: "about an hour",
hours: "about %d hours",
day: "a day",
days: "%d days",
month: "about a month",
months: "%d months",
year: "about a year",
years: "%d years",
numbers: []
}
},
inWords: function(distanceMillis) {
var $l = this.settings.strings;
var prefix = $l.prefixAgo;
var suffix = $l.suffixAgo;
if (this.settings.allowFuture) {
if (distanceMillis < 0) {
prefix = $l.prefixFromNow;
suffix = $l.suffixFromNow;
}
distanceMillis = Math.abs(distanceMillis);
}
var seconds = distanceMillis / 1000;
var minutes = seconds / 60;
var hours = minutes / 60;
var days = hours / 24;
var years = days / 365;
function substitute(stringOrFunction, number) {
var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
var value = ($l.numbers && $l.numbers[number]) || number;
return string.replace(/%d/i, value);
}
var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
seconds < 90 && substitute($l.minute, 1) ||
minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
minutes < 90 && substitute($l.hour, 1) ||
hours < 24 && substitute($l.hours, Math.round(hours)) ||
hours < 48 && substitute($l.day, 1) ||
days < 30 && substitute($l.days, Math.floor(days)) ||
days < 60 && substitute($l.month, 1) ||
days < 365 && substitute($l.months, Math.floor(days / 30)) ||
years < 2 && substitute($l.year, 1) ||
substitute($l.years, Math.floor(years));
return $.trim([prefix, words, suffix].join(" "));
},
parse: function(iso8601) {
var s = $.trim(iso8601);
s = s.replace(/\.\d\d\d+/,""); // remove milliseconds
s = s.replace(/-/,"/").replace(/-/,"/");
s = s.replace(/T/," ").replace(/Z/," UTC");
s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
return new Date(s);
},
datetime: function(elem) {
// jQuery's `is()` doesn't play well with HTML5 in IE
var isTime = $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
var iso8601 = isTime ? $(elem).attr("datetime") : $(elem).attr("title");
return $t.parse(iso8601);
}
});
$.fn.timeago = function() {
var self = this;
self.each(refresh);
var $s = $t.settings;
if ($s.refreshMillis > 0) {
setInterval(function() { self.each(refresh); }, $s.refreshMillis);
}
return self;
};
function refresh() {
var data = prepareData(this);
if (!isNaN(data.datetime)) {
$(this).text(inWords(data.datetime));
}
return this;
}
function prepareData(element) {
element = $(element);
if (!element.data("timeago")) {
element.data("timeago", { datetime: $t.datetime(element) });
var text = $.trim(element.text());
if (text.length > 0) {
element.attr("title", text);
}
}
return element.data("timeago");
}
function inWords(date) {
return $t.inWords(distance(date));
}
function distance(date) {
return (new Date().getTime() - date.getTime());
}
// fix for IE6 suckage
document.createElement("abbr");
document.createElement("time");
}(jQuery));

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@ -0,0 +1,22 @@
# Typing Notifications
A plugin for Candy Chat to enable typing notifications to show up. Fully compatible with the lefttabs plugin.
## Todo
It would be nice to extend this to groupchat as well. Currenly only working for private chat. (Simpler.)
![Typing Notifications - Regular](screenshot1.png)
![Typing Notifications - Left Tabs](screenshot2.png)
## Usage
Include the JavaScript and CSS files:
```HTML
<script type="text/javascript" src="candyshop/typingnotifications/typingnotifications.js"></script>
<link rel="stylesheet" type="text/css" href="candyshop/typingnotifications/typingnotifications.css" />
```
To enable this typing notifications plugin, add its `init` method after you `init` Candy, but before `Candy.connect()`:
```JavaScript
CandyShop.TypingNotifications.init();
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -0,0 +1,16 @@
/**
* TypingNotifications CSS
*
* Author: Melissa Adamaitis <madamei@mojolingo.com>
*/
.message-pane-wrapper {
padding-bottom: 5px;
}
.typing-notification-area {
position: fixed;
bottom: 34px;
color: #ADADAD;
font-style: italic;
margin-left: 7px;
font-size: 0.8em;
}

View File

@ -0,0 +1,55 @@
/** File: typingnotifications.js
* Candy Plugin Typing Notifications
* Author: Melissa Adamaitis <madamei@mojolingo.com>
*/
var CandyShop = (function(self) { return self; }(CandyShop || {}));
CandyShop.TypingNotifications = (function(self, Candy, $) {
/** Object: about
*
* Contains:
* (String) name - Candy Plugin Typing Notifications
* (Float) version - Candy Plugin Typing Notifications
*/
self.about = {
name: 'Candy Plugin Typing Notifications',
version: '1.0'
};
/**
* Initializes the Typing Notifications plugin with the default settings.
*/
self.init = function(){
// After a room is added, make sure to tack on a little div that we can put the typing notification into.
$(Candy).on('candy:view.private-room.after-open', function(ev, obj){
self.addTypingNotificationDiv(obj);
});
// When a typing notification is recieved, display it.
$(Candy).on('candy:core.message.chatstate', function(ev, obj) {
var pane, chatstate_string;
pane = Candy.View.Pane.Room.getPane(obj.roomJid);
chatstate_string = self.getChatstateString(obj.chatstate, obj.name);
$(pane).find('.typing-notification-area').html(chatstate_string);
return true;
});
};
self.getChatstateString = function(chatstate, name) {
switch (chatstate) {
case 'paused': return name + ' has entered text.';
case 'inactive': return name + ' is away from the window.';
case 'composing': return name + ' is composing...';
case 'gone': return name + ' has closed the window.';
default: return '';
}
};
self.addTypingNotificationDiv = function(obj){
var pane_html = Candy.View.Pane.Room.getPane(obj.roomJid),
typing_notification_div_html = '<div class="typing-notification-area"></div>';
$(pane_html).find('.message-form-wrapper').append(typing_notification_div_html);
};
return self;
}(CandyShop.TypingNotifications || {}, Candy, jQuery));

View File

@ -167,11 +167,11 @@ ul {
}
#chat-statusmessage-control {
background: url(img/action/statusmessage-off.png);
background-image: url(img/action/statusmessage-off.png);
}
#chat-statusmessage-control.checked {
background: url(img/action/statusmessage-on.png);
background-image: url(img/action/statusmessage-on.png);
}
#chat-toolbar .usercount {
@ -575,7 +575,7 @@ ul {
width: 15px;
}
#chat-modal {
#chat-modal.modal-common {
background: #eee;
width: 300px;
padding: 20px 5px;
@ -641,6 +641,21 @@ ul {
color: #333;
}
#chat-modal.login-with-domains {
width: 650px;
margin-left: -330px;
}
#chat-modal span.at-symbol {
float: left;
padding: 6px;
font-size: 14px;
}
#chat-modal select[name=domain] {
width: 320px;
}
#chat-modal label {
text-align: right;
padding-right: 1em;

Binary file not shown.

Binary file not shown.

Binary file not shown.

54
content/static/candy/setup.sh Executable file
View File

@ -0,0 +1,54 @@
#!/usr/bin/env bash
#
# Easy installation for contributing to candy
#
# Copyright 2014 Michael Weibel <michael.weibel@gmail.com>
# License: MIT
#
# Show errors in case of undefined variables
set -o nounset
echo
echo "Welcome to the Candy Vagrant setup"
echo
echo "This script will setup a Vagrant box with development dependencies on it."
echo "It will also build Candy and run tests to verify that everything is working."
echo
echo "In case of an error, use 'install.log' for log informations."
echo
touch install.log
echo "" > install.log
echo -n "* Booting Vagrant box (this might take a while)..."
if vagrant up --no-provision >> install.log 2>&1
then echo "done"
else
echo "failed!"
echo "Do you have 'vagrant' installed in your PATH?"
echo "Please check install.log"
echo
echo "Aborting"
exit 2
fi
echo -n "* Provisioning Vagrant box (this might take a few minutes)..."
if vagrant provision >> install.log 2>&1
then echo "done"
else
echo "failed!"
echo "Please check install.log"
echo
echo "Aborting"
exit 2
fi
echo -n "* Building Candy and running tests..."
vagrant ssh -c "cd /vagrant && grunt && grunt test"
echo
echo "Candy is now running on http://localhost:8080"
echo
exit 0

View File

@ -1,56 +0,0 @@
/** File: candy.js
* Candy - Chats are not dead yet.
*
* Authors:
* - Patrick Stadler <patrick.stadler@gmail.com>
* - Michael Weibel <michael.weibel@gmail.com>
*
* Copyright:
* (c) 2011 Amiado Group AG. All rights reserved.
* (c) 2012-2014 Patrick Stadler & Michael Weibel. All rights reserved.
*/
'use strict';
/* global jQuery */
/** Class: Candy
* Candy base class for initalizing the view and the core
*
* Parameters:
* (Candy) self - itself
* (jQuery) $ - jQuery
*/
var Candy = (function(self, $) {
/** Object: about
* About candy
*
* Contains:
* (String) name - Candy
* (Float) version - Candy version
*/
self.about = {
name: 'Candy',
version: '1.7.1'
};
/** Function: init
* Init view & core
*
* Parameters:
* (String) service - URL to the BOSH interface
* (Object) options - Options for candy
*
* Options:
* (Boolean) debug - Debug (Default: false)
* (Array|Boolean) autojoin - Autojoin these channels. When boolean true, do not autojoin, wait if the server sends something.
*/
self.init = function(service, options) {
if (!options.viewClass) {
options.viewClass = self.View;
}
options.viewClass.init($('#candy'), options.view);
self.Core.init(service, options.core);
};
return self;
}(Candy || {}, jQuery));

View File

@ -1,415 +0,0 @@
/** File: core.js
* Candy - Chats are not dead yet.
*
* Authors:
* - Patrick Stadler <patrick.stadler@gmail.com>
* - Michael Weibel <michael.weibel@gmail.com>
*
* Copyright:
* (c) 2011 Amiado Group AG. All rights reserved.
* (c) 2012-2014 Patrick Stadler & Michael Weibel. All rights reserved.
*/
'use strict';
/* global Candy, window, Strophe, jQuery */
/** Class: Candy.Core
* Candy Chat Core
*
* Parameters:
* (Candy.Core) self - itself
* (Strophe) Strophe - Strophe JS
* (jQuery) $ - jQuery
*/
Candy.Core = (function(self, Strophe, $) {
/** PrivateVariable: _connection
* Strophe connection
*/
var _connection = null,
/** PrivateVariable: _service
* URL of BOSH service
*/
_service = null,
/** PrivateVariable: _user
* Current user (me)
*/
_user = null,
/** PrivateVariable: _rooms
* Opened rooms, containing instances of Candy.Core.ChatRooms
*/
_rooms = {},
/** PrivateVariable: _anonymousConnection
* Set in <Candy.Core.connect> when jidOrHost doesn't contain a @-char.
*/
_anonymousConnection = false,
/** PrivateVariable: _status
* Current Strophe connection state
*/
_status,
/** PrivateVariable: _options
* Options:
* (Boolean) debug - Debug (Default: false)
* (Array|Boolean) autojoin - Autojoin these channels. When boolean true, do not autojoin, wait if the server sends something.
*/
_options = {
/** Boolean: autojoin
* If set to `true` try to get the bookmarks and autojoin the rooms (supported by ejabberd, Openfire).
* You may want to define an array of rooms to autojoin: `['room1@conference.host.tld', 'room2...]` (ejabberd, Openfire, ...)
*/
autojoin: undefined,
debug: false,
disableWindowUnload: false,
/** Integer: presencePriority
* Default priority for presence messages in order to receive messages across different resources
*/
presencePriority: 1,
/** String: resource
* JID resource to use when connecting to the server.
* Specify `''` (an empty string) to request a random resource.
*/
resource: Candy.about.name
},
/** PrivateFunction: _addNamespace
* Adds a namespace.
*
* Parameters:
* (String) name - namespace name (will become a constant living in Strophe.NS.*)
* (String) value - XML Namespace
*/
_addNamespace = function(name, value) {
Strophe.addNamespace(name, value);
},
/** PrivateFunction: _addNamespaces
* Adds namespaces needed by Candy.
*/
_addNamespaces = function() {
_addNamespace('PRIVATE', 'jabber:iq:private');
_addNamespace('BOOKMARKS', 'storage:bookmarks');
_addNamespace('PRIVACY', 'jabber:iq:privacy');
_addNamespace('DELAY', 'jabber:x:delay');
_addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub');
},
_getEscapedJidFromJid = function(jid) {
var node = Strophe.getNodeFromJid(jid),
domain = Strophe.getDomainFromJid(jid);
return node ? Strophe.escapeNode(node) + '@' + domain : domain;
};
/** Function: init
* Initialize Core.
*
* Parameters:
* (String) service - URL of BOSH/Websocket service
* (Object) options - Options for candy
*/
self.init = function(service, options) {
_service = service;
// Apply options
$.extend(true, _options, options);
// Enable debug logging
if(_options.debug) {
if(typeof window.console !== undefined && typeof window.console.log !== undefined) {
// Strophe has a polyfill for bind which doesn't work in IE8.
if(Function.prototype.bind && Candy.Util.getIeVersion() > 8) {
self.log = Function.prototype.bind.call(console.log, console);
} else {
self.log = function() {
Function.prototype.apply.call(console.log, console, arguments);
};
}
}
self.log('[Init] Debugging enabled');
}
_addNamespaces();
// Connect to BOSH/Websocket service
_connection = new Strophe.Connection(_service);
_connection.rawInput = self.rawInput.bind(self);
_connection.rawOutput = self.rawOutput.bind(self);
// set caps node
_connection.caps.node = 'https://candy-chat.github.io/candy/';
// Window unload handler... works on all browsers but Opera. There is NO workaround.
// Opera clients getting disconnected 1-2 minutes delayed.
if (!_options.disableWindowUnload) {
window.onbeforeunload = self.onWindowUnload;
}
};
/** Function: registerEventHandlers
* Adds listening handlers to the connection.
*
* Use with caution from outside of Candy.
*/
self.registerEventHandlers = function() {
self.addHandler(self.Event.Jabber.Version, Strophe.NS.VERSION, 'iq');
self.addHandler(self.Event.Jabber.Presence, null, 'presence');
self.addHandler(self.Event.Jabber.Message, null, 'message');
self.addHandler(self.Event.Jabber.Bookmarks, Strophe.NS.PRIVATE, 'iq');
self.addHandler(self.Event.Jabber.Room.Disco, Strophe.NS.DISCO_INFO, 'iq', 'result');
self.addHandler(_connection.disco._onDiscoInfo.bind(_connection.disco), Strophe.NS.DISCO_INFO, 'iq', 'get');
self.addHandler(_connection.disco._onDiscoItems.bind(_connection.disco), Strophe.NS.DISCO_ITEMS, 'iq', 'get');
self.addHandler(_connection.caps._delegateCapabilities.bind(_connection.caps), Strophe.NS.CAPS);
};
/** Function: connect
* Connect to the jabber host.
*
* There are four different procedures to login:
* connect('JID', 'password') - Connect a registered user
* connect('domain') - Connect anonymously to the domain. The user should receive a random JID.
* connect('domain', null, 'nick') - Connect anonymously to the domain. The user should receive a random JID but with a nick set.
* connect('JID') - Show login form and prompt for password. JID input is hidden.
* connect() - Show login form and prompt for JID and password.
*
* See:
* <Candy.Core.attach()> for attaching an already established session.
*
* Parameters:
* (String) jidOrHost - JID or Host
* (String) password - Password of the user
* (String) nick - Nick of the user. Set one if you want to anonymously connect but preset a nick. If jidOrHost is a domain
* and this param is not set, Candy will prompt for a nick.
*/
self.connect = function(jidOrHost, password, nick) {
// Reset before every connection attempt to make sure reconnections work after authfail, alltabsclosed, ...
_connection.reset();
self.registerEventHandlers();
/** Event: candy:core.before-connect
* Triggered before a connection attempt is made.
*
* Plugins should register their stanza handlers using this event
* to ensure that they are set.
*
* See also <#84 at https://github.com/candy-chat/candy/issues/84>.
*
* Parameters:
* (Strophe.Connection) conncetion - Strophe connection
*/
$(Candy).triggerHandler('candy:core.before-connect', {
connection: _connection
});
_anonymousConnection = !_anonymousConnection ? jidOrHost && jidOrHost.indexOf("@") < 0 : true;
if(jidOrHost && password) {
// authentication
_connection.connect(_getEscapedJidFromJid(jidOrHost) + '/' + _options.resource, password, Candy.Core.Event.Strophe.Connect);
if (nick) {
_user = new self.ChatUser(jidOrHost, nick);
} else {
_user = new self.ChatUser(jidOrHost, Strophe.getNodeFromJid(jidOrHost));
}
} else if(jidOrHost && nick) {
// anonymous connect
_connection.connect(_getEscapedJidFromJid(jidOrHost) + '/' + _options.resource, null, Candy.Core.Event.Strophe.Connect);
_user = new self.ChatUser(null, nick); // set jid to null because we'll later receive it
} else if(jidOrHost) {
Candy.Core.Event.Login(jidOrHost);
} else {
// display login modal
Candy.Core.Event.Login();
}
};
/** Function: attach
* Attach an already binded & connected session to the server
*
* _See_ Strophe.Connection.attach
*
* Parameters:
* (String) jid - Jabber ID
* (Integer) sid - Session ID
* (Integer) rid - rid
*/
self.attach = function(jid, sid, rid) {
_user = new self.ChatUser(jid, Strophe.getNodeFromJid(jid));
self.registerEventHandlers();
_connection.attach(jid, sid, rid, Candy.Core.Event.Strophe.Connect);
};
/** Function: disconnect
* Leave all rooms and disconnect
*/
self.disconnect = function() {
if(_connection.connected) {
$.each(self.getRooms(), function() {
Candy.Core.Action.Jabber.Room.Leave(this.getJid());
});
_connection.disconnect();
}
};
/** Function: addHandler
* Wrapper for Strophe.Connection.addHandler() to add a stanza handler for the connection.
*
* Parameters:
* (Function) handler - The user callback.
* (String) ns - The namespace to match.
* (String) name - The stanza name to match.
* (String) type - The stanza type attribute to match.
* (String) id - The stanza id attribute to match.
* (String) from - The stanza from attribute to match.
* (String) options - The handler options
*
* Returns:
* A reference to the handler that can be used to remove it.
*/
self.addHandler = function(handler, ns, name, type, id, from, options) {
return _connection.addHandler(handler, ns, name, type, id, from, options);
};
/** Function: getUser
* Gets current user
*
* Returns:
* Instance of Candy.Core.ChatUser
*/
self.getUser = function() {
return _user;
};
/** Function: setUser
* Set current user. Needed when anonymous login is used, as jid gets retrieved later.
*
* Parameters:
* (Candy.Core.ChatUser) user - User instance
*/
self.setUser = function(user) {
_user = user;
};
/** Function: getConnection
* Gets Strophe connection
*
* Returns:
* Instance of Strophe.Connection
*/
self.getConnection = function() {
return _connection;
};
/** Function: removeRoom
* Removes a room from the rooms list
*
* Parameters:
* (String) roomJid - roomJid
*/
self.removeRoom = function(roomJid) {
delete _rooms[roomJid];
};
/** Function: getRooms
* Gets all joined rooms
*
* Returns:
* Object containing instances of Candy.Core.ChatRoom
*/
self.getRooms = function() {
return _rooms;
};
/** Function: getStropheStatus
* Get the status set by Strophe.
*
* Returns:
* (Strophe.Status.*) - one of Strophe's statuses
*/
self.getStropheStatus = function() {
return _status;
};
/** Function: setStropheStatus
* Set the strophe status
*
* Called by:
* Candy.Core.Event.Strophe.Connect
*
* Parameters:
* (Strophe.Status.*) status - Strophe's status
*/
self.setStropheStatus = function(status) {
_status = status;
};
/** Function: isAnonymousConnection
* Returns true if <Candy.Core.connect> was first called with a domain instead of a jid as the first param.
*
* Returns:
* (Boolean)
*/
self.isAnonymousConnection = function() {
return _anonymousConnection;
};
/** Function: getOptions
* Gets options
*
* Returns:
* Object
*/
self.getOptions = function() {
return _options;
};
/** Function: getRoom
* Gets a specific room
*
* Parameters:
* (String) roomJid - JID of the room
*
* Returns:
* If the room is joined, instance of Candy.Core.ChatRoom, otherwise null.
*/
self.getRoom = function(roomJid) {
if (_rooms[roomJid]) {
return _rooms[roomJid];
}
return null;
};
/** Function: onWindowUnload
* window.onbeforeunload event which disconnects the client from the Jabber server.
*/
self.onWindowUnload = function() {
// Enable synchronous requests because Safari doesn't send asynchronous requests within unbeforeunload events.
// Only works properly when following patch is applied to strophejs: https://github.com/metajack/strophejs/issues/16/#issuecomment-600266
_connection.options.sync = true;
self.disconnect();
_connection.flush();
};
/** Function: rawInput
* (Overridden from Strophe.Connection.rawInput)
*
* Logs all raw input if debug is set to true.
*/
self.rawInput = function(data) {
this.log('RECV: ' + data);
};
/** Function rawOutput
* (Overridden from Strophe.Connection.rawOutput)
*
* Logs all raw output if debug is set to true.
*/
self.rawOutput = function(data) {
this.log('SENT: ' + data);
};
/** Function: log
* Overridden to do something useful if debug is set to true.
*
* See: Candy.Core#init
*/
self.log = function() {};
return self;
}(Candy.Core || {}, Strophe, jQuery));

View File

@ -1,419 +0,0 @@
/** File: action.js
* Candy - Chats are not dead yet.
*
* Authors:
* - Patrick Stadler <patrick.stadler@gmail.com>
* - Michael Weibel <michael.weibel@gmail.com>
*
* Copyright:
* (c) 2011 Amiado Group AG. All rights reserved.
* (c) 2012-2014 Patrick Stadler & Michael Weibel. All rights reserved.
*/
'use strict';
/* global Candy, $iq, navigator, Candy, $pres, Strophe, jQuery, $msg */
/** Class: Candy.Core.Action
* Chat Actions (basicly a abstraction of Jabber commands)
*
* Parameters:
* (Candy.Core.Action) self - itself
* (Strophe) Strophe - Strophe
* (jQuery) $ - jQuery
*/
Candy.Core.Action = (function(self, Strophe, $) {
/** Class: Candy.Core.Action.Jabber
* Jabber actions
*/
self.Jabber = {
/** Function: Version
* Replies to a version request
*
* Parameters:
* (jQuery.element) msg - jQuery element
*/
Version: function(msg) {
Candy.Core.getConnection().sendIQ($iq({
type: 'result',
to: Candy.Util.escapeJid(msg.attr('from')),
from: Candy.Util.escapeJid(msg.attr('to')),
id: msg.attr('id')
}).c('query', {
name: Candy.about.name,
version: Candy.about.version,
os: navigator.userAgent
}));
},
/** Function: SetNickname
* Sets the supplied nickname for all rooms (if parameter "room" is not specified) or
* sets it only for the specified rooms
*
* Parameters:
* (String) nickname - New nickname
* (Array) rooms - Rooms
*/
SetNickname: function(nickname, rooms) {
rooms = rooms instanceof Array ? rooms : Candy.Core.getRooms();
var roomNick, presence,
conn = Candy.Core.getConnection();
$.each(rooms, function(roomJid) {
roomNick = Candy.Util.escapeJid(roomJid + '/' + nickname);
presence = $pres({
to: roomNick,
from: conn.jid,
id: 'pres:' + conn.getUniqueId()
});
Candy.Core.getConnection().send(presence);
});
},
/** Function: Roster
* Sends a request for a roster
*/
Roster: function() {
Candy.Core.getConnection().sendIQ($iq({
type: 'get',
xmlns: Strophe.NS.CLIENT
}).c('query', {xmlns: Strophe.NS.ROSTER}).tree());
},
/** Function: Presence
* Sends a request for presence
*
* Parameters:
* (Object) attr - Optional attributes
* (Strophe.Builder) el - Optional element to include in presence stanza
*/
Presence: function(attr, el) {
var conn = Candy.Core.getConnection();
attr = attr || {};
if(!attr.id) {
attr.id = 'pres:' + conn.getUniqueId();
}
var pres = $pres(attr).c('priority').t(Candy.Core.getOptions().presencePriority.toString())
.up().c('c', conn.caps.generateCapsAttrs())
.up();
if(el) {
pres.node.appendChild(el.node);
}
conn.send(pres.tree());
},
/** Function: Services
* Sends a request for disco items
*/
Services: function() {
Candy.Core.getConnection().sendIQ($iq({
type: 'get',
xmlns: Strophe.NS.CLIENT
}).c('query', {xmlns: Strophe.NS.DISCO_ITEMS}).tree());
},
/** Function: Autojoin
* When Candy.Core.getOptions().autojoin is true, request autojoin bookmarks (OpenFire)
*
* Otherwise, if Candy.Core.getOptions().autojoin is an array, join each channel specified.
* Channel can be in jid:password format to pass room password if needed.
* Triggers:
* candy:core.autojoin-missing in case no autojoin info has been found
*/
Autojoin: function() {
// Request bookmarks
if(Candy.Core.getOptions().autojoin === true) {
Candy.Core.getConnection().sendIQ($iq({
type: 'get',
xmlns: Strophe.NS.CLIENT
})
.c('query', {xmlns: Strophe.NS.PRIVATE})
.c('storage', {xmlns: Strophe.NS.BOOKMARKS})
.tree());
var pubsubBookmarkRequest = Candy.Core.getConnection().getUniqueId('pubsub');
Candy.Core.addHandler(Candy.Core.Event.Jabber.Bookmarks, Strophe.NS.PUBSUB, 'iq', 'result', pubsubBookmarkRequest);
Candy.Core.getConnection().sendIQ($iq({
type: 'get',
id: pubsubBookmarkRequest
})
.c('pubsub', { xmlns: Strophe.NS.PUBSUB })
.c('items', { node: Strophe.NS.BOOKMARKS })
.tree());
// Join defined rooms
} else if($.isArray(Candy.Core.getOptions().autojoin)) {
$.each(Candy.Core.getOptions().autojoin, function() {
self.Jabber.Room.Join.apply(null, this.valueOf().split(':',2));
});
} else {
/** Event: candy:core.autojoin-missing
* Triggered when no autojoin information has been found
*/
$(Candy).triggerHandler('candy:core.autojoin-missing');
}
},
/** Function: ResetIgnoreList
* Create new ignore privacy list (and reset the previous one, if it exists).
*/
ResetIgnoreList: function() {
Candy.Core.getConnection().sendIQ($iq({
type: 'set',
from: Candy.Core.getUser().getEscapedJid()
})
.c('query', {xmlns: Strophe.NS.PRIVACY })
.c('list', {name: 'ignore'})
.c('item', {'action': 'allow', 'order': '0'})
.tree());
},
/** Function: RemoveIgnoreList
* Remove an existing ignore list.
*/
RemoveIgnoreList: function() {
Candy.Core.getConnection().sendIQ($iq({
type: 'set',
from: Candy.Core.getUser().getEscapedJid()
})
.c('query', {xmlns: Strophe.NS.PRIVACY })
.c('list', {name: 'ignore'}).tree());
},
/** Function: GetIgnoreList
* Get existing ignore privacy list when connecting.
*/
GetIgnoreList: function() {
var iq = $iq({
type: 'get',
from: Candy.Core.getUser().getEscapedJid()
})
.c('query', {xmlns: Strophe.NS.PRIVACY})
.c('list', {name: 'ignore'}).tree();
var iqId = Candy.Core.getConnection().sendIQ(iq);
// add handler (<#200 at https://github.com/candy-chat/candy/issues/200>)
Candy.Core.addHandler(Candy.Core.Event.Jabber.PrivacyList, null, 'iq', null, iqId);
},
/** Function: SetIgnoreListActive
* Set ignore privacy list active
*/
SetIgnoreListActive: function() {
Candy.Core.getConnection().sendIQ($iq({
type: 'set',
from: Candy.Core.getUser().getEscapedJid()})
.c('query', {xmlns: Strophe.NS.PRIVACY })
.c('active', {name:'ignore'}).tree());
},
/** Function: GetJidIfAnonymous
* On anonymous login, initially we don't know the jid and as a result, Candy.Core._user doesn't have a jid.
* Check if user doesn't have a jid and get it if necessary from the connection.
*/
GetJidIfAnonymous: function() {
if (!Candy.Core.getUser().getJid()) {
Candy.Core.log("[Jabber] Anonymous login");
Candy.Core.getUser().data.jid = Candy.Core.getConnection().jid;
}
},
/** Class: Candy.Core.Action.Jabber.Room
* Room-specific commands
*/
Room: {
/** Function: Join
* Requests disco of specified room and joins afterwards.
*
* TODO:
* maybe we should wait for disco and later join the room?
* but what if we send disco but don't want/can join the room
*
* Parameters:
* (String) roomJid - Room to join
* (String) password - [optional] Password for the room
*/
Join: function(roomJid, password) {
self.Jabber.Room.Disco(roomJid);
roomJid = Candy.Util.escapeJid(roomJid);
var conn = Candy.Core.getConnection(),
roomNick = roomJid + '/' + Candy.Core.getUser().getNick(),
pres = $pres({ to: roomNick, id: 'pres:' + conn.getUniqueId() })
.c('x', {xmlns: Strophe.NS.MUC});
if (password) {
pres.c('password').t(password);
}
pres.up().c('c', conn.caps.generateCapsAttrs());
conn.send(pres.tree());
},
/** Function: Leave
* Leaves a room.
*
* Parameters:
* (String) roomJid - Room to leave
*/
Leave: function(roomJid) {
var user = Candy.Core.getRoom(roomJid).getUser();
roomJid = Candy.Util.escapeJid(roomJid);
if (user) {
Candy.Core.getConnection().muc.leave(roomJid, user.getNick(), function() {});
}
},
/** Function: Disco
* Requests <disco info of a room at http://xmpp.org/extensions/xep-0045.html#disco-roominfo>.
*
* Parameters:
* (String) roomJid - Room to get info for
*/
Disco: function(roomJid) {
Candy.Core.getConnection().sendIQ($iq({
type: 'get',
from: Candy.Core.getUser().getEscapedJid(),
to: Candy.Util.escapeJid(roomJid)
}).c('query', {xmlns: Strophe.NS.DISCO_INFO}).tree());
},
/** Function: Message
* Send message
*
* Parameters:
* (String) roomJid - Room to which send the message into
* (String) msg - Message
* (String) type - "groupchat" or "chat" ("chat" is for private messages)
* (String) xhtmlMsg - XHTML formatted message [optional]
*
* Returns:
* (Boolean) - true if message is not empty after trimming, false otherwise.
*/
Message: function(roomJid, msg, type, xhtmlMsg) {
// Trim message
msg = $.trim(msg);
if(msg === '') {
return false;
}
var nick = null;
if(type === 'chat') {
nick = Strophe.getResourceFromJid(roomJid);
roomJid = Strophe.getBareJidFromJid(roomJid);
}
// muc takes care of the escaping now.
Candy.Core.getConnection().muc.message(roomJid, nick, msg, xhtmlMsg, type);
return true;
},
/** Function: Invite
* Sends an invite stanza to multiple JIDs
*
* Parameters:
* (String) roomJid - Room to which send the message into
* (Array) invitees - Array of JIDs to be invited to the room
* (String) reason - Message to include with the invitation [optional]
* (String) password - Password for the MUC, if required [optional]
*/
Invite: function(roomJid, invitees, reason, password) {
reason = $.trim(reason);
var message = $msg({to: roomJid});
var x = message.c('x', {xmlns: Strophe.NS.MUC_USER});
$.each(invitees, function(i, invitee) {
invitee = Strophe.getBareJidFromJid(invitee);
x.c('invite', {to: invitee});
if (typeof reason !== 'undefined' && reason !== '') {
x.c('reason', reason);
}
});
if (typeof password !== 'undefined' && password !== '') {
x.c('password', password);
}
Candy.Core.getConnection().send(message);
},
/** Function: IgnoreUnignore
* Checks if the user is already ignoring the target user, if yes: unignore him, if no: ignore him.
*
* Uses the ignore privacy list set on connecting.
*
* Parameters:
* (String) userJid - Target user jid
*/
IgnoreUnignore: function(userJid) {
Candy.Core.getUser().addToOrRemoveFromPrivacyList('ignore', userJid);
Candy.Core.Action.Jabber.Room.UpdatePrivacyList();
},
/** Function: UpdatePrivacyList
* Updates privacy list according to the privacylist in the currentUser
*/
UpdatePrivacyList: function() {
var currentUser = Candy.Core.getUser(),
iq = $iq({type: 'set', from: currentUser.getEscapedJid()})
.c('query', {xmlns: 'jabber:iq:privacy' })
.c('list', {name: 'ignore'}),
privacyList = currentUser.getPrivacyList('ignore');
if (privacyList.length > 0) {
$.each(privacyList, function(index, jid) {
iq.c('item', {type:'jid', value: Candy.Util.escapeJid(jid), action: 'deny', order : index})
.c('message').up().up();
});
} else {
iq.c('item', {action: 'allow', order : '0'});
}
Candy.Core.getConnection().sendIQ(iq.tree());
},
/** Class: Candy.Core.Action.Jabber.Room.Admin
* Room administration commands
*/
Admin: {
/** Function: UserAction
* Kick or ban a user
*
* Parameters:
* (String) roomJid - Room in which the kick/ban should be done
* (String) userJid - Victim
* (String) type - "kick" or "ban"
* (String) msg - Reason
*
* Returns:
* (Boolean) - true if sent successfully, false if type is not one of "kick" or "ban".
*/
UserAction: function(roomJid, userJid, type, reason) {
roomJid = Candy.Util.escapeJid(roomJid);
userJid = Candy.Util.escapeJid(userJid);
var itemObj = {nick: Strophe.getResourceFromJid(userJid)};
switch(type) {
case 'kick':
itemObj.role = 'none';
break;
case 'ban':
itemObj.affiliation = 'outcast';
break;
default:
return false;
}
Candy.Core.getConnection().sendIQ($iq({
type: 'set',
from: Candy.Core.getUser().getEscapedJid(),
to: roomJid
}).c('query', {xmlns: Strophe.NS.MUC_ADMIN })
.c('item', itemObj).c('reason').t(reason).tree());
return true;
},
/** Function: SetSubject
* Sets subject (topic) of a room.
*
* Parameters:
* (String) roomJid - Room
* (String) subject - Subject to set
*/
SetSubject: function(roomJid, subject) {
Candy.Core.getConnection().muc.setTopic(Candy.Util.escapeJid(roomJid), subject);
}
}
}
};
return self;
}(Candy.Core.Action || {}, Strophe, jQuery));

View File

@ -1,110 +0,0 @@
/** File: chatRoom.js
* Candy - Chats are not dead yet.
*
* Authors:
* - Patrick Stadler <patrick.stadler@gmail.com>
* - Michael Weibel <michael.weibel@gmail.com>
*
* Copyright:
* (c) 2011 Amiado Group AG. All rights reserved.
* (c) 2012-2014 Patrick Stadler & Michael Weibel. All rights reserved.
*/
'use strict';
/* global Candy, Strophe */
/** Class: Candy.Core.ChatRoom
* Candy Chat Room
*
* Parameters:
* (String) roomJid - Room jid
*/
Candy.Core.ChatRoom = function(roomJid) {
/** Object: room
* Object containing roomJid and name.
*/
this.room = {
jid: roomJid,
name: Strophe.getNodeFromJid(roomJid)
};
/** Variable: user
* Current local user of this room.
*/
this.user = null;
/** Variable: Roster
* Candy.Core.ChatRoster instance
*/
this.roster = new Candy.Core.ChatRoster();
/** Function: setUser
* Set user of this room.
*
* Parameters:
* (Candy.Core.ChatUser) user - Chat user
*/
this.setUser = function(user) {
this.user = user;
};
/** Function: getUser
* Get current local user
*
* Returns:
* (Object) - Candy.Core.ChatUser instance or null
*/
this.getUser = function() {
return this.user;
};
/** Function: getJid
* Get room jid
*
* Returns:
* (String) - Room jid
*/
this.getJid = function() {
return this.room.jid;
};
/** Function: setName
* Set room name
*
* Parameters:
* (String) name - Room name
*/
this.setName = function(name) {
this.room.name = name;
};
/** Function: getName
* Get room name
*
* Returns:
* (String) - Room name
*/
this.getName = function() {
return this.room.name;
};
/** Function: setRoster
* Set roster of room
*
* Parameters:
* (Candy.Core.ChatRoster) roster - Chat roster
*/
this.setRoster = function(roster) {
this.roster = roster;
};
/** Function: getRoster
* Get roster
*
* Returns
* (Candy.Core.ChatRoster) - instance
*/
this.getRoster = function() {
return this.roster;
};
};

View File

@ -1,67 +0,0 @@
/** File: chatRoster.js
* Candy - Chats are not dead yet.
*
* Authors:
* - Patrick Stadler <patrick.stadler@gmail.com>
* - Michael Weibel <michael.weibel@gmail.com>
*
* Copyright:
* (c) 2011 Amiado Group AG. All rights reserved.
* (c) 2012-2014 Patrick Stadler & Michael Weibel. All rights reserved.
*/
'use strict';
/* global Candy */
/** Class: Candy.Core.ChatRoster
* Chat Roster
*/
Candy.Core.ChatRoster = function () {
/** Object: items
* Roster items
*/
this.items = {};
/** Function: add
* Add user to roster
*
* Parameters:
* (Candy.Core.ChatUser) user - User to add
*/
this.add = function(user) {
this.items[user.getJid()] = user;
};
/** Function: remove
* Remove user from roster
*
* Parameters:
* (String) jid - User jid
*/
this.remove = function(jid) {
delete this.items[jid];
};
/** Function: get
* Get user from roster
*
* Parameters:
* (String) jid - User jid
*
* Returns:
* (Candy.Core.ChatUser) - User
*/
this.get = function(jid) {
return this.items[jid];
};
/** Function: getAll
* Get all items
*
* Returns:
* (Object) - all roster items
*/
this.getAll = function() {
return this.items;
};
};

View File

@ -1,265 +0,0 @@
/** File: chatUser.js
* Candy - Chats are not dead yet.
*
* Authors:
* - Patrick Stadler <patrick.stadler@gmail.com>
* - Michael Weibel <michael.weibel@gmail.com>
*
* Copyright:
* (c) 2011 Amiado Group AG. All rights reserved.
* (c) 2012-2014 Patrick Stadler & Michael Weibel. All rights reserved.
*/
'use strict';
/* global Candy, Strophe */
/** Class: Candy.Core.ChatUser
* Chat User
*/
Candy.Core.ChatUser = function(jid, nick, affiliation, role) {
/** Constant: ROLE_MODERATOR
* Moderator role
*/
this.ROLE_MODERATOR = 'moderator';
/** Constant: AFFILIATION_OWNER
* Affiliation owner
*/
this.AFFILIATION_OWNER = 'owner';
/** Object: data
* User data containing:
* - jid
* - nick
* - affiliation
* - role
* - privacyLists
* - customData to be used by e.g. plugins
*/
this.data = {
jid: jid,
nick: Strophe.unescapeNode(nick),
affiliation: affiliation,
role: role,
privacyLists: {},
customData: {},
previousNick: undefined
};
/** Function: getJid
* Gets an unescaped user jid
*
* See:
* <Candy.Util.unescapeJid>
*
* Returns:
* (String) - jid
*/
this.getJid = function() {
if(this.data.jid) {
return Candy.Util.unescapeJid(this.data.jid);
}
return;
};
/** Function: getEscapedJid
* Escapes the user's jid (node & resource get escaped)
*
* See:
* <Candy.Util.escapeJid>
*
* Returns:
* (String) - escaped jid
*/
this.getEscapedJid = function() {
return Candy.Util.escapeJid(this.data.jid);
};
/** Function: setJid
* Sets a user's jid
*
* Parameters:
* (String) jid - New Jid
*/
this.setJid = function(jid) {
this.data.jid = jid;
};
/** Function: getNick
* Gets user nick
*
* Returns:
* (String) - nick
*/
this.getNick = function() {
return Strophe.unescapeNode(this.data.nick);
};
/** Function: setNick
* Sets a user's nick
*
* Parameters:
* (String) nick - New nick
*/
this.setNick = function(nick) {
this.data.nick = nick;
};
/** Function: getRole
* Gets user role
*
* Returns:
* (String) - role
*/
this.getRole = function() {
return this.data.role;
};
/** Function: setRole
* Sets user role
*
* Parameters:
* (String) role - Role
*/
this.setRole = function(role) {
this.data.role = role;
};
/** Function: setAffiliation
* Sets user affiliation
*
* Parameters:
* (String) affiliation - new affiliation
*/
this.setAffiliation = function(affiliation) {
this.data.affiliation = affiliation;
};
/** Function: getAffiliation
* Gets user affiliation
*
* Returns:
* (String) - affiliation
*/
this.getAffiliation = function() {
return this.data.affiliation;
};
/** Function: isModerator
* Check if user is moderator. Depends on the room.
*
* Returns:
* (Boolean) - true if user has role moderator or affiliation owner
*/
this.isModerator = function() {
return this.getRole() === this.ROLE_MODERATOR || this.getAffiliation() === this.AFFILIATION_OWNER;
};
/** Function: addToOrRemoveFromPrivacyList
* Convenience function for adding/removing users from ignore list.
*
* Check if user is already in privacy list. If yes, remove it. If no, add it.
*
* Parameters:
* (String) list - To which privacy list the user should be added / removed from. Candy supports curently only the "ignore" list.
* (String) jid - User jid to add/remove
*
* Returns:
* (Array) - Current privacy list.
*/
this.addToOrRemoveFromPrivacyList = function(list, jid) {
if (!this.data.privacyLists[list]) {
this.data.privacyLists[list] = [];
}
var index = -1;
if ((index = this.data.privacyLists[list].indexOf(jid)) !== -1) {
this.data.privacyLists[list].splice(index, 1);
} else {
this.data.privacyLists[list].push(jid);
}
return this.data.privacyLists[list];
};
/** Function: getPrivacyList
* Returns the privacy list of the listname of the param.
*
* Parameters:
* (String) list - To which privacy list the user should be added / removed from. Candy supports curently only the "ignore" list.
*
* Returns:
* (Array) - Privacy List
*/
this.getPrivacyList = function(list) {
if (!this.data.privacyLists[list]) {
this.data.privacyLists[list] = [];
}
return this.data.privacyLists[list];
};
/** Function: setPrivacyLists
* Sets privacy lists.
*
* Parameters:
* (Object) lists - List object
*/
this.setPrivacyLists = function(lists) {
this.data.privacyLists = lists;
};
/** Function: isInPrivacyList
* Tests if this user ignores the user provided by jid.
*
* Parameters:
* (String) list - Privacy list
* (String) jid - Jid to test for
*
* Returns:
* (Boolean)
*/
this.isInPrivacyList = function(list, jid) {
if (!this.data.privacyLists[list]) {
return false;
}
return this.data.privacyLists[list].indexOf(jid) !== -1;
};
/** Function: setCustomData
* Stores custom data
*
* Parameter:
* (Object) data - Object containing custom data
*/
this.setCustomData = function(data) {
this.data.customData = data;
};
/** Function: getCustomData
* Retrieve custom data
*
* Returns:
* (Object) - Object containing custom data
*/
this.getCustomData = function() {
return this.data.customData;
};
/** Function: setPreviousNick
* If user has nickname changed, set previous nickname.
*
* Parameters:
* (String) previousNick - the previous nickname
*/
this.setPreviousNick = function(previousNick) {
this.data.previousNick = previousNick;
};
/** Function: hasNicknameChanged
* Gets the previous nickname if available.
*
* Returns:
* (String) - previous nickname
*/
this.getPreviousNick = function() {
return this.data.previousNick;
};
};

View File

@ -1,797 +0,0 @@
/** File: event.js
* Candy - Chats are not dead yet.
*
* Authors:
* - Patrick Stadler <patrick.stadler@gmail.com>
* - Michael Weibel <michael.weibel@gmail.com>
*
* Copyright:
* (c) 2011 Amiado Group AG. All rights reserved.
* (c) 2012-2014 Patrick Stadler & Michael Weibel. All rights reserved.
*/
'use strict';
/* global Candy, Strophe, jQuery */
/** Class: Candy.Core.Event
* Chat Events
*
* Parameters:
* (Candy.Core.Event) self - itself
* (Strophe) Strophe - Strophe
* (jQuery) $ - jQuery
*/
Candy.Core.Event = (function(self, Strophe, $) {
/** Function: Login
* Notify view that the login window should be displayed
*
* Parameters:
* (String) presetJid - Preset user JID
*
* Triggers:
* candy:core.login using {presetJid}
*/
self.Login = function(presetJid) {
/** Event: candy:core.login
* Triggered when the login window should be displayed
*
* Parameters:
* (String) presetJid - Preset user JID
*/
$(Candy).triggerHandler('candy:core.login', { presetJid: presetJid } );
};
/** Class: Candy.Core.Event.Strophe
* Strophe-related events
*/
self.Strophe = {
/** Function: Connect
* Acts on strophe status events and notifies view.
*
* Parameters:
* (Strophe.Status) status - Strophe statuses
*
* Triggers:
* candy:core.chat.connection using {status}
*/
Connect: function(status) {
Candy.Core.setStropheStatus(status);
switch(status) {
case Strophe.Status.CONNECTED:
Candy.Core.log('[Connection] Connected');
Candy.Core.Action.Jabber.GetJidIfAnonymous();
/* falls through */
case Strophe.Status.ATTACHED:
Candy.Core.log('[Connection] Attached');
Candy.Core.Action.Jabber.Presence();
Candy.Core.Action.Jabber.Autojoin();
Candy.Core.Action.Jabber.GetIgnoreList();
break;
case Strophe.Status.DISCONNECTED:
Candy.Core.log('[Connection] Disconnected');
break;
case Strophe.Status.AUTHFAIL:
Candy.Core.log('[Connection] Authentication failed');
break;
case Strophe.Status.CONNECTING:
Candy.Core.log('[Connection] Connecting');
break;
case Strophe.Status.DISCONNECTING:
Candy.Core.log('[Connection] Disconnecting');
break;
case Strophe.Status.AUTHENTICATING:
Candy.Core.log('[Connection] Authenticating');
break;
case Strophe.Status.ERROR:
case Strophe.Status.CONNFAIL:
Candy.Core.log('[Connection] Failed (' + status + ')');
break;
default:
Candy.Core.log('[Connection] What?!');
break;
}
/** Event: candy:core.chat.connection
* Connection status updates
*
* Parameters:
* (Strophe.Status) status - Strophe status
*/
$(Candy).triggerHandler('candy:core.chat.connection', { status: status } );
}
};
/** Class: Candy.Core.Event.Jabber
* Jabber related events
*/
self.Jabber = {
/** Function: Version
* Responds to a version request
*
* Parameters:
* (String) msg - Raw XML Message
*
* Returns:
* (Boolean) - true
*/
Version: function(msg) {
Candy.Core.log('[Jabber] Version');
Candy.Core.Action.Jabber.Version($(msg));
return true;
},
/** Function: Presence
* Acts on a presence event
*
* Parameters:
* (String) msg - Raw XML Message
*
* Triggers:
* candy:core.presence using {from, stanza}
*
* Returns:
* (Boolean) - true
*/
Presence: function(msg) {
Candy.Core.log('[Jabber] Presence');
msg = $(msg);
if(msg.children('x[xmlns^="' + Strophe.NS.MUC + '"]').length > 0) {
if (msg.attr('type') === 'error') {
self.Jabber.Room.PresenceError(msg);
} else {
self.Jabber.Room.Presence(msg);
}
} else {
/** Event: candy:core.presence
* Presence updates. Emitted only when not a muc presence.
*
* Parameters:
* (JID) from - From Jid
* (String) stanza - Stanza
*/
$(Candy).triggerHandler('candy:core.presence', {'from': msg.attr('from'), 'stanza': msg});
}
return true;
},
/** Function: Bookmarks
* Acts on a bookmarks event. When a bookmark has the attribute autojoin set, joins this room.
*
* Parameters:
* (String) msg - Raw XML Message
*
* Returns:
* (Boolean) - true
*/
Bookmarks: function(msg) {
Candy.Core.log('[Jabber] Bookmarks');
// Autojoin bookmarks
$('conference', msg).each(function() {
var item = $(this);
if(item.attr('autojoin')) {
Candy.Core.Action.Jabber.Room.Join(item.attr('jid'));
}
});
return true;
},
/** Function: PrivacyList
* Acts on a privacy list event and sets up the current privacy list of this user.
*
* If no privacy list has been added yet, create the privacy list and listen again to this event.
*
* Parameters:
* (String) msg - Raw XML Message
*
* Returns:
* (Boolean) - false to disable the handler after first call.
*/
PrivacyList: function(msg) {
Candy.Core.log('[Jabber] PrivacyList');
var currentUser = Candy.Core.getUser();
msg = $(msg);
if(msg.attr('type') === 'result') {
$('list[name="ignore"] item', msg).each(function() {
var item = $(this);
if (item.attr('action') === 'deny') {
currentUser.addToOrRemoveFromPrivacyList('ignore', item.attr('value'));
}
});
Candy.Core.Action.Jabber.SetIgnoreListActive();
return false;
}
return self.Jabber.PrivacyListError(msg);
},
/** Function: PrivacyListError
* Acts when a privacy list error has been received.
*
* Currently only handles the case, when a privacy list doesn't exist yet and creates one.
*
* Parameters:
* (String) msg - Raw XML Message
*
* Returns:
* (Boolean) - false to disable the handler after first call.
*/
PrivacyListError: function(msg) {
Candy.Core.log('[Jabber] PrivacyListError');
// check if msg says that privacyList doesn't exist
if ($('error[code="404"][type="cancel"] item-not-found', msg)) {
Candy.Core.Action.Jabber.ResetIgnoreList();
Candy.Core.Action.Jabber.SetIgnoreListActive();
}
return false;
},
/** Function: Message
* Acts on room, admin and server messages and notifies the view if required.
*
* Parameters:
* (String) msg - Raw XML Message
*
* Triggers:
* candy:core.chat.message.admin using {type, message}
* candy:core.chat.message.server {type, subject, message}
*
* Returns:
* (Boolean) - true
*/
Message: function(msg) {
Candy.Core.log('[Jabber] Message');
msg = $(msg);
var fromJid = msg.attr('from'),
type = msg.attr('type') || 'undefined',
toJid = msg.attr('to');
// Inspect the message type.
if (type === 'normal' || type === 'undefined') {
var mediatedInvite = msg.find('invite'),
directInvite = msg.find('x[xmlns="jabber:x:conference"]');
if(mediatedInvite.length > 0) {
var passwordNode = msg.find('password'),
password = null,
continueNode = mediatedInvite.find('continue'),
continuedThread = null;
if(passwordNode) {
password = passwordNode.text();
}
if(continueNode) {
continuedThread = continueNode.attr('thread');
}
/** Event: candy:core:chat:invite
* Incoming chat invite for a MUC.
*
* Parameters:
* (String) roomJid - The room the invite is to
* (String) from - User JID that invite is from text
* (String) reason - Reason for invite [default: '']
* (String) password - Password for the room [default: null]
* (String) continuedThread - The thread ID if this is a continuation of a 1-on-1 chat [default: null]
*/
$(Candy).triggerHandler('candy:core:chat:invite', {
roomJid: fromJid,
from: mediatedInvite.attr('from') || 'undefined',
reason: mediatedInvite.find('reason').html() || '',
password: password,
continuedThread: continuedThread
});
}
if(directInvite.length > 0) {
/** Event: candy:core:chat:invite
* Incoming chat invite for a MUC.
*
* Parameters:
* (String) roomJid - The room the invite is to
* (String) from - User JID that invite is from text
* (String) reason - Reason for invite [default: '']
* (String) password - Password for the room [default: null]
* (String) continuedThread - The thread ID if this is a continuation of a 1-on-1 chat [default: null]
*/
$(Candy).triggerHandler('candy:core:chat:invite', {
roomJid: directInvite.attr('jid'),
from: fromJid,
reason: directInvite.attr('reason') || '',
password: directInvite.attr('password'),
continuedThread: directInvite.attr('thread')
});
}
/** Event: candy:core:chat:message:normal
* Messages with the type attribute of normal or those
* that do not have the optional type attribute.
*
* Parameters:
* (String) type - Type of the message [default: message]
* (Object) message - Message object.
*/
// Detect message with type normal or with no type.
$(Candy).triggerHandler('candy:core:chat:message:normal', {
type: (type || 'normal'),
message: msg
});
return true;
} else if (type !== 'groupchat' && type !== 'chat' && type !== 'error' && type !== 'headline') {
/** Event: candy:core:chat:message:other
* Messages with a type other than the ones listed in RFC3921
* section 2.1.1. This allows plugins to catch custom message
* types.
*
* Parameters:
* (String) type - Type of the message [default: message]
* (Object) message - Message object.
*/
// Detect message with type normal or with no type.
$(Candy).triggerHandler('candy:core:chat:message:other', {
type: type,
message: msg
});
return true;
}
// Room message
if(fromJid !== Strophe.getDomainFromJid(fromJid) && (type === 'groupchat' || type === 'chat' || type === 'error')) {
self.Jabber.Room.Message(msg);
// Admin message
} else if(!toJid && fromJid === Strophe.getDomainFromJid(fromJid)) {
/** Event: candy:core.chat.message.admin
* Admin message
*
* Parameters:
* (String) type - Type of the message [default: message]
* (String) message - Message text
*/
$(Candy).triggerHandler('candy:core.chat.message.admin', { type: (type || 'message'), message: msg.children('body').text() });
// Server Message
} else if(toJid && fromJid === Strophe.getDomainFromJid(fromJid)) {
/** Event: candy:core.chat.message.server
* Server message (e.g. subject)
*
* Parameters:
* (String) type - Message type [default: message]
* (String) subject - Subject text
* (String) message - Message text
*/
$(Candy).triggerHandler('candy:core.chat.message.server', {
type: (type || 'message'),
subject: msg.children('subject').text(),
message: msg.children('body').text()
});
}
return true;
},
/** Class: Candy.Core.Event.Jabber.Room
* Room specific events
*/
Room: {
/** Function: Leave
* Leaves a room and cleans up related data and notifies view.
*
* Parameters:
* (String) msg - Raw XML Message
*
* Triggers:
* candy:core.presence.leave using {roomJid, roomName, type, reason, actor, user}
*
* Returns:
* (Boolean) - true
*/
Leave: function(msg) {
Candy.Core.log('[Jabber:Room] Leave');
msg = $(msg);
var from = Candy.Util.unescapeJid(msg.attr('from')),
roomJid = Strophe.getBareJidFromJid(from);
// if room is not joined yet, ignore.
if (!Candy.Core.getRoom(roomJid)) {
return true;
}
var roomName = Candy.Core.getRoom(roomJid).getName(),
item = msg.find('item'),
type = 'leave',
reason,
actor;
delete Candy.Core.getRooms()[roomJid];
// if user gets kicked, role is none and there's a status code 307
if(item.attr('role') === 'none') {
var code = msg.find('status').attr('code');
if(code === '307') {
type = 'kick';
} else if(code === '301') {
type = 'ban';
}
reason = item.find('reason').text();
actor = item.find('actor').attr('jid');
}
var user = new Candy.Core.ChatUser(from, Strophe.getResourceFromJid(from), item.attr('affiliation'), item.attr('role'));
/** Event: candy:core.presence.leave
* When the local client leaves a room
*
* Also triggered when the local client gets kicked or banned from a room.
*
* Parameters:
* (String) roomJid - Room
* (String) roomName - Name of room
* (String) type - Presence type [kick, ban, leave]
* (String) reason - When type equals kick|ban, this is the reason the moderator has supplied.
* (String) actor - When type equals kick|ban, this is the moderator which did the kick
* (Candy.Core.ChatUser) user - user which leaves the room
*/
$(Candy).triggerHandler('candy:core.presence.leave', {
'roomJid': roomJid,
'roomName': roomName,
'type': type,
'reason': reason,
'actor': actor,
'user': user
});
return true;
},
/** Function: Disco
* Sets informations to rooms according to the disco info received.
*
* Parameters:
* (String) msg - Raw XML Message
*
* Returns:
* (Boolean) - true
*/
Disco: function(msg) {
Candy.Core.log('[Jabber:Room] Disco');
msg = $(msg);
// Temp fix for #219
// Don't go further if it's no conference disco reply
// FIXME: Do this in a more beautiful way
if(!msg.find('identity[category="conference"]').length) {
return true;
}
var roomJid = Strophe.getBareJidFromJid(Candy.Util.unescapeJid(msg.attr('from')));
// Client joined a room
if(!Candy.Core.getRooms()[roomJid]) {
Candy.Core.getRooms()[roomJid] = new Candy.Core.ChatRoom(roomJid);
}
// Room existed but room name was unknown
var identity = msg.find('identity');
if(identity.length) {
var roomName = identity.attr('name'),
room = Candy.Core.getRoom(roomJid);
if(room.getName() === null) {
room.setName(Strophe.unescapeNode(roomName));
// Room name changed
}/*else if(room.getName() !== roomName && room.getUser() !== null) {
// NOTE: We want to notify the View here but jabber doesn't send anything when the room name changes :-(
}*/
}
return true;
},
/** Function: Presence
* Acts on various presence messages (room leaving, room joining, error presence) and notifies view.
*
* Parameters:
* (Object) msg - jQuery object of XML message
*
* Triggers:
* candy:core.presence.room using {roomJid, roomName, user, action, currentUser}
*
* Returns:
* (Boolean) - true
*/
Presence: function(msg) {
Candy.Core.log('[Jabber:Room] Presence');
var from = Candy.Util.unescapeJid(msg.attr('from')),
roomJid = Strophe.getBareJidFromJid(from),
presenceType = msg.attr('type'),
status = msg.find('status'),
nickAssign = false,
nickChange = false;
if(status.length) {
// check if status code indicates a nick assignment or nick change
for(var i = 0, l = status.length; i < l; i++) {
var $status = $(status[i]),
code = $status.attr('code');
if(code === '303') {
nickChange = true;
} else if(code === '210') {
nickAssign = true;
}
}
}
// Current User joined a room
var room = Candy.Core.getRoom(roomJid);
if(!room) {
Candy.Core.getRooms()[roomJid] = new Candy.Core.ChatRoom(roomJid);
room = Candy.Core.getRoom(roomJid);
}
// Current User left a room
var currentUser = room.getUser() ? room.getUser() : Candy.Core.getUser();
if(Strophe.getResourceFromJid(from) === currentUser.getNick() && presenceType === 'unavailable' && nickChange === false) {
self.Jabber.Room.Leave(msg);
return true;
}
var roster = room.getRoster(),
action, user,
nick,
item = msg.find('item');
// User joined a room
if(presenceType !== 'unavailable') {
if (roster.get(from)) {
// role/affiliation change
user = roster.get(from);
var role = item.attr('role'),
affiliation = item.attr('affiliation');
user.setRole(role);
user.setAffiliation(affiliation);
// FIXME: currently role/affilation changes are handled with this action
action = 'join';
} else {
nick = Strophe.getResourceFromJid(from);
user = new Candy.Core.ChatUser(from, nick, item.attr('affiliation'), item.attr('role'));
// Room existed but client (myself) is not yet registered
if(room.getUser() === null && (Candy.Core.getUser().getNick() === nick || nickAssign)) {
room.setUser(user);
currentUser = user;
}
roster.add(user);
action = 'join';
}
// User left a room
} else {
user = roster.get(from);
roster.remove(from);
if(nickChange) {
// user changed nick
nick = item.attr('nick');
action = 'nickchange';
user.setPreviousNick(user.getNick());
user.setNick(nick);
user.setJid(Strophe.getBareJidFromJid(from) + '/' + nick);
roster.add(user);
} else {
action = 'leave';
if(item.attr('role') === 'none') {
if(msg.find('status').attr('code') === '307') {
action = 'kick';
} else if(msg.find('status').attr('code') === '301') {
action = 'ban';
}
}
}
}
/** Event: candy:core.presence.room
* Room presence updates
*
* Parameters:
* (String) roomJid - Room JID
* (String) roomName - Room name
* (Candy.Core.ChatUser) user - User which does the presence update
* (String) action - Action [kick, ban, leave, join]
* (Candy.Core.ChatUser) currentUser - Current local user
*/
$(Candy).triggerHandler('candy:core.presence.room', {
'roomJid': roomJid,
'roomName': room.getName(),
'user': user,
'action': action,
'currentUser': currentUser
});
return true;
},
/** Function: PresenceError
* Acts when a presence of type error has been retrieved.
*
* Parameters:
* (Object) msg - jQuery object of XML message
*
* Triggers:
* candy:core.presence.error using {msg, type, roomJid, roomName}
*
* Returns:
* (Boolean) - true
*/
PresenceError: function(msg) {
Candy.Core.log('[Jabber:Room] Presence Error');
var from = Candy.Util.unescapeJid(msg.attr('from')),
roomJid = Strophe.getBareJidFromJid(from),
room = Candy.Core.getRooms()[roomJid],
roomName = room.getName();
// Presence error: Remove room from array to prevent error when disconnecting
Candy.Core.removeRoom(roomJid);
room = undefined;
/** Event: candy:core.presence.error
* Triggered when a presence error happened
*
* Parameters:
* (Object) msg - jQuery object of XML message
* (String) type - Error type
* (String) roomJid - Room jid
* (String) roomName - Room name
*/
$(Candy).triggerHandler('candy:core.presence.error', {
'msg' : msg,
'type': msg.children('error').children()[0].tagName.toLowerCase(),
'roomJid': roomJid,
'roomName': roomName
});
return true;
},
/** Function: Message
* Acts on various message events (subject changed, private chat message, multi-user chat message)
* and notifies view.
*
* Parameters:
* (String) msg - jQuery object of XML message
*
* Triggers:
* candy:core.message using {roomJid, message, timestamp}
*
* Returns:
* (Boolean) - true
*/
Message: function(msg) {
Candy.Core.log('[Jabber:Room] Message');
// Room subject
var roomJid, message, name;
if(msg.children('subject').length > 0 && msg.children('subject').text().length > 0 && msg.attr('type') === 'groupchat') {
roomJid = Candy.Util.unescapeJid(Strophe.getBareJidFromJid(msg.attr('from')));
message = { name: Strophe.getNodeFromJid(roomJid), body: msg.children('subject').text(), type: 'subject' };
// Error messsage
} else if(msg.attr('type') === 'error') {
var error = msg.children('error');
if(error.children('text').length > 0) {
roomJid = msg.attr('from');
message = { type: 'info', body: error.children('text').text() };
}
// Chat message
} else if(msg.children('body').length > 0) {
// Private chat message
if(msg.attr('type') === 'chat' || msg.attr('type') === 'normal') {
roomJid = Candy.Util.unescapeJid(msg.attr('from'));
var bareRoomJid = Strophe.getBareJidFromJid(roomJid),
// if a 3rd-party client sends a direct message to this user (not via the room) then the username is the node and not the resource.
isNoConferenceRoomJid = !Candy.Core.getRoom(bareRoomJid);
name = isNoConferenceRoomJid ? Strophe.getNodeFromJid(roomJid) : Strophe.getResourceFromJid(roomJid);
message = { name: name, body: msg.children('body').text(), type: msg.attr('type'), isNoConferenceRoomJid: isNoConferenceRoomJid };
// Multi-user chat message
} else {
roomJid = Candy.Util.unescapeJid(Strophe.getBareJidFromJid(msg.attr('from')));
var resource = Strophe.getResourceFromJid(msg.attr('from'));
// Message from a user
if(resource) {
resource = Strophe.unescapeNode(resource);
message = { name: resource, body: msg.children('body').text(), type: msg.attr('type') };
// Message from server (XEP-0045#registrar-statuscodes)
} else {
// we are not yet present in the room, let's just drop this message (issue #105)
if(!Candy.View.Pane.Chat.rooms[msg.attr('from')]) {
return true;
}
message = { name: '', body: msg.children('body').text(), type: 'info' };
}
}
var xhtmlChild = msg.children('html[xmlns="' + Strophe.NS.XHTML_IM + '"]');
if(Candy.View.getOptions().enableXHTML === true && xhtmlChild.length > 0) {
var xhtmlMessage = xhtmlChild.children('body[xmlns="' + Strophe.NS.XHTML + '"]').first().html();
message.xhtmlMessage = xhtmlMessage;
}
// Typing notification
} else if(msg.children('composing').length > 0 || msg.children('inactive').length > 0 || msg.children('paused').length > 0) {
roomJid = Candy.Util.unescapeJid(msg.attr('from'));
name = Strophe.getResourceFromJid(roomJid);
var chatstate;
if(msg.children('composing').length > 0) {
chatstate = 'composing';
} else if(msg.children('paused').length > 0) {
chatstate = 'paused';
} else if(msg.children('inactive').length > 0) {
chatstate = 'inactive';
} else if(msg.children('gone').length > 0) {
chatstate = 'gone';
}
/** Event: candy:core.message.chatstate
* Triggers on any recieved chatstate notification.
*
* The resulting message object contains the name of the person, the roomJid, and the indicated chatstate.
*
* The following lists explain those parameters:
*
* Message Object Parameters:
* (String) name - User name
* (String) roomJid - Room jid
* (String) chatstate - Chatstate being indicated. ("paused", "inactive", "composing", "gone")
*
* TODO:
* Perhaps handle blank "active" as specified by XEP-0085?
*/
$(Candy).triggerHandler('candy:core.message.chatstate', {
name: name,
roomJid: roomJid,
chatstate: chatstate
});
return true;
// Unhandled message
} else {
return true;
}
// besides the delayed delivery (XEP-0203), there exists also XEP-0091 which is the legacy delayed delivery.
// the x[xmlns=jabber:x:delay] is the format in XEP-0091.
var delay = msg.children('delay') ? msg.children('delay') : msg.children('x[xmlns="' + Strophe.NS.DELAY +'"]'),
timestamp = delay !== undefined ? delay.attr('stamp') : null;
/** Event: candy:core.message
* Triggers on various message events (subject changed, private chat message, multi-user chat message).
*
* The resulting message object can contain different key-value pairs as stated in the documentation
* of the parameters itself.
*
* The following lists explain those parameters:
*
* Message Object Parameters:
* (String) name - Room name
* (String) body - Message text
* (String) type - Message type ([normal, chat, groupchat])
* or 'info' which is used internally for displaying informational messages
* (Boolean) isNoConferenceRoomJid - if a 3rd-party client sends a direct message to
* this user (not via the room) then the username is the node
* and not the resource.
* This flag tells if this is the case.
*
* Parameters:
* (String) roomJid - Room jid
* (Object) message - Depending on what kind of message, the object consists of different key-value pairs:
* - Room Subject: {name, body, type}
* - Error message: {type = 'info', body}
* - Private chat message: {name, body, type, isNoConferenceRoomJid}
* - MUC msg from a user: {name, body, type}
* - MUC msg from server: {name = '', body, type = 'info'}
* (String) timestamp - Timestamp, only when it's an offline message
*
* TODO:
* Streamline those events sent and rename the parameters.
*/
$(Candy).triggerHandler('candy:core.message', {
roomJid: roomJid,
message: message,
timestamp: timestamp
});
return true;
}
}
};
return self;
}(Candy.Core.Event || {}, Strophe, jQuery));

View File

@ -1,631 +0,0 @@
/** File: util.js
* Candy - Chats are not dead yet.
*
* Authors:
* - Patrick Stadler <patrick.stadler@gmail.com>
* - Michael Weibel <michael.weibel@gmail.com>
*
* Copyright:
* (c) 2011 Amiado Group AG. All rights reserved.
* (c) 2012-2014 Patrick Stadler & Michael Weibel. All rights reserved.
*/
'use strict';
/* global Candy, MD5, Strophe, document, escape, jQuery */
/** Class: Candy.Util
* Candy utils
*
* Parameters:
* (Candy.Util) self - itself
* (jQuery) $ - jQuery
*/
Candy.Util = (function(self, $){
/** Function: jidToId
* Translates a jid to a MD5-Id
*
* Parameters:
* (String) jid - Jid
*
* Returns:
* MD5-ified jid
*/
self.jidToId = function(jid) {
return MD5.hexdigest(jid);
};
/** Function: escapeJid
* Escapes a jid (node & resource get escaped)
*
* See:
* XEP-0106
*
* Parameters:
* (String) jid - Jid
*
* Returns:
* (String) - escaped jid
*/
self.escapeJid = function(jid) {
var node = Strophe.escapeNode(Strophe.getNodeFromJid(jid)),
domain = Strophe.getDomainFromJid(jid),
resource = Strophe.getResourceFromJid(jid);
jid = node + '@' + domain;
if (resource) {
jid += '/' + resource;
}
return jid;
};
/** Function: unescapeJid
* Unescapes a jid (node & resource get unescaped)
*
* See:
* XEP-0106
*
* Parameters:
* (String) jid - Jid
*
* Returns:
* (String) - unescaped Jid
*/
self.unescapeJid = function(jid) {
var node = Strophe.unescapeNode(Strophe.getNodeFromJid(jid)),
domain = Strophe.getDomainFromJid(jid),
resource = Strophe.getResourceFromJid(jid);
jid = node + '@' + domain;
if(resource) {
jid += '/' + resource;
}
return jid;
};
/** Function: crop
* Crop a string with the specified length
*
* Parameters:
* (String) str - String to crop
* (Integer) len - Max length
*/
self.crop = function(str, len) {
if (str.length > len) {
str = str.substr(0, len - 3) + '...';
}
return str;
};
/** Function: parseAndCropXhtml
* Parses the XHTML and applies various Candy related filters to it.
*
* - Ensures it contains only valid XHTML
* - Crops text to a max length
* - Parses the text in order to display html
*
* Parameters:
* (String) str - String containing XHTML
* (Integer) len - Max text length
*/
self.parseAndCropXhtml = function(str, len) {
return $('<div/>').append(self.createHtml($(str).get(0), len)).html();
};
/** Function: setCookie
* Sets a new cookie
*
* Parameters:
* (String) name - cookie name
* (String) value - Value
* (Integer) lifetime_days - Lifetime in days
*/
self.setCookie = function(name, value, lifetime_days) {
var exp = new Date();
exp.setDate(new Date().getDate() + lifetime_days);
document.cookie = name + '=' + value + ';expires=' + exp.toUTCString() + ';path=/';
};
/** Function: cookieExists
* Tests if a cookie with the given name exists
*
* Parameters:
* (String) name - Cookie name
*
* Returns:
* (Boolean) - true/false
*/
self.cookieExists = function(name) {
return document.cookie.indexOf(name) > -1;
};
/** Function: getCookie
* Returns the cookie value if there's one with this name, otherwise returns undefined
*
* Parameters:
* (String) name - Cookie name
*
* Returns:
* Cookie value or undefined
*/
self.getCookie = function(name) {
if(document.cookie) {
var regex = new RegExp(escape(name) + '=([^;]*)', 'gm'),
matches = regex.exec(document.cookie);
if(matches) {
return matches[1];
}
}
};
/** Function: deleteCookie
* Deletes a cookie with the given name
*
* Parameters:
* (String) name - cookie name
*/
self.deleteCookie = function(name) {
document.cookie = name + '=;expires=Thu, 01-Jan-70 00:00:01 GMT;path=/';
};
/** Function: getPosLeftAccordingToWindowBounds
* Fetches the window width and element width
* and checks if specified position + element width is bigger
* than the window width.
*
* If this evaluates to true, the position gets substracted by the element width.
*
* Parameters:
* (jQuery.Element) elem - Element to position
* (Integer) pos - Position left
*
* Returns:
* Object containing `px` (calculated position in pixel) and `alignment` (alignment of the element in relation to pos, either 'left' or 'right')
*/
self.getPosLeftAccordingToWindowBounds = function(elem, pos) {
var windowWidth = $(document).width(),
elemWidth = elem.outerWidth(),
marginDiff = elemWidth - elem.outerWidth(true),
backgroundPositionAlignment = 'left';
if (pos + elemWidth >= windowWidth) {
pos -= elemWidth - marginDiff;
backgroundPositionAlignment = 'right';
}
return { px: pos, backgroundPositionAlignment: backgroundPositionAlignment };
};
/** Function: getPosTopAccordingToWindowBounds
* Fetches the window height and element height
* and checks if specified position + element height is bigger
* than the window height.
*
* If this evaluates to true, the position gets substracted by the element height.
*
* Parameters:
* (jQuery.Element) elem - Element to position
* (Integer) pos - Position top
*
* Returns:
* Object containing `px` (calculated position in pixel) and `alignment` (alignment of the element in relation to pos, either 'top' or 'bottom')
*/
self.getPosTopAccordingToWindowBounds = function(elem, pos) {
var windowHeight = $(document).height(),
elemHeight = elem.outerHeight(),
marginDiff = elemHeight - elem.outerHeight(true),
backgroundPositionAlignment = 'top';
if (pos + elemHeight >= windowHeight) {
pos -= elemHeight - marginDiff;
backgroundPositionAlignment = 'bottom';
}
return { px: pos, backgroundPositionAlignment: backgroundPositionAlignment };
};
/** Function: localizedTime
* Localizes ISO-8610 Date with the time/dateformat specified in the translation.
*
* See: libs/dateformat/dateFormat.js
* See: src/view/translation.js
* See: jquery-i18n/jquery.i18n.js
*
* Parameters:
* (String) dateTime - ISO-8610 Datetime
*
* Returns:
* If current date is equal to the date supplied, format with timeFormat, otherwise with dateFormat
*/
self.localizedTime = function(dateTime) {
if (dateTime === undefined) {
return undefined;
}
var date = self.iso8601toDate(dateTime);
if(date.toDateString() === new Date().toDateString()) {
return date.format($.i18n._('timeFormat'));
} else {
return date.format($.i18n._('dateFormat'));
}
};
/** Function: iso8610toDate
* Parses a ISO-8610 Date to a Date-Object.
*
* Uses a fallback if the client's browser doesn't support it.
*
* Quote:
* ECMAScript revision 5 adds native support for ISO-8601 dates in the Date.parse method,
* but many browsers currently on the market (Safari 4, Chrome 4, IE 6-8) do not support it.
*
* Credits:
* <Colin Snover at http://zetafleet.com/blog/javascript-dateparse-for-iso-8601>
*
* Parameters:
* (String) date - ISO-8610 Date
*
* Returns:
* Date-Object
*/
self.iso8601toDate = function(date) {
var timestamp = Date.parse(date);
if(isNaN(timestamp)) {
var struct = /^(\d{4}|[+\-]\d{6})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3,}))?)?(?:(Z)|([+\-])(\d{2})(?::?(\d{2}))?))?/.exec(date);
if(struct) {
var minutesOffset = 0;
if(struct[8] !== 'Z') {
minutesOffset = +struct[10] * 60 + (+struct[11]);
if(struct[9] === '+') {
minutesOffset = -minutesOffset;
}
}
minutesOffset -= new Date().getTimezoneOffset();
return new Date(+struct[1], +struct[2] - 1, +struct[3], +struct[4], +struct[5] + minutesOffset, +struct[6], struct[7] ? +struct[7].substr(0, 3) : 0);
} else {
// XEP-0091 date
timestamp = Date.parse(date.replace(/^(\d{4})(\d{2})(\d{2})/, '$1-$2-$3') + 'Z');
}
}
return new Date(timestamp);
};
/** Function: isEmptyObject
* IE7 doesn't work with jQuery.isEmptyObject (<=1.5.1), workaround.
*
* Parameters:
* (Object) obj - the object to test for
*
* Returns:
* Boolean true or false.
*/
self.isEmptyObject = function(obj) {
var prop;
for(prop in obj) {
if (obj.hasOwnProperty(prop)) {
return false;
}
}
return true;
};
/** Function: forceRedraw
* Fix IE7 not redrawing under some circumstances.
*
* Parameters:
* (jQuery.element) elem - jQuery element to redraw
*/
self.forceRedraw = function(elem) {
elem.css({display:'none'});
setTimeout(function() {
this.css({display:'block'});
}.bind(elem), 1);
};
/** PrivateVariable: ie
* Checks for IE version
*
* From: http://stackoverflow.com/a/5574871/315242
*/
var ie = (function(){
var undef,
v = 3,
div = document.createElement('div'),
all = div.getElementsByTagName('i');
while (
// adds innerhtml and continues as long as all[0] is truthy
div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',
all[0]
) {}
return v > 4 ? v : undef;
}());
/** Function: getIeVersion
* Returns local variable `ie` which you can use to detect which IE version
* is available.
*
* Use e.g. like this: if(Candy.Util.getIeVersion() < 9) alert('kaboom');
*/
self.getIeVersion = function() {
return ie;
};
/** Class: Candy.Util.Parser
* Parser for emoticons, links and also supports escaping.
*/
self.Parser = {
/** PrivateVariable: _emoticonPath
* Path to emoticons.
*
* Use setEmoticonPath() to change it
*/
_emoticonPath: '',
/** Function: setEmoticonPath
* Set emoticons location.
*
* Parameters:
* (String) path - location of emoticons with trailing slash
*/
setEmoticonPath: function(path) {
this._emoticonPath = path;
},
/** Array: emoticons
* Array containing emoticons to be replaced by their images.
*
* Can be overridden/extended.
*/
emoticons: [
{
plain: ':)',
regex: /((\s):-?\)|:-?\)(\s|$))/gm,
image: 'Smiling.png'
},
{
plain: ';)',
regex: /((\s);-?\)|;-?\)(\s|$))/gm,
image: 'Winking.png'
},
{
plain: ':D',
regex: /((\s):-?D|:-?D(\s|$))/gm,
image: 'Grinning.png'
},
{
plain: ';D',
regex: /((\s);-?D|;-?D(\s|$))/gm,
image: 'Grinning_Winking.png'
},
{
plain: ':(',
regex: /((\s):-?\(|:-?\((\s|$))/gm,
image: 'Unhappy.png'
},
{
plain: '^^',
regex: /((\s)\^\^|\^\^(\s|$))/gm,
image: 'Happy_3.png'
},
{
plain: ':P',
regex: /((\s):-?P|:-?P(\s|$))/igm,
image: 'Tongue_Out.png'
},
{
plain: ';P',
regex: /((\s);-?P|;-?P(\s|$))/igm,
image: 'Tongue_Out_Winking.png'
},
{
plain: ':S',
regex: /((\s):-?S|:-?S(\s|$))/igm,
image: 'Confused.png'
},
{
plain: ':/',
regex: /((\s):-?\/|:-?\/(\s|$))/gm,
image: 'Uncertain.png'
},
{
plain: '8)',
regex: /((\s)8-?\)|8-?\)(\s|$))/gm,
image: 'Sunglasses.png'
},
{
plain: '$)',
regex: /((\s)\$-?\)|\$-?\)(\s|$))/gm,
image: 'Greedy.png'
},
{
plain: 'oO',
regex: /((\s)oO|oO(\s|$))/gm,
image: 'Huh.png'
},
{
plain: ':x',
regex: /((\s):x|:x(\s|$))/gm,
image: 'Lips_Sealed.png'
},
{
plain: ':666:',
regex: /((\s):666:|:666:(\s|$))/gm,
image: 'Devil.png'
},
{
plain: '<3',
regex: /((\s)&lt;3|&lt;3(\s|$))/gm,
image: 'Heart.png'
}
],
/** Function: emotify
* Replaces text-emoticons with their image equivalent.
*
* Parameters:
* (String) text - Text to emotify
*
* Returns:
* Emotified text
*/
emotify: function(text) {
var i;
for(i = this.emoticons.length-1; i >= 0; i--) {
text = text.replace(this.emoticons[i].regex, '$2<img class="emoticon" alt="$1" src="' + this._emoticonPath + this.emoticons[i].image + '" />$3');
}
return text;
},
/** Function: linkify
* Replaces URLs with a HTML-link.
*
* Parameters:
* (String) text - Text to linkify
*
* Returns:
* Linkified text
*/
linkify: function(text) {
text = text.replace(/(^|[^\/])(www\.[^\.]+\.[\S]+(\b|$))/gi, '$1http://$2');
return text.replace(/(\b(https?|ftp|file):\/\/[\-A-Z0-9+&@#\/%?=~_|!:,.;]*[\-A-Z0-9+&@#\/%=~_|])/ig, '<a href="$1" target="_blank">$1</a>');
},
/** Function: escape
* Escapes a text using a jQuery function (like htmlspecialchars in PHP)
*
* Parameters:
* (String) text - Text to escape
*
* Returns:
* Escaped text
*/
escape: function(text) {
return $('<div/>').text(text).html();
},
/** Function: nl2br
* replaces newline characters with a <br/> to make multi line messages look nice
*
* Parameters:
* (String) text - Text to process
*
* Returns:
* Processed text
*/
nl2br: function(text) {
return text.replace(/\r\n|\r|\n/g, '<br />');
},
/** Function: all
* Does everything of the parser: escaping, linkifying and emotifying.
*
* Parameters:
* (String) text - Text to parse
*
* Returns:
* (String) Parsed text
*/
all: function(text) {
if(text) {
text = this.escape(text);
text = this.linkify(text);
text = this.emotify(text);
text = this.nl2br(text);
}
return text;
}
};
/** Function: createHtml
* Copy an HTML DOM element into an XML DOM.
*
* This function copies a DOM element and all its descendants and returns
* the new copy.
*
* It's a function copied & adapted from [Strophe.js core.js](https://github.com/strophe/strophejs/blob/master/src/core.js).
*
* Parameters:
* (HTMLElement) elem - A DOM element.
* (Integer) maxLength - Max length of text
* (Integer) currentLength - Current accumulated text length
*
* Returns:
* A new, copied DOM element tree.
*/
self.createHtml = function(elem, maxLength, currentLength) {
/* jshint -W073 */
currentLength = currentLength || 0;
var i, el, j, tag, attribute, value, css, cssAttrs, attr, cssName, cssValue;
if (elem.nodeType === Strophe.ElementType.NORMAL) {
tag = elem.nodeName.toLowerCase();
if(Strophe.XHTML.validTag(tag)) {
try {
el = $('<' + tag + '/>');
for(i = 0; i < Strophe.XHTML.attributes[tag].length; i++) {
attribute = Strophe.XHTML.attributes[tag][i];
value = elem.getAttribute(attribute);
if(typeof value === 'undefined' || value === null || value === '' || value === false || value === 0) {
continue;
}
if(attribute === 'style' && typeof value === 'object') {
if(typeof value.cssText !== 'undefined') {
value = value.cssText; // we're dealing with IE, need to get CSS out
}
}
// filter out invalid css styles
if(attribute === 'style') {
css = [];
cssAttrs = value.split(';');
for(j = 0; j < cssAttrs.length; j++) {
attr = cssAttrs[j].split(':');
cssName = attr[0].replace(/^\s*/, "").replace(/\s*$/, "").toLowerCase();
if(Strophe.XHTML.validCSS(cssName)) {
cssValue = attr[1].replace(/^\s*/, "").replace(/\s*$/, "");
css.push(cssName + ': ' + cssValue);
}
}
if(css.length > 0) {
value = css.join('; ');
el.attr(attribute, value);
}
} else {
el.attr(attribute, value);
}
}
for (i = 0; i < elem.childNodes.length; i++) {
el.append(self.createHtml(elem.childNodes[i], maxLength, currentLength));
}
} catch(e) { // invalid elements
Candy.Core.log("[Util:createHtml] Error while parsing XHTML:");
Candy.Core.log(e);
el = Strophe.xmlTextNode('');
}
} else {
el = Strophe.xmlGenerator().createDocumentFragment();
for (i = 0; i < elem.childNodes.length; i++) {
el.appendChild(self.createHtml(elem.childNodes[i], maxLength, currentLength));
}
}
} else if (elem.nodeType === Strophe.ElementType.FRAGMENT) {
el = Strophe.xmlGenerator().createDocumentFragment();
for (i = 0; i < elem.childNodes.length; i++) {
el.appendChild(self.createHtml(elem.childNodes[i], maxLength, currentLength));
}
} else if (elem.nodeType === Strophe.ElementType.TEXT) {
var text = elem.nodeValue;
currentLength += text.length;
if(maxLength && currentLength > maxLength) {
text = text.substring(0, maxLength);
}
text = Candy.Util.Parser.all(text);
el = $.parseHTML(text);
}
return el;
/* jshint +W073 */
};
return self;
}(Candy.Util || {}, jQuery));

View File

@ -1,172 +0,0 @@
/** File: view.js
* Candy - Chats are not dead yet.
*
* Authors:
* - Patrick Stadler <patrick.stadler@gmail.com>
* - Michael Weibel <michael.weibel@gmail.com>
*
* Copyright:
* (c) 2011 Amiado Group AG. All rights reserved.
* (c) 2012-2014 Patrick Stadler & Michael Weibel. All rights reserved.
*/
'use strict';
/* global jQuery, Candy, window, Mustache, document */
/** Class: Candy.View
* The Candy View Class
*
* Parameters:
* (Candy.View) self - itself
* (jQuery) $ - jQuery
*/
Candy.View = (function(self, $) {
/** PrivateObject: _current
* Object containing current container & roomJid which the client sees.
*/
var _current = { container: null, roomJid: null },
/** PrivateObject: _options
*
* Options:
* (String) language - language to use
* (String) assets - path to assets (res) directory (with trailing slash)
* (Object) messages - limit: clean up message pane when n is reached / remove: remove n messages after limit has been reached
* (Object) crop - crop if longer than defined: message.nickname=15, message.body=1000, roster.nickname=15
* (Bool) enableXHTML - [default: false] enables XHTML messages sending & displaying
*/
_options = {
language: 'en',
assets: 'res/',
messages: { limit: 2000, remove: 500 },
crop: {
message: { nickname: 15, body: 1000 },
roster: { nickname: 15 }
},
enableXHTML: false
},
/** PrivateFunction: _setupTranslation
* Set dictionary using jQuery.i18n plugin.
*
* See: view/translation.js
* See: libs/jquery-i18n/jquery.i18n.js
*
* Parameters:
* (String) language - Language identifier
*/
_setupTranslation = function(language) {
$.i18n.load(self.Translation[language]);
},
/** PrivateFunction: _registerObservers
* Register observers. Candy core will now notify the View on changes.
*/
_registerObservers = function() {
$(Candy).on('candy:core.chat.connection', self.Observer.Chat.Connection);
$(Candy).on('candy:core.chat.message', self.Observer.Chat.Message);
$(Candy).on('candy:core.login', self.Observer.Login);
$(Candy).on('candy:core.autojoin-missing', self.Observer.AutojoinMissing);
$(Candy).on('candy:core.presence', self.Observer.Presence.update);
$(Candy).on('candy:core.presence.leave', self.Observer.Presence.update);
$(Candy).on('candy:core.presence.room', self.Observer.Presence.update);
$(Candy).on('candy:core.presence.error', self.Observer.PresenceError);
$(Candy).on('candy:core.message', self.Observer.Message);
},
/** PrivateFunction: _registerWindowHandlers
* Register window focus / blur / resize handlers.
*
* jQuery.focus()/.blur() <= 1.5.1 do not work for IE < 9. Fortunately onfocusin/onfocusout will work for them.
*/
_registerWindowHandlers = function() {
if(Candy.Util.getIeVersion() < 9) {
$(document).focusin(Candy.View.Pane.Window.onFocus).focusout(Candy.View.Pane.Window.onBlur);
} else {
$(window).focus(Candy.View.Pane.Window.onFocus).blur(Candy.View.Pane.Window.onBlur);
}
$(window).resize(Candy.View.Pane.Chat.fitTabs);
},
/** PrivateFunction: _initToolbar
* Initialize toolbar.
*/
_initToolbar = function() {
self.Pane.Chat.Toolbar.init();
},
/** PrivateFunction: _delegateTooltips
* Delegate mouseenter on tooltipified element to <Candy.View.Pane.Chat.Tooltip.show>.
*/
_delegateTooltips = function() {
$('body').delegate('li[data-tooltip]', 'mouseenter', Candy.View.Pane.Chat.Tooltip.show);
};
/** Function: init
* Initialize chat view (setup DOM, register handlers & observers)
*
* Parameters:
* (jQuery.element) container - Container element of the whole chat view
* (Object) options - Options: see _options field (value passed here gets extended by the default value in _options field)
*/
self.init = function(container, options) {
// #216
// Rename `resources` to `assets` but prevent installations from failing
// after upgrade
if(options.resources) {
options.assets = options.resources;
}
delete options.resources;
$.extend(true, _options, options);
_setupTranslation(_options.language);
// Set path to emoticons
Candy.Util.Parser.setEmoticonPath(this.getOptions().assets + 'img/emoticons/');
// Start DOMination...
_current.container = container;
_current.container.html(Mustache.to_html(Candy.View.Template.Chat.pane, {
tooltipEmoticons : $.i18n._('tooltipEmoticons'),
tooltipSound : $.i18n._('tooltipSound'),
tooltipAutoscroll : $.i18n._('tooltipAutoscroll'),
tooltipStatusmessage : $.i18n._('tooltipStatusmessage'),
tooltipAdministration : $.i18n._('tooltipAdministration'),
tooltipUsercount : $.i18n._('tooltipUsercount'),
assetsPath : this.getOptions().assets
}, {
tabs: Candy.View.Template.Chat.tabs,
rooms: Candy.View.Template.Chat.rooms,
modal: Candy.View.Template.Chat.modal,
toolbar: Candy.View.Template.Chat.toolbar,
soundcontrol: Candy.View.Template.Chat.soundcontrol
}));
// ... and let the elements dance.
_registerWindowHandlers();
_initToolbar();
_registerObservers();
_delegateTooltips();
};
/** Function: getCurrent
* Get current container & roomJid in an object.
*
* Returns:
* Object containing container & roomJid
*/
self.getCurrent = function() {
return _current;
};
/** Function: getOptions
* Gets options
*
* Returns:
* Object
*/
self.getOptions = function() {
return _options;
};
return self;
}(Candy.View || {}, jQuery));

View File

@ -1,313 +0,0 @@
/** File: observer.js
* Candy - Chats are not dead yet.
*
* Authors:
* - Patrick Stadler <patrick.stadler@gmail.com>
* - Michael Weibel <michael.weibel@gmail.com>
*
* Copyright:
* (c) 2011 Amiado Group AG. All rights reserved.
* (c) 2012-2014 Patrick Stadler & Michael Weibel
*/
'use strict';
/* global Candy, Strophe, Mustache, jQuery */
/** Class: Candy.View.Observer
* Observes Candy core events
*
* Parameters:
* (Candy.View.Observer) self - itself
* (jQuery) $ - jQuery
*/
Candy.View.Observer = (function(self, $) {
/** PrivateVariable: _showConnectedMessageModal
* Ugly way to determine if the 'connected' modal should be shown.
* Is set to false in case no autojoin param is set.
*/
var _showConnectedMessageModal = true;
/** Class: Candy.View.Observer.Chat
* Chat events
*/
self.Chat = {
/** Function: Connection
* The update method gets called whenever an event to which "Chat" is subscribed.
*
* Currently listens for connection status updates
*
* Parameters:
* (jQuery.Event) event - jQuery Event object
* (Object) args - {status (Strophe.Status.*)}
*/
Connection: function(event, args) {
var eventName = 'candy:view.connection.status-' + args.status;
/** Event: candy:view.connection.status-<STROPHE-STATUS>
* Using this event, you can alter the default Candy (View) behaviour when reacting
* to connection updates.
*
* STROPHE-STATUS has to be replaced by one of <Strophe.Status at https://github.com/strophe/strophejs/blob/master/src/core.js#L276>:
* - ERROR: 0,
* - CONNECTING: 1,
* - CONNFAIL: 2,
* - AUTHENTICATING: 3,
* - AUTHFAIL: 4,
* - CONNECTED: 5,
* - DISCONNECTED: 6,
* - DISCONNECTING: 7,
* - ATTACHED: 8
*
*
* If your event handler returns `false`, no View changes will take place.
* You can, of course, also return `true` and do custom things but still
* let Candy (View) do it's job.
*
* This event has been implemented due to <issue #202 at https://github.com/candy-chat/candy/issues/202>
* and here's an example use-case for it:
*
* (start code)
* // react to DISCONNECTED event
* $(Candy).on('candy:view.connection.status-6', function() {
* // on next browser event loop
* setTimeout(function() {
* // reload page to automatically reattach on disconnect
* window.location.reload();
* }, 0);
* // stop view changes right here.
* return false;
* });
* (end code)
*/
if($(Candy).triggerHandler(eventName) === false) {
return false;
}
switch(args.status) {
case Strophe.Status.CONNECTING:
case Strophe.Status.AUTHENTICATING:
Candy.View.Pane.Chat.Modal.show($.i18n._('statusConnecting'), false, true);
break;
case Strophe.Status.ATTACHED:
case Strophe.Status.CONNECTED:
if(_showConnectedMessageModal === true) {
// only show 'connected' if the autojoin error is not shown
// which is determined by having a visible modal in this stage.
Candy.View.Pane.Chat.Modal.show($.i18n._('statusConnected'));
Candy.View.Pane.Chat.Modal.hide();
}
break;
case Strophe.Status.DISCONNECTING:
Candy.View.Pane.Chat.Modal.show($.i18n._('statusDisconnecting'), false, true);
break;
case Strophe.Status.DISCONNECTED:
var presetJid = Candy.Core.isAnonymousConnection() ? Strophe.getDomainFromJid(Candy.Core.getUser().getJid()) : null;
Candy.View.Pane.Chat.Modal.showLoginForm($.i18n._('statusDisconnected'), presetJid);
break;
case Strophe.Status.AUTHFAIL:
Candy.View.Pane.Chat.Modal.showLoginForm($.i18n._('statusAuthfail'));
break;
default:
Candy.View.Pane.Chat.Modal.show($.i18n._('status', args.status));
break;
}
},
/** Function: Message
* Dispatches admin and info messages
*
* Parameters:
* (jQuery.Event) event - jQuery Event object
* (Object) args - {type (message/chat/groupchat), subject (if type = message), message}
*/
Message: function(event, args) {
if(args.type === 'message') {
Candy.View.Pane.Chat.adminMessage((args.subject || ''), args.message);
} else if(args.type === 'chat' || args.type === 'groupchat') {
// use onInfoMessage as infos from the server shouldn't be hidden by the infoMessage switch.
Candy.View.Pane.Chat.onInfoMessage(Candy.View.getCurrent().roomJid, (args.subject || ''), args.message);
}
}
};
/** Class: Candy.View.Observer.Presence
* Presence update events
*/
self.Presence = {
/** Function: update
* Every presence update gets dispatched from this method.
*
* Parameters:
* (jQuery.Event) event - jQuery.Event object
* (Object) args - Arguments differ on each type
*
* Uses:
* - <notifyPrivateChats>
*/
update: function(event, args) {
// Client left
if(args.type === 'leave') {
var user = Candy.View.Pane.Room.getUser(args.roomJid);
Candy.View.Pane.Room.close(args.roomJid);
self.Presence.notifyPrivateChats(user, args.type);
// Client has been kicked or banned
} else if (args.type === 'kick' || args.type === 'ban') {
var actorName = args.actor ? Strophe.getNodeFromJid(args.actor) : null,
actionLabel,
translationParams = [args.roomName];
if (actorName) {
translationParams.push(actorName);
}
switch(args.type) {
case 'kick':
actionLabel = $.i18n._((actorName ? 'youHaveBeenKickedBy' : 'youHaveBeenKicked'), translationParams);
break;
case 'ban':
actionLabel = $.i18n._((actorName ? 'youHaveBeenBannedBy' : 'youHaveBeenBanned'), translationParams);
break;
}
Candy.View.Pane.Chat.Modal.show(Mustache.to_html(Candy.View.Template.Chat.Context.adminMessageReason, {
reason: args.reason,
_action: actionLabel,
_reason: $.i18n._('reasonWas', [args.reason])
}));
setTimeout(function() {
Candy.View.Pane.Chat.Modal.hide(function() {
Candy.View.Pane.Room.close(args.roomJid);
self.Presence.notifyPrivateChats(args.user, args.type);
});
}, 5000);
var evtData = { type: args.type, reason: args.reason, roomJid: args.roomJid, user: args.user };
/** Event: candy:view.presence
* Presence update when kicked or banned
*
* Parameters:
* (String) type - Presence type [kick, ban]
* (String) reason - Reason for the kick|ban [optional]
* (String) roomJid - Room JID
* (Candy.Core.ChatUser) user - User which has been kicked or banned
*/
$(Candy).triggerHandler('candy:view.presence', [evtData]);
// A user changed presence
} else if(args.roomJid) {
args.roomJid = Candy.Util.unescapeJid(args.roomJid);
// Initialize room if not yet existing
if(!Candy.View.Pane.Chat.rooms[args.roomJid]) {
if(Candy.View.Pane.Room.init(args.roomJid, args.roomName) === false) {
return false;
}
Candy.View.Pane.Room.show(args.roomJid);
}
Candy.View.Pane.Roster.update(args.roomJid, args.user, args.action, args.currentUser);
// Notify private user chats if existing, but not in case the action is nickchange
// -- this is because the nickchange presence already contains the new
// user jid
if(Candy.View.Pane.Chat.rooms[args.user.getJid()] && args.action !== 'nickchange') {
Candy.View.Pane.Roster.update(args.user.getJid(), args.user, args.action, args.currentUser);
Candy.View.Pane.PrivateRoom.setStatus(args.user.getJid(), args.action);
}
}
},
/** Function: notifyPrivateChats
* Notify private user chats if existing
*
* Parameters:
* (Candy.Core.ChatUser) user - User which has done the event
* (String) type - Event type (leave, join, kick/ban)
*/
notifyPrivateChats: function(user, type) {
Candy.Core.log('[View:Observer] notify Private Chats');
var roomJid;
for(roomJid in Candy.View.Pane.Chat.rooms) {
if(Candy.View.Pane.Chat.rooms.hasOwnProperty(roomJid) && Candy.View.Pane.Room.getUser(roomJid) && user.getJid() === Candy.View.Pane.Room.getUser(roomJid).getJid()) {
Candy.View.Pane.Roster.update(roomJid, user, type, user);
Candy.View.Pane.PrivateRoom.setStatus(roomJid, type);
}
}
}
};
/** Function: Candy.View.Observer.PresenceError
* Presence errors get handled in this method
*
* Parameters:
* (jQuery.Event) event - jQuery.Event object
* (Object) args - {msg, type, roomJid, roomName}
*/
self.PresenceError = function(obj, args) {
switch(args.type) {
case 'not-authorized':
var message;
if (args.msg.children('x').children('password').length > 0) {
message = $.i18n._('passwordEnteredInvalid', [args.roomName]);
}
Candy.View.Pane.Chat.Modal.showEnterPasswordForm(args.roomJid, args.roomName, message);
break;
case 'conflict':
Candy.View.Pane.Chat.Modal.showNicknameConflictForm(args.roomJid);
break;
case 'registration-required':
Candy.View.Pane.Chat.Modal.showError('errorMembersOnly', [args.roomName]);
break;
case 'service-unavailable':
Candy.View.Pane.Chat.Modal.showError('errorMaxOccupantsReached', [args.roomName]);
break;
}
};
/** Function: Candy.View.Observer.Message
* Messages received get dispatched from this method.
*
* Parameters:
* (jQuery.Event) event - jQuery Event object
* (Object) args - {message, roomJid}
*/
self.Message = function(event, args) {
if(args.message.type === 'subject') {
if (!Candy.View.Pane.Chat.rooms[args.roomJid]) {
Candy.View.Pane.Room.init(args.roomJid, args.message.name);
Candy.View.Pane.Room.show(args.roomJid);
}
Candy.View.Pane.Room.setSubject(args.roomJid, args.message.body);
} else if(args.message.type === 'info') {
Candy.View.Pane.Chat.infoMessage(args.roomJid, args.message.body);
} else {
// Initialize room if it's a message for a new private user chat
if(args.message.type === 'chat' && !Candy.View.Pane.Chat.rooms[args.roomJid]) {
Candy.View.Pane.PrivateRoom.open(args.roomJid, args.message.name, false, args.message.isNoConferenceRoomJid);
}
Candy.View.Pane.Message.show(args.roomJid, args.message.name, args.message.body, args.message.xhtmlMessage, args.timestamp);
}
};
/** Function: Candy.View.Observer.Login
* The login event gets dispatched to this method
*
* Parameters:
* (jQuery.Event) event - jQuery Event object
* (Object) args - {presetJid}
*/
self.Login = function(event, args) {
Candy.View.Pane.Chat.Modal.showLoginForm(null, args.presetJid);
};
/** Class: Candy.View.Observer.AutojoinMissing
* Displays an error about missing autojoin information
*/
self.AutojoinMissing = function() {
_showConnectedMessageModal = false;
Candy.View.Pane.Chat.Modal.showError('errorAutojoinMissing');
};
return self;
}(Candy.View.Observer || {}, jQuery));

File diff suppressed because it is too large Load Diff

View File

@ -1,127 +0,0 @@
/** File: template.js
* Candy - Chats are not dead yet.
*
* Authors:
* - Patrick Stadler <patrick.stadler@gmail.com>
* - Michael Weibel <michael.weibel@gmail.com>
*
* Copyright:
* (c) 2011 Amiado Group AG. All rights reserved.
* (c) 2012-2014 Patrick Stadler & Michael Weibel. All rights reserved.
*/
'use strict';
/* global Candy */
/** Class: Candy.View.Template
* Contains mustache.js templates
*/
Candy.View.Template = (function(self){
self.Window = {
/**
* Unread messages - used to extend the window title
*/
unreadmessages: '({{count}}) {{title}}'
};
self.Chat = {
pane: '<div id="chat-pane">{{> tabs}}{{> toolbar}}{{> rooms}}</div>{{> modal}}',
rooms: '<div id="chat-rooms" class="rooms"></div>',
tabs: '<ul id="chat-tabs"></ul>',
tab: '<li class="roomtype-{{roomType}}" data-roomjid="{{roomJid}}" data-roomtype="{{roomType}}">' +
'<a href="#" class="label">{{#privateUserChat}}@{{/privateUserChat}}{{name}}</a>' +
'<a href="#" class="transition"></a><a href="#" class="close">\u00D7</a>' +
'<small class="unread"></small></li>',
modal: '<div id="chat-modal"><a id="admin-message-cancel" class="close" href="#">\u00D7</a>' +
'<span id="chat-modal-body"></span>' +
'<img src="{{assetsPath}}img/modal-spinner.gif" id="chat-modal-spinner" />' +
'</div><div id="chat-modal-overlay"></div>',
adminMessage: '<li><small>{{time}}</small><div class="adminmessage">' +
'<span class="label">{{sender}}</span>' +
'<span class="spacer">▸</span>{{subject}} {{message}}</div></li>',
infoMessage: '<li><small>{{time}}</small><div class="infomessage">' +
'<span class="spacer">•</span>{{subject}} {{message}}</div></li>',
toolbar: '<ul id="chat-toolbar">' +
'<li id="emoticons-icon" data-tooltip="{{tooltipEmoticons}}"></li>' +
'<li id="chat-sound-control" class="checked" data-tooltip="{{tooltipSound}}">{{> soundcontrol}}</li>' +
'<li id="chat-autoscroll-control" class="checked" data-tooltip="{{tooltipAutoscroll}}"></li>' +
'<li class="checked" id="chat-statusmessage-control" data-tooltip="{{tooltipStatusmessage}}">' +
'</li><li class="context" data-tooltip="{{tooltipAdministration}}"></li>' +
'<li class="usercount" data-tooltip="{{tooltipUsercount}}">' +
'<span id="chat-usercount"></span></li></ul>',
soundcontrol: '<script type="text/javascript">var audioplayerListener = new Object();' +
' audioplayerListener.onInit = function() { };' +
'</script><object id="chat-sound-player" type="application/x-shockwave-flash" data="{{assetsPath}}audioplayer.swf"' +
' width="0" height="0"><param name="movie" value="{{assetsPath}}audioplayer.swf" /><param name="AllowScriptAccess"' +
' value="always" /><param name="FlashVars" value="listener=audioplayerListener&amp;mp3={{assetsPath}}notify.mp3" />' +
'</object>',
Context: {
menu: '<div id="context-menu"><i class="arrow arrow-top"></i>' +
'<ul></ul><i class="arrow arrow-bottom"></i></div>',
menulinks: '<li class="{{class}}" id="context-menu-{{id}}">{{label}}</li>',
contextModalForm: '<form action="#" id="context-modal-form">' +
'<label for="context-modal-label">{{_label}}</label>' +
'<input type="text" name="contextModalField" id="context-modal-field" />' +
'<input type="submit" class="button" name="send" value="{{_submit}}" /></form>',
adminMessageReason: '<a id="admin-message-cancel" class="close" href="#">×</a>' +
'<p>{{_action}}</p>{{#reason}}<p>{{_reason}}</p>{{/reason}}'
},
tooltip: '<div id="tooltip"><i class="arrow arrow-top"></i>' +
'<div></div><i class="arrow arrow-bottom"></i></div>'
};
self.Room = {
pane: '<div class="room-pane roomtype-{{roomType}}" id="chat-room-{{roomId}}" data-roomjid="{{roomJid}}" data-roomtype="{{roomType}}">' +
'{{> roster}}{{> messages}}{{> form}}</div>',
subject: '<li><small>{{time}}</small><div class="subject">' +
'<span class="label">{{roomName}}</span>' +
'<span class="spacer">▸</span>{{_roomSubject}} {{{subject}}}</div></li>',
form: '<div class="message-form-wrapper">' +
'<form method="post" class="message-form">' +
'<input name="message" class="field" type="text" aria-label="Message Form Text Field" autocomplete="off" maxlength="1000" />' +
'<input type="submit" class="submit" name="submit" value="{{_messageSubmit}}" /></form></div>'
};
self.Roster = {
pane: '<div class="roster-pane"></div>',
user: '<div class="user role-{{role}} affiliation-{{affiliation}}{{#me}} me{{/me}}"' +
' id="user-{{roomId}}-{{userId}}" data-jid="{{userJid}}"' +
' data-nick="{{nick}}" data-role="{{role}}" data-affiliation="{{affiliation}}">' +
'<div class="label">{{displayNick}}</div><ul>' +
'<li class="context" id="context-{{roomId}}-{{userId}}">&#x25BE;</li>' +
'<li class="role role-{{role}} affiliation-{{affiliation}}" data-tooltip="{{tooltipRole}}"></li>' +
'<li class="ignore" data-tooltip="{{tooltipIgnored}}"></li></ul></div>'
};
self.Message = {
pane: '<div class="message-pane-wrapper"><ul class="message-pane"></ul></div>',
item: '<li><small>{{time}}</small><div>' +
'<a class="label" href="#" class="name">{{displayName}}</a>' +
'<span class="spacer">▸</span>{{{message}}}</div></li>'
};
self.Login = {
form: '<form method="post" id="login-form" class="login-form">' +
'{{#displayNickname}}<label for="username">{{_labelNickname}}</label><input type="text" id="username" name="username"/>{{/displayNickname}}' +
'{{#displayUsername}}<label for="username">{{_labelUsername}}</label>' +
'<input type="text" id="username" name="username"/>{{/displayUsername}}' +
'{{#presetJid}}<input type="hidden" id="username" name="username" value="{{presetJid}}"/>{{/presetJid}}' +
'{{#displayPassword}}<label for="password">{{_labelPassword}}</label>' +
'<input type="password" id="password" name="password" />{{/displayPassword}}' +
'<input type="submit" class="button" value="{{_loginSubmit}}" /></form>'
};
self.PresenceError = {
enterPasswordForm: '<strong>{{_label}}</strong>' +
'<form method="post" id="enter-password-form" class="enter-password-form">' +
'<label for="password">{{_labelPassword}}</label><input type="password" id="password" name="password" />' +
'<input type="submit" class="button" value="{{_joinSubmit}}" /></form>',
nicknameConflictForm: '<strong>{{_label}}</strong>' +
'<form method="post" id="nickname-conflict-form" class="nickname-conflict-form">' +
'<label for="nickname">{{_labelNickname}}</label><input type="text" id="nickname" name="nickname" />' +
'<input type="submit" class="button" value="{{_loginSubmit}}" /></form>',
displayError: '<strong>{{_error}}</strong>'
};
return self;
}(Candy.View.Template || {}));

View File

@ -1,868 +0,0 @@
/** File: translation.js
* Candy - Chats are not dead yet.
*
* Authors:
* - Patrick Stadler <patrick.stadler@gmail.com>
* - Michael Weibel <michael.weibel@gmail.com>
*
* Copyright:
* (c) 2011 Amiado Group AG. All rights reserved.
* (c) 2012-2014 Patrick Stadler & Michael Weibel. All rights reserved.
*/
'use strict';
/* global Candy */
/** Class: Candy.View.Translation
* Contains translations
*/
Candy.View.Translation = {
'en' : {
'status': 'Status: %s',
'statusConnecting': 'Connecting...',
'statusConnected' : 'Connected',
'statusDisconnecting': 'Disconnecting...',
'statusDisconnected' : 'Disconnected',
'statusAuthfail': 'Authentication failed',
'roomSubject' : 'Subject:',
'messageSubmit': 'Send',
'labelUsername': 'Username:',
'labelNickname': 'Nickname:',
'labelPassword': 'Password:',
'loginSubmit' : 'Login',
'loginInvalid' : 'Invalid JID',
'reason' : 'Reason:',
'subject' : 'Subject:',
'reasonWas' : 'Reason was: %s.',
'kickActionLabel' : 'Kick',
'youHaveBeenKickedBy' : 'You have been kicked from %2$s by %1$s',
'youHaveBeenKicked' : 'You have been kicked from %s',
'banActionLabel' : 'Ban',
'youHaveBeenBannedBy' : 'You have been banned from %1$s by %2$s',
'youHaveBeenBanned' : 'You have been banned from %s',
'privateActionLabel' : 'Private chat',
'ignoreActionLabel' : 'Ignore',
'unignoreActionLabel' : 'Unignore',
'setSubjectActionLabel': 'Change Subject',
'administratorMessageSubject' : 'Administrator',
'userJoinedRoom' : '%s joined the room.',
'userLeftRoom' : '%s left the room.',
'userHasBeenKickedFromRoom': '%s has been kicked from the room.',
'userHasBeenBannedFromRoom': '%s has been banned from the room.',
'userChangedNick': '%1$s has changed his nickname to %2$s.',
'presenceUnknownWarningSubject': 'Notice:',
'presenceUnknownWarning' : 'This user might be offline. We can\'t track his presence.',
'dateFormat': 'dd.mm.yyyy',
'timeFormat': 'HH:MM:ss',
'tooltipRole' : 'Moderator',
'tooltipIgnored' : 'You ignore this user',
'tooltipEmoticons' : 'Emoticons',
'tooltipSound' : 'Play sound for new private messages',
'tooltipAutoscroll' : 'Autoscroll',
'tooltipStatusmessage' : 'Display status messages',
'tooltipAdministration' : 'Room Administration',
'tooltipUsercount' : 'Room Occupants',
'enterRoomPassword' : 'Room "%s" is password protected.',
'enterRoomPasswordSubmit' : 'Join room',
'passwordEnteredInvalid' : 'Invalid password for room "%s".',
'nicknameConflict': 'Username already in use. Please choose another one.',
'errorMembersOnly': 'You can\'t join room "%s": Insufficient rights.',
'errorMaxOccupantsReached': 'You can\'t join room "%s": Too many occupants.',
'errorAutojoinMissing': 'No autojoin parameter set in configuration. Please set one to continue.',
'antiSpamMessage' : 'Please do not spam. You have been blocked for a short-time.'
},
'de' : {
'status': 'Status: %s',
'statusConnecting': 'Verbinden...',
'statusConnected' : 'Verbunden',
'statusDisconnecting': 'Verbindung trennen...',
'statusDisconnected' : 'Verbindung getrennt',
'statusAuthfail': 'Authentifizierung fehlgeschlagen',
'roomSubject' : 'Thema:',
'messageSubmit': 'Senden',
'labelUsername': 'Benutzername:',
'labelNickname': 'Spitzname:',
'labelPassword': 'Passwort:',
'loginSubmit' : 'Anmelden',
'loginInvalid' : 'Ungültige JID',
'reason' : 'Begründung:',
'subject' : 'Titel:',
'reasonWas' : 'Begründung: %s.',
'kickActionLabel' : 'Kick',
'youHaveBeenKickedBy' : 'Du wurdest soeben aus dem Raum %1$s gekickt (%2$s)',
'youHaveBeenKicked' : 'Du wurdest soeben aus dem Raum %s gekickt',
'banActionLabel' : 'Ban',
'youHaveBeenBannedBy' : 'Du wurdest soeben aus dem Raum %1$s verbannt (%2$s)',
'youHaveBeenBanned' : 'Du wurdest soeben aus dem Raum %s verbannt',
'privateActionLabel' : 'Privater Chat',
'ignoreActionLabel' : 'Ignorieren',
'unignoreActionLabel' : 'Nicht mehr ignorieren',
'setSubjectActionLabel': 'Thema ändern',
'administratorMessageSubject' : 'Administrator',
'userJoinedRoom' : '%s hat soeben den Raum betreten.',
'userLeftRoom' : '%s hat soeben den Raum verlassen.',
'userHasBeenKickedFromRoom': '%s ist aus dem Raum gekickt worden.',
'userHasBeenBannedFromRoom': '%s ist aus dem Raum verbannt worden.',
'userChangedNick': '%1$s hat den Nicknamen zu %2$s geändert.',
'presenceUnknownWarningSubject': 'Hinweis:',
'presenceUnknownWarning' : 'Dieser Benutzer könnte bereits abgemeldet sein. Wir können seine Anwesenheit nicht verfolgen.',
'dateFormat': 'dd.mm.yyyy',
'timeFormat': 'HH:MM:ss',
'tooltipRole' : 'Moderator',
'tooltipIgnored' : 'Du ignorierst diesen Benutzer',
'tooltipEmoticons' : 'Smileys',
'tooltipSound' : 'Ton abspielen bei neuen privaten Nachrichten',
'tooltipAutoscroll' : 'Autoscroll',
'tooltipStatusmessage' : 'Statusnachrichten anzeigen',
'tooltipAdministration' : 'Raum Administration',
'tooltipUsercount' : 'Anzahl Benutzer im Raum',
'enterRoomPassword' : 'Raum "%s" ist durch ein Passwort geschützt.',
'enterRoomPasswordSubmit' : 'Raum betreten',
'passwordEnteredInvalid' : 'Inkorrektes Passwort für Raum "%s".',
'nicknameConflict': 'Der Benutzername wird bereits verwendet. Bitte wähle einen anderen.',
'errorMembersOnly': 'Du kannst den Raum "%s" nicht betreten: Ungenügende Rechte.',
'errorMaxOccupantsReached': 'Du kannst den Raum "%s" nicht betreten: Benutzerlimit erreicht.',
'errorAutojoinMissing': 'Keine "autojoin" Konfiguration gefunden. Bitte setze eine konfiguration um fortzufahren.',
'antiSpamMessage' : 'Bitte nicht spammen. Du wurdest für eine kurze Zeit blockiert.'
},
'fr' : {
'status': 'Status : %s',
'statusConnecting': 'Connexion…',
'statusConnected' : 'Connecté.',
'statusDisconnecting': 'Déconnexion…',
'statusDisconnected' : 'Déconnecté.',
'statusAuthfail': 'L\'authentification a échoué',
'roomSubject' : 'Sujet :',
'messageSubmit': 'Envoyer',
'labelUsername': 'Nom d\'utilisateur :',
'labelPassword': 'Mot de passe :',
'loginSubmit' : 'Connexion',
'loginInvalid' : 'JID invalide',
'reason' : 'Motif :',
'subject' : 'Titre :',
'reasonWas' : 'Motif : %s.',
'kickActionLabel' : 'Kick',
'youHaveBeenKickedBy' : 'Vous avez été expulsé du salon %1$s (%2$s)',
'youHaveBeenKicked' : 'Vous avez été expulsé du salon %s',
'banActionLabel' : 'Ban',
'youHaveBeenBannedBy' : 'Vous avez été banni du salon %1$s (%2$s)',
'youHaveBeenBanned' : 'Vous avez été banni du salon %s',
'privateActionLabel' : 'Chat privé',
'ignoreActionLabel' : 'Ignorer',
'unignoreActionLabel' : 'Ne plus ignorer',
'setSubjectActionLabel': 'Changer le sujet',
'administratorMessageSubject' : 'Administrateur',
'userJoinedRoom' : '%s vient d\'entrer dans le salon.',
'userLeftRoom' : '%s vient de quitter le salon.',
'userHasBeenKickedFromRoom': '%s a été expulsé du salon.',
'userHasBeenBannedFromRoom': '%s a été banni du salon.',
'presenceUnknownWarningSubject': 'Note :',
'presenceUnknownWarning' : 'Cet utilisateur n\'est malheureusement plus connecté, le message ne sera pas envoyé.',
'dateFormat': 'dd/mm/yyyy',
'timeFormat': 'HH:MM:ss',
'tooltipRole' : 'Modérateur',
'tooltipIgnored' : 'Vous ignorez cette personne',
'tooltipEmoticons' : 'Smileys',
'tooltipSound' : 'Jouer un son lors de la réception de nouveaux messages privés',
'tooltipAutoscroll' : 'Défilement automatique',
'tooltipStatusmessage' : 'Messages d\'état',
'tooltipAdministration' : 'Administration du salon',
'tooltipUsercount' : 'Nombre d\'utilisateurs dans le salon',
'enterRoomPassword' : 'Le salon "%s" est protégé par un mot de passe.',
'enterRoomPasswordSubmit' : 'Entrer dans le salon',
'passwordEnteredInvalid' : 'Le mot de passe pour le salon "%s" est invalide.',
'nicknameConflict': 'Le nom d\'utilisateur est déjà utilisé. Veuillez en choisir un autre.',
'errorMembersOnly': 'Vous ne pouvez pas entrer dans le salon "%s" : droits insuffisants.',
'errorMaxOccupantsReached': 'Vous ne pouvez pas entrer dans le salon "%s": Limite d\'utilisateur atteint.',
'antiSpamMessage' : 'Merci de ne pas envoyer de spam. Vous avez été bloqué pendant une courte période..'
},
'nl' : {
'status': 'Status: %s',
'statusConnecting': 'Verbinding maken...',
'statusConnected' : 'Verbinding is gereed',
'statusDisconnecting': 'Verbinding verbreken...',
'statusDisconnected' : 'Verbinding is verbroken',
'statusAuthfail': 'Authenticatie is mislukt',
'roomSubject' : 'Onderwerp:',
'messageSubmit': 'Verstuur',
'labelUsername': 'Gebruikersnaam:',
'labelPassword': 'Wachtwoord:',
'loginSubmit' : 'Inloggen',
'loginInvalid' : 'JID is onjuist',
'reason' : 'Reden:',
'subject' : 'Onderwerp:',
'reasonWas' : 'De reden was: %s.',
'kickActionLabel' : 'Verwijderen',
'youHaveBeenKickedBy' : 'Je bent verwijderd van %1$s door %2$s',
'youHaveBeenKicked' : 'Je bent verwijderd van %s',
'banActionLabel' : 'Blokkeren',
'youHaveBeenBannedBy' : 'Je bent geblokkeerd van %1$s door %2$s',
'youHaveBeenBanned' : 'Je bent geblokkeerd van %s',
'privateActionLabel' : 'Prive gesprek',
'ignoreActionLabel' : 'Negeren',
'unignoreActionLabel' : 'Niet negeren',
'setSubjectActionLabel': 'Onderwerp wijzigen',
'administratorMessageSubject' : 'Beheerder',
'userJoinedRoom' : '%s komt de chat binnen.',
'userLeftRoom' : '%s heeft de chat verlaten.',
'userHasBeenKickedFromRoom': '%s is verwijderd.',
'userHasBeenBannedFromRoom': '%s is geblokkeerd.',
'presenceUnknownWarningSubject': 'Mededeling:',
'presenceUnknownWarning' : 'Deze gebruiker is waarschijnlijk offline, we kunnen zijn/haar aanwezigheid niet vaststellen.',
'dateFormat': 'dd.mm.yyyy',
'timeFormat': 'HH:MM:ss',
'tooltipRole' : 'Moderator',
'tooltipIgnored' : 'Je negeert deze gebruiker',
'tooltipEmoticons' : 'Emotie-iconen',
'tooltipSound' : 'Speel een geluid af bij nieuwe privé berichten.',
'tooltipAutoscroll' : 'Automatisch scrollen',
'tooltipStatusmessage' : 'Statusberichten weergeven',
'tooltipAdministration' : 'Instellingen',
'tooltipUsercount' : 'Gebruikers',
'enterRoomPassword' : 'De Chatroom "%s" is met een wachtwoord beveiligd.',
'enterRoomPasswordSubmit' : 'Ga naar Chatroom',
'passwordEnteredInvalid' : 'Het wachtwoord voor de Chatroom "%s" is onjuist.',
'nicknameConflict': 'De gebruikersnaam is reeds in gebruik. Probeer a.u.b. een andere gebruikersnaam.',
'errorMembersOnly': 'Je kunt niet deelnemen aan de Chatroom "%s": Je hebt onvoldoende rechten.',
'errorMaxOccupantsReached': 'Je kunt niet deelnemen aan de Chatroom "%s": Het maximum aantal gebruikers is bereikt.',
'antiSpamMessage' : 'Het is niet toegestaan om veel berichten naar de server te versturen. Je bent voor een korte periode geblokkeerd.'
},
'es': {
'status': 'Estado: %s',
'statusConnecting': 'Conectando...',
'statusConnected' : 'Conectado',
'statusDisconnecting': 'Desconectando...',
'statusDisconnected' : 'Desconectado',
'statusAuthfail': 'Falló la autenticación',
'roomSubject' : 'Asunto:',
'messageSubmit': 'Enviar',
'labelUsername': 'Usuario:',
'labelPassword': 'Clave:',
'loginSubmit' : 'Entrar',
'loginInvalid' : 'JID no válido',
'reason' : 'Razón:',
'subject' : 'Asunto:',
'reasonWas' : 'La razón fue: %s.',
'kickActionLabel' : 'Expulsar',
'youHaveBeenKickedBy' : 'Has sido expulsado de %1$s por %2$s',
'youHaveBeenKicked' : 'Has sido expulsado de %s',
'banActionLabel' : 'Prohibir',
'youHaveBeenBannedBy' : 'Has sido expulsado permanentemente de %1$s por %2$s',
'youHaveBeenBanned' : 'Has sido expulsado permanentemente de %s',
'privateActionLabel' : 'Chat privado',
'ignoreActionLabel' : 'Ignorar',
'unignoreActionLabel' : 'No ignorar',
'setSubjectActionLabel': 'Cambiar asunto',
'administratorMessageSubject' : 'Administrador',
'userJoinedRoom' : '%s se ha unido a la sala.',
'userLeftRoom' : '%s ha dejado la sala.',
'userHasBeenKickedFromRoom': '%s ha sido expulsado de la sala.',
'userHasBeenBannedFromRoom': '%s ha sido expulsado permanentemente de la sala.',
'presenceUnknownWarningSubject': 'Atención:',
'presenceUnknownWarning' : 'Éste usuario podría estar desconectado..',
'dateFormat': 'dd.mm.yyyy',
'timeFormat': 'HH:MM:ss',
'tooltipRole' : 'Moderador',
'tooltipIgnored' : 'Ignoras a éste usuario',
'tooltipEmoticons' : 'Emoticonos',
'tooltipSound' : 'Reproducir un sonido para nuevos mensajes privados',
'tooltipAutoscroll' : 'Desplazamiento automático',
'tooltipStatusmessage' : 'Mostrar mensajes de estado',
'tooltipAdministration' : 'Administración de la sala',
'tooltipUsercount' : 'Usuarios en la sala',
'enterRoomPassword' : 'La sala "%s" está protegida mediante contraseña.',
'enterRoomPasswordSubmit' : 'Unirse a la sala',
'passwordEnteredInvalid' : 'Contraseña incorrecta para la sala "%s".',
'nicknameConflict': 'El nombre de usuario ya está siendo utilizado. Por favor elija otro.',
'errorMembersOnly': 'No se puede unir a la sala "%s": no tiene privilegios suficientes.',
'errorMaxOccupantsReached': 'No se puede unir a la sala "%s": demasiados participantes.',
'antiSpamMessage' : 'Por favor, no hagas spam. Has sido bloqueado temporalmente.'
},
'cn': {
'status': '状态: %s',
'statusConnecting': '连接中...',
'statusConnected': '已连接',
'statusDisconnecting': '断开连接中...',
'statusDisconnected': '已断开连接',
'statusAuthfail': '认证失败',
'roomSubject': '主题:',
'messageSubmit': '发送',
'labelUsername': '用户名:',
'labelPassword': '密码:',
'loginSubmit': '登录',
'loginInvalid': '用户名不合法',
'reason': '原因:',
'subject': '主题:',
'reasonWas': '原因是: %s.',
'kickActionLabel': '踢除',
'youHaveBeenKickedBy': '你在 %1$s 被管理者 %2$s 请出房间',
'banActionLabel': '禁言',
'youHaveBeenBannedBy': '你在 %1$s 被管理者 %2$s 禁言',
'privateActionLabel': '单独对话',
'ignoreActionLabel': '忽略',
'unignoreActionLabel': '不忽略',
'setSubjectActionLabel': '变更主题',
'administratorMessageSubject': '管理员',
'userJoinedRoom': '%s 加入房间',
'userLeftRoom': '%s 离开房间',
'userHasBeenKickedFromRoom': '%s 被请出这个房间',
'userHasBeenBannedFromRoom': '%s 被管理者禁言',
'presenceUnknownWarningSubject': '注意:',
'presenceUnknownWarning': '这个会员可能已经下线,不能追踪到他的连接信息',
'dateFormat': 'dd.mm.yyyy',
'timeFormat': 'HH:MM:ss',
'tooltipRole': '管理',
'tooltipIgnored': '你忽略了这个会员',
'tooltipEmoticons': '表情',
'tooltipSound': '新消息发音',
'tooltipAutoscroll': '滚动条',
'tooltipStatusmessage': '禁用状态消息',
'tooltipAdministration': '房间管理',
'tooltipUsercount': '房间占有者',
'enterRoomPassword': '登录房间 "%s" 需要密码.',
'enterRoomPasswordSubmit': '加入房间',
'passwordEnteredInvalid': '登录房间 "%s" 的密码不正确',
'nicknameConflict': '用户名已经存在,请另选一个',
'errorMembersOnly': '您的权限不够,不能登录房间 "%s" ',
'errorMaxOccupantsReached': '房间 "%s" 的人数已达上限,您不能登录',
'antiSpamMessage': '因为您在短时间内发送过多的消息 服务器要阻止您一小段时间。'
},
'ja' : {
'status' : 'ステータス: %s',
'statusConnecting' : '接続中…',
'statusConnected' : '接続されました',
'statusDisconnecting' : 'ディスコネクト中…',
'statusDisconnected' : 'ディスコネクトされました',
'statusAuthfail' : '認証に失敗しました',
'roomSubject' : 'トピック:',
'messageSubmit' : '送信',
'labelUsername' : 'ユーザーネーム:',
'labelPassword' : 'パスワード:',
'loginSubmit' : 'ログイン',
'loginInvalid' : 'ユーザーネームが正しくありません',
'reason' : '理由:',
'subject' : 'トピック:',
'reasonWas' : '理由: %s。',
'kickActionLabel' : 'キック',
'youHaveBeenKickedBy' : 'あなたは%2$sにより%1$sからキックされました。',
'youHaveBeenKicked' : 'あなたは%sからキックされました。',
'banActionLabel' : 'アカウントバン',
'youHaveBeenBannedBy' : 'あなたは%2$sにより%1$sからアカウントバンされました。',
'youHaveBeenBanned' : 'あなたは%sからアカウントバンされました。',
'privateActionLabel' : 'プライベートメッセージ',
'ignoreActionLabel' : '無視する',
'unignoreActionLabel' : '無視をやめる',
'setSubjectActionLabel' : 'トピックを変える',
'administratorMessageSubject' : '管理者',
'userJoinedRoom' : '%sは入室しました。',
'userLeftRoom' : '%sは退室しました。',
'userHasBeenKickedFromRoom' : '%sは部屋からキックされました。',
'userHasBeenBannedFromRoom' : '%sは部屋からアカウントバンされました。',
'presenceUnknownWarningSubject' : '忠告:',
'presenceUnknownWarning' : 'このユーザーのステータスは不明です。',
'dateFormat' : 'dd.mm.yyyy',
'timeFormat' : 'HH:MM:ss',
'tooltipRole' : 'モデレーター',
'tooltipIgnored' : 'このユーザーを無視設定にしている',
'tooltipEmoticons' : '絵文字',
'tooltipSound' : '新しいメッセージが届くたびに音を鳴らす',
'tooltipAutoscroll' : 'オートスクロール',
'tooltipStatusmessage' : 'ステータスメッセージを表示',
'tooltipAdministration' : '部屋の管理',
'tooltipUsercount' : 'この部屋の参加者の数',
'enterRoomPassword' : '"%s"の部屋に入るにはパスワードが必要です。',
'enterRoomPasswordSubmit' : '部屋に入る',
'passwordEnteredInvalid' : '"%s"のパスワードと異なるパスワードを入力しました。',
'nicknameConflict' : 'このユーザーネームはすでに利用されているため、別のユーザーネームを選んでください。',
'errorMembersOnly' : '"%s"の部屋に入ることができません: 利用権限を満たしていません。',
'errorMaxOccupantsReached' : '"%s"の部屋に入ることができません: 参加者の数はすでに上限に達しました。',
'antiSpamMessage' : 'スパムなどの行為はやめてください。あなたは一時的にブロックされました。'
},
'sv' : {
'status': 'Status: %s',
'statusConnecting': 'Ansluter...',
'statusConnected' : 'Ansluten',
'statusDisconnecting': 'Kopplar från...',
'statusDisconnected' : 'Frånkopplad',
'statusAuthfail': 'Autentisering misslyckades',
'roomSubject' : 'Ämne:',
'messageSubmit': 'Skicka',
'labelUsername': 'Användarnamn:',
'labelPassword': 'Lösenord:',
'loginSubmit' : 'Logga in',
'loginInvalid' : 'Ogiltigt JID',
'reason' : 'Anledning:',
'subject' : 'Ämne:',
'reasonWas' : 'Anledningen var: %s.',
'kickActionLabel' : 'Sparka ut',
'youHaveBeenKickedBy' : 'Du har blivit utsparkad från %2$s av %1$s',
'youHaveBeenKicked' : 'Du har blivit utsparkad från %s',
'banActionLabel' : 'Bannlys',
'youHaveBeenBannedBy' : 'Du har blivit bannlyst från %1$s av %2$s',
'youHaveBeenBanned' : 'Du har blivit bannlyst från %s',
'privateActionLabel' : 'Privat chatt',
'ignoreActionLabel' : 'Blockera',
'unignoreActionLabel' : 'Avblockera',
'setSubjectActionLabel': 'Ändra ämne',
'administratorMessageSubject' : 'Administratör',
'userJoinedRoom' : '%s kom in i rummet.',
'userLeftRoom' : '%s har lämnat rummet.',
'userHasBeenKickedFromRoom': '%s har blivit utsparkad ur rummet.',
'userHasBeenBannedFromRoom': '%s har blivit bannlyst från rummet.',
'presenceUnknownWarningSubject': 'Notera:',
'presenceUnknownWarning' : 'Denna användare kan vara offline. Vi kan inte följa dennes närvaro.',
'dateFormat': 'yyyy-mm-dd',
'timeFormat': 'HH:MM:ss',
'tooltipRole' : 'Moderator',
'tooltipIgnored' : 'Du blockerar denna användare',
'tooltipEmoticons' : 'Smilies',
'tooltipSound' : 'Spela upp ett ljud vid nytt privat meddelande',
'tooltipAutoscroll' : 'Autoskrolla',
'tooltipStatusmessage' : 'Visa statusmeddelanden',
'tooltipAdministration' : 'Rumadministrering',
'tooltipUsercount' : 'Antal användare i rummet',
'enterRoomPassword' : 'Rummet "%s" är lösenordsskyddat.',
'enterRoomPasswordSubmit' : 'Anslut till rum',
'passwordEnteredInvalid' : 'Ogiltigt lösenord för rummet "%s".',
'nicknameConflict': 'Upptaget användarnamn. Var god välj ett annat.',
'errorMembersOnly': 'Du kan inte ansluta till rummet "%s": Otillräckliga rättigheter.',
'errorMaxOccupantsReached': 'Du kan inte ansluta till rummet "%s": Rummet är fullt.',
'antiSpamMessage' : 'Var god avstå från att spamma. Du har blivit blockerad för en kort stund.'
},
'it' : {
'status': 'Stato: %s',
'statusConnecting': 'Connessione...',
'statusConnected' : 'Connessione',
'statusDisconnecting': 'Disconnessione...',
'statusDisconnected' : 'Disconnesso',
'statusAuthfail': 'Autenticazione fallita',
'roomSubject' : 'Oggetto:',
'messageSubmit': 'Invia',
'labelUsername': 'Nome utente:',
'labelPassword': 'Password:',
'loginSubmit' : 'Login',
'loginInvalid' : 'JID non valido',
'reason' : 'Ragione:',
'subject' : 'Oggetto:',
'reasonWas' : 'Ragione precedente: %s.',
'kickActionLabel' : 'Espelli',
'youHaveBeenKickedBy' : 'Sei stato espulso da %2$s da %1$s',
'youHaveBeenKicked' : 'Sei stato espulso da %s',
'banActionLabel' : 'Escluso',
'youHaveBeenBannedBy' : 'Sei stato escluso da %1$s da %2$s',
'youHaveBeenBanned' : 'Sei stato escluso da %s',
'privateActionLabel' : 'Stanza privata',
'ignoreActionLabel' : 'Ignora',
'unignoreActionLabel' : 'Non ignorare',
'setSubjectActionLabel': 'Cambia oggetto',
'administratorMessageSubject' : 'Amministratore',
'userJoinedRoom' : '%s si è unito alla stanza.',
'userLeftRoom' : '%s ha lasciato la stanza.',
'userHasBeenKickedFromRoom': '%s è stato espulso dalla stanza.',
'userHasBeenBannedFromRoom': '%s è stato escluso dalla stanza.',
'presenceUnknownWarningSubject': 'Nota:',
'presenceUnknownWarning' : 'Questo utente potrebbe essere offline. Non possiamo tracciare la sua presenza.',
'dateFormat': 'dd/mm/yyyy',
'timeFormat': 'HH:MM:ss',
'tooltipRole' : 'Moderatore',
'tooltipIgnored' : 'Stai ignorando questo utente',
'tooltipEmoticons' : 'Emoticons',
'tooltipSound' : 'Riproduci un suono quando arrivano messaggi privati',
'tooltipAutoscroll' : 'Autoscroll',
'tooltipStatusmessage' : 'Mostra messaggi di stato',
'tooltipAdministration' : 'Amministrazione stanza',
'tooltipUsercount' : 'Partecipanti alla stanza',
'enterRoomPassword' : 'La stanza "%s" è protetta da password.',
'enterRoomPasswordSubmit' : 'Unisciti alla stanza',
'passwordEnteredInvalid' : 'Password non valida per la stanza "%s".',
'nicknameConflict': 'Nome utente già in uso. Scegline un altro.',
'errorMembersOnly': 'Non puoi unirti alla stanza "%s": Permessi insufficienti.',
'errorMaxOccupantsReached': 'Non puoi unirti alla stanza "%s": Troppi partecipanti.',
'antiSpamMessage' : 'Per favore non scrivere messaggi pubblicitari. Sei stato bloccato per un po\' di tempo.'
},
'pt': {
'status': 'Status: %s',
'statusConnecting': 'Conectando...',
'statusConnected' : 'Conectado',
'statusDisconnecting': 'Desligando...',
'statusDisconnected' : 'Desligado',
'statusAuthfail': 'Falha na autenticação',
'roomSubject' : 'Assunto:',
'messageSubmit': 'Enviar',
'labelUsername': 'Usuário:',
'labelPassword': 'Senha:',
'loginSubmit' : 'Entrar',
'loginInvalid' : 'JID inválido',
'reason' : 'Motivo:',
'subject' : 'Assunto:',
'reasonWas' : 'O motivo foi: %s.',
'kickActionLabel' : 'Excluir',
'youHaveBeenKickedBy' : 'Você foi excluido de %1$s por %2$s',
'youHaveBeenKicked' : 'Você foi excluido de %s',
'banActionLabel' : 'Bloquear',
'youHaveBeenBannedBy' : 'Você foi excluido permanentemente de %1$s por %2$s',
'youHaveBeenBanned' : 'Você foi excluido permanentemente de %s',
'privateActionLabel' : 'Bate-papo privado',
'ignoreActionLabel' : 'Ignorar',
'unignoreActionLabel' : 'Não ignorar',
'setSubjectActionLabel': 'Trocar Assunto',
'administratorMessageSubject' : 'Administrador',
'userJoinedRoom' : '%s entrou na sala.',
'userLeftRoom' : '%s saiu da sala.',
'userHasBeenKickedFromRoom': '%s foi excluido da sala.',
'userHasBeenBannedFromRoom': '%s foi excluido permanentemente da sala.',
'presenceUnknownWarning' : 'Este usuário pode estar desconectado. Não é possível determinar o status.',
'dateFormat': 'dd.mm.yyyy',
'timeFormat': 'HH:MM:ss',
'tooltipRole' : 'Moderador',
'tooltipIgnored' : 'Você ignora este usuário',
'tooltipEmoticons' : 'Emoticons',
'tooltipSound' : 'Reproduzir o som para novas mensagens privados',
'tooltipAutoscroll' : 'Deslocamento automático',
'tooltipStatusmessage' : 'Mostrar mensagens de status',
'tooltipAdministration' : 'Administração da sala',
'tooltipUsercount' : 'Usuários na sala',
'enterRoomPassword' : 'A sala "%s" é protegida por senha.',
'enterRoomPasswordSubmit' : 'Junte-se à sala',
'passwordEnteredInvalid' : 'Senha incorreta para a sala "%s".',
'nicknameConflict': 'O nome de usuário já está em uso. Por favor, escolha outro.',
'errorMembersOnly': 'Você não pode participar da sala "%s": privilégios insuficientes.',
'errorMaxOccupantsReached': 'Você não pode participar da sala "%s": muitos participantes.',
'antiSpamMessage' : 'Por favor, não envie spam. Você foi bloqueado temporariamente.'
},
'pt_br' : {
'status': 'Estado: %s',
'statusConnecting': 'Conectando...',
'statusConnected' : 'Conectado',
'statusDisconnecting': 'Desconectando...',
'statusDisconnected' : 'Desconectado',
'statusAuthfail': 'Autenticação falhou',
'roomSubject' : 'Assunto:',
'messageSubmit': 'Enviar',
'labelUsername': 'Usuário:',
'labelPassword': 'Senha:',
'loginSubmit' : 'Entrar',
'loginInvalid' : 'JID inválido',
'reason' : 'Motivo:',
'subject' : 'Assunto:',
'reasonWas' : 'Motivo foi: %s.',
'kickActionLabel' : 'Derrubar',
'youHaveBeenKickedBy' : 'Você foi derrubado de %2$s por %1$s',
'youHaveBeenKicked' : 'Você foi derrubado de %s',
'banActionLabel' : 'Banir',
'youHaveBeenBannedBy' : 'Você foi banido de %1$s por %2$s',
'youHaveBeenBanned' : 'Você foi banido de %s',
'privateActionLabel' : 'Conversa privada',
'ignoreActionLabel' : 'Ignorar',
'unignoreActionLabel' : 'Não ignorar',
'setSubjectActionLabel': 'Mudar Assunto',
'administratorMessageSubject' : 'Administrador',
'userJoinedRoom' : '%s entrou na sala.',
'userLeftRoom' : '%s saiu da sala.',
'userHasBeenKickedFromRoom': '%s foi derrubado da sala.',
'userHasBeenBannedFromRoom': '%s foi banido da sala.',
'presenceUnknownWarningSubject': 'Aviso:',
'presenceUnknownWarning' : 'Este usuário pode estar desconectado.. Não conseguimos rastrear sua presença..',
'dateFormat': 'dd.mm.yyyy',
'timeFormat': 'HH:MM:ss',
'tooltipRole' : 'Moderador',
'tooltipIgnored' : 'Você ignora este usuário',
'tooltipEmoticons' : 'Emoticons',
'tooltipSound' : 'Tocar som para novas mensagens privadas',
'tooltipAutoscroll' : 'Auto-rolagem',
'tooltipStatusmessage' : 'Exibir mensagens de estados',
'tooltipAdministration' : 'Administração de Sala',
'tooltipUsercount' : 'Participantes da Sala',
'enterRoomPassword' : 'Sala "%s" é protegida por senha.',
'enterRoomPasswordSubmit' : 'Entrar na sala',
'passwordEnteredInvalid' : 'Senha inváida para sala "%s".',
'nicknameConflict': 'Nome de usuário já em uso. Por favor escolha outro.',
'errorMembersOnly': 'Você não pode entrar na sala "%s": privilégios insuficientes.',
'errorMaxOccupantsReached': 'Você não pode entrar na sala "%s": máximo de participantes atingido.',
'antiSpamMessage' : 'Por favor, não faça spam. Você foi bloqueado temporariamente.'
},
'ru' : {
'status': 'Статус: %s',
'statusConnecting': 'Подключение...',
'statusConnected' : 'Подключено',
'statusDisconnecting': 'Отключение...',
'statusDisconnected' : 'Отключено',
'statusAuthfail': 'Неверный логин',
'roomSubject' : 'Топик:',
'messageSubmit': 'Послать',
'labelUsername': 'Имя:',
'labelPassword': 'Пароль:',
'loginSubmit' : 'Логин',
'loginInvalid' : 'Неверный JID',
'reason' : 'Причина:',
'subject' : 'Топик:',
'reasonWas' : 'Причина была: %s.',
'kickActionLabel' : 'Выбросить',
'youHaveBeenKickedBy' : 'Пользователь %1$s выбросил вас из чата %2$s',
'youHaveBeenKicked' : 'Вас выбросили из чата %s',
'banActionLabel' : 'Запретить доступ',
'youHaveBeenBannedBy' : 'Пользователь %1$s запретил вам доступ в чат %2$s',
'youHaveBeenBanned' : 'Вам запретили доступ в чат %s',
'privateActionLabel' : 'Один-на-один чат',
'ignoreActionLabel' : 'Игнорировать',
'unignoreActionLabel' : 'Отменить игнорирование',
'setSubjectActionLabel': 'Изменить топик',
'administratorMessageSubject' : 'Администратор',
'userJoinedRoom' : '%s вошёл в чат.',
'userLeftRoom' : '%s вышел из чата.',
'userHasBeenKickedFromRoom': '%s выброшен из чата.',
'userHasBeenBannedFromRoom': '%s запрещён доступ в чат.',
'presenceUnknownWarningSubject': 'Уведомление:',
'presenceUnknownWarning' : 'Этот пользователь вероятнее всего оффлайн.',
'dateFormat': 'mm.dd.yyyy',
'timeFormat': 'HH:MM:ss',
'tooltipRole' : 'Модератор',
'tooltipIgnored' : 'Вы игнорируете этого пользователя.',
'tooltipEmoticons' : 'Смайлики',
'tooltipSound' : 'Озвучивать новое частное сообщение',
'tooltipAutoscroll' : 'Авто-прокручивание',
'tooltipStatusmessage' : 'Показывать статус сообщения',
'tooltipAdministration' : 'Администрирование чат комнаты',
'tooltipUsercount' : 'Участники чата',
'enterRoomPassword' : 'Чат комната "%s" защищена паролем.',
'enterRoomPasswordSubmit' : 'Войти в чат',
'passwordEnteredInvalid' : 'Неверный пароль для комнаты "%s".',
'nicknameConflict': 'Это имя уже используется. Пожалуйста выберите другое имя.',
'errorMembersOnly': 'Вы не можете войти в чат "%s": Недостаточно прав доступа.',
'errorMaxOccupantsReached': 'Вы не можете войти в чат "%s": Слишком много участников.',
'antiSpamMessage' : 'Пожалуйста не рассылайте спам. Вас заблокировали на короткое время.'
},
'ca': {
'status': 'Estat: %s',
'statusConnecting': 'Connectant...',
'statusConnected' : 'Connectat',
'statusDisconnecting': 'Desconnectant...',
'statusDisconnected' : 'Desconnectat',
'statusAuthfail': 'Ha fallat la autenticació',
'roomSubject' : 'Assumpte:',
'messageSubmit': 'Enviar',
'labelUsername': 'Usuari:',
'labelPassword': 'Clau:',
'loginSubmit' : 'Entrar',
'loginInvalid' : 'JID no vàlid',
'reason' : 'Raó:',
'subject' : 'Assumpte:',
'reasonWas' : 'La raó ha estat: %s.',
'kickActionLabel' : 'Expulsar',
'youHaveBeenKickedBy' : 'Has estat expulsat de %1$s per %2$s',
'youHaveBeenKicked' : 'Has estat expulsat de %s',
'banActionLabel' : 'Prohibir',
'youHaveBeenBannedBy' : 'Has estat expulsat permanentment de %1$s per %2$s',
'youHaveBeenBanned' : 'Has estat expulsat permanentment de %s',
'privateActionLabel' : 'Xat privat',
'ignoreActionLabel' : 'Ignorar',
'unignoreActionLabel' : 'No ignorar',
'setSubjectActionLabel': 'Canviar assumpte',
'administratorMessageSubject' : 'Administrador',
'userJoinedRoom' : '%s ha entrat a la sala.',
'userLeftRoom' : '%s ha deixat la sala.',
'userHasBeenKickedFromRoom': '%s ha estat expulsat de la sala.',
'userHasBeenBannedFromRoom': '%s ha estat expulsat permanentment de la sala.',
'presenceUnknownWarningSubject': 'Atenció:',
'presenceUnknownWarning' : 'Aquest usuari podria estar desconnectat ...',
'dateFormat': 'dd.mm.yyyy',
'timeFormat': 'HH:MM:ss',
'tooltipRole' : 'Moderador',
'tooltipIgnored' : 'Estàs ignorant aquest usuari',
'tooltipEmoticons' : 'Emoticones',
'tooltipSound' : 'Reproduir un so per a nous missatges',
'tooltipAutoscroll' : 'Desplaçament automàtic',
'tooltipStatusmessage' : 'Mostrar missatges d\'estat',
'tooltipAdministration' : 'Administració de la sala',
'tooltipUsercount' : 'Usuaris dins la sala',
'enterRoomPassword' : 'La sala "%s" està protegida amb contrasenya.',
'enterRoomPasswordSubmit' : 'Entrar a la sala',
'passwordEnteredInvalid' : 'Contrasenya incorrecta per a la sala "%s".',
'nicknameConflict': 'El nom d\'usuari ja s\'està utilitzant. Si us plau, escolleix-ne un altre.',
'errorMembersOnly': 'No pots unir-te a la sala "%s": no tens prous privilegis.',
'errorMaxOccupantsReached': 'No pots unir-te a la sala "%s": hi ha masses participants.',
'antiSpamMessage' : 'Si us plau, no facis spam. Has estat bloquejat temporalment.'
}
};