Commit 57bcf9f3 authored by Dele Olajide's avatar Dele Olajide

Merge pull request #127 from deleolajide/master

Openfire Meetings plugin. Initial release
parents 971f90c8 22226a32
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Openfire Meetings Plugin Changelog</title>
<style type="text/css">
BODY {
font-size: 100%;
}
BODY, TD, TH {
font-family: tahoma, verdana, arial, helvetica, sans-serif;
font-size: 0.8em;
}
H2 {
font-size: 10pt;
font-weight: bold;
padding-left: 1em;
}
A:hover {
text-decoration: none;
}
H1 {
font-family: tahoma, arial, helvetica, sans-serif;
font-size: 1.4em;
font-weight: bold;
border-bottom: 1px #ccc solid;
padding-bottom: 2px;
}
TT {
font-family: courier new;
font-weight: bold;
color: #060;
}
PRE {
font-family: courier new;
font-size: 100%;
}
</style>
</head>
<body>
<h1>
Openfire Meetings Plugin Changelog
</h1>
<p><b>0.0.2</b> -- Nov 30th, 2014</p>
<ul>
<li>Added Candy and Fastpath.</li>
</ul>
<p><b>0.0.1</b> -- Nov 27th, 2014</p>
<ul>
<li>Initial release.</li>
</ul>
</body>
</html>
Some of the files here, notably libjitsi.jar, are OSGi bundles from the Jitsi
project. In Jitsi they are generated in the 'sc-bundles' directory by the
'bundles' ant target. To update the files here, run 'ant rebuild' in Jitsi and
then copy the files here, e.g.:
for f in *.jar; do
if [ -e ../../jitsi/sc-bundles/$f ]; then
cp ../../jitsi/sc-bundles/$f .;
fi;
done
handlers= java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level = ALL
java.util.logging.ConsoleHandler.formatter = net.java.sip.communicator.util.ScLogFormatter
.level=INFO
# FIXME: remove once RTT issue is fixed
org.jitsi.impl.neomedia.MediaStreamStatsImpl.level=WARNING
\ No newline at end of file
# The amount of memory that the java process can consume.
# Use the format that is in use for java's -Xmx switch
# The default is 3072m
# Only effective on linux, 64 bit
# VIDEOBRIDGE_MAX_MEMORY=3072m
# Uncomment the next line to enable the remote debugging of the video bridge
# VIDEOBRIDGE_DEBUG_OPTIONS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000"
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<plugin>
<class>org.jivesoftware.openfire.plugin.ofmeet.OfMeetPlugin</class>
<name>ofmeet</name>
<description>Openfire Meetings</description>
<author></author>
<version>0.0.2</version>
<date>30/11/2014</date>
<minServerVersion>3.10.0</minServerVersion>
<adminconsole>
<tab id="tab-ofmeet" name="${plugin.title}" url="ofmeet-summary.jsp" description="${plugin.description}">
<sidebar id="siderbar-ofmeet" name="${plugin.title}">
<item id="ofmeet-summary" name="${config.page.summary.title}" description="${config.page.summary.description}" url="ofmeet-summary.jsp"/>
<item id="ofmeet-settings" name="${config.page.settings.title}" description="${config.page.settings.description}" url="ofmeet-settings.jsp"/>
</sidebar>
</tab>
</adminconsole>
</plugin>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Openfire Meetings Plugin Readme</title>
<style type="text/css">
BODY {
font-size: 100%;
}
BODY, TD, TH {
font-family: tahoma, verdana, arial, helvetica, sans-serif;
font-size: 0.8em;
}
H2 {
font-size: 10pt;
font-weight: bold;
}
A:hover {
text-decoration: none;
}
H1 {
font-family: tahoma, arial, helvetica, sans-serif;
font-size: 1.4em;
font-weight: bold;
border-bottom: 1px #ccc solid;
padding-bottom: 2px;
}
TT {
font-family: courier new;
font-weight: bold;
color: #060;
}
PRE {
font-family: courier new;
font-size: 100%;
}
</style>
</head>
<body>
<h1>
Openfire Meetings Plugin Readme
</h1>
<h2>Overview</h2>
<p>
Openfire Meetings
</p>
<h2>Installation</h2>
</body>
</html>
# Server Installation for Jitsi Meet
This describes configuring a server `jitsi.example.com`. You will need to
change references to that to match your host, and generate some passwords for
`YOURSECRET1` and `YOURSECRET2`.
There are also some complete [example config files](https://github.com/jitsi/jitsi-meet/tree/master/doc/example-config-files/) available, mentioned in each section.
## Install prosody and otalk modules
```sh
apt-get install lsb-release
echo deb http://packages.prosody.im/debian $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list
wget --no-check-certificate https://prosody.im/files/prosody-debian-packages.key -O- | sudo apt-key add -
apt-get update
apt-get install prosody-trunk
apt-get install git lua-zlib lua-sec-prosody lua-dbi-sqlite3 liblua5.1-bitop-dev liblua5.1-bitop0
git clone https://github.com/andyet/otalk-server.git
cd otalk-server
cp -r mod* /usr/lib/prosody/modules
```
## Configure prosody
Modify the config file in `/etc/prosody/prosody.cfg.lua` (see also the example config file):
- modules to enable/add: compression, bosh, smacks, carbons, mam, lastactivity, offline, pubsub, adhoc, websocket, http_altconnect
- comment out: `c2s_require_encryption = true`, and `s2s_secure_auth = false`
- change `authentication = "internal_hashed"`
- add this:
```
daemonize = true
cross_domain_bosh = true;
storage = {archive2 = "sql2"}
sql = { driver = "SQLite3", database = "prosody.sqlite" }
default_archive_policy = "roster"
```
- configure your domain by editing the example.com virtual host section section:
```
VirtualHost "jitsi.example.com"
authentication = "anonymous"
ssl = {
key = "/var/lib/prosody/jitsi.example.com.key";
certificate = "/var/lib/prosody/jitsi.example.com.crt";
}
```
- and finally configure components:
```
Component "conference.jitsi.example.com" "muc"
Component "jitsi-videobridge.jitsi.example.com"
component_secret = "YOURSECRET1"
```
Generate certs for the domain:
```sh
prosodyctl cert generate jitsi.example.com
```
Restart prosody XMPP server with the new config
```sh
prosodyctl restart
```
## Install nginx
```sh
apt-get install nginx
```
Add nginx config for domain in `/etc/nginx/nginx.conf`:
```
tcp_nopush on;
types_hash_max_size 2048;
server_names_hash_bucket_size 64;
```
Add a new file `jitsi.example.com` in `/etc/nginx/sites-available` (see also the example config file):
```
server {
listen 80;
server_name jitsi.example.com;
# set the root
root /srv/jitsi.example.com;
index index.html;
location ~ ^/([a-zA-Z0-9]+)$ {
rewrite ^/(.*)$ / break;
}
# BOSH
location /http-bind {
proxy_pass http://localhost:5280/http-bind;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
}
# xmpp websockets
location /xmpp-websocket {
proxy_pass http://localhost:5280;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
tcp_nodelay on;
}
}
```
Add link for the added configuration
```sh
cd /etc/nginx/sites-enabled
ln -s ../sites-available/jitsi.example.com jitsi.example.com
```
## Fix firewall if needed
```sh
ufw allow 80
ufw allow 5222
```
## Install Jitsi Videobridge
```sh
wget https://download.jitsi.org/jitsi-videobridge/linux/jitsi-videobridge-linux-{arch-buildnum}.zip
unzip jitsi-videobridge-linux-{arch-buildnum}.zip
```
Install JRE if missing:
```
apt-get install default-jre
```
In the user home that will be starting Jitsi Videobridge create `.sip-communicator` folder and add the file `sip-communicator.properties` with one line in it:
```
org.jitsi.impl.neomedia.transform.srtp.SRTPCryptoContext.checkReplay=false
```
Start the videobridge with:
```sh
./jvb.sh --host=localhost --domain=jitsi.example.com --port=5347 --secret=YOURSECRET1 &
```
Or autostart it by adding the line in `/etc/rc.local`:
```sh
/bin/bash /root/jitsi-videobridge-linux-{arch-buildnum}/jvb.sh --host=localhost --domain=jitsi.example.com --port=5347 --secret=YOURSECRET1 </dev/null >> /var/log/jvb.log 2>&1
```
## Deploy Jitsi Meet
Checkout and configure Jitsi Meet:
```sh
cd /srv
git clone https://github.com/jitsi/jitsi-meet.git
mv jitsi-meet/ jitsi.example.com
```
Edit host names in `/srv/jitsi.example.com/config.js` (see also the example config file):
```
var config = {
hosts: {
domain: 'jitsi.example.com',
muc: 'conference.jitsi.example.com',
bridge: 'jitsi-videobridge.jitsi.example.com'
},
useNicks: false,
bosh: '//jitsi.example.com/http-bind', // FIXME: use xep-0156 for that
desktopSharing: 'false' // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
//chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension
//minChromeExtVersion: '0.1' // Required version of Chrome extension
};
```
Restart nginx to get the new configuration:
```sh
invoke-rc.d nginx restart
```
## Install [Turn server](https://github.com/andyet/otalk-server/tree/master/restund)
```sh
apt-get install make gcc
wget http://creytiv.com/pub/re-0.4.7.tar.gz
tar zxvf re-0.4.7.tar.gz
ln -s re-0.4.7 re
cd re-0.4.7
sudo make install PREFIX=/usr
cd ..
wget http://creytiv.com/pub/restund-0.4.2.tar.gz
wget https://raw.github.com/andyet/otalk-server/master/restund/restund-auth.patch
tar zxvf restund-0.4.2.tar.gz
cd restund-0.4.2/
patch -p1 < ../restund-auth.patch
sudo make install PREFIX=/usr
cp debian/restund.init /etc/init.d/restund
chmod +x /etc/init.d/restund
cd /etc
wget https://raw.github.com/andyet/otalk-server/master/restund/restund.conf
```
Configure addresses and ports as desired, and the password to be configured in prosody:
```
realm jitsi.example.com
# share this with your prosody server
auth_shared YOURSECRET2
# modules
module_path /usr/lib/restund/modules
turn_relay_addr [turn ip address]
```
Configure prosody to use it in `/etc/prosody/prosody.cfg.lua`. Add to your virtual host:
```
turncredentials_secret = "YOURSECRET2";
turncredentials = {
{ type = "turn", host = "turn.address.ip.configured", port = 3478, transport = "tcp" }
}
```
Add turncredentials module in the "modules_enabled" section
Reload prosody if needed
```
prosodyctl restart
```
## Running behind NAT
In case of videobridge being installed on a machine behind NAT, add the following extra lines to the file `~/.sip-communicator/sip-communicator.properties` (in the home of user running the videobridge):
```
org.jitsi.videobridge.NAT_HARVESTER_LOCAL_ADDRESS=<Local.IP.Address>
org.jitsi.videobridge.NAT_HARVESTER_PUBLIC_ADDRESS=<Public.IP.Address>
```
So the file should look like this at the end:
```
org.jitsi.impl.neomedia.transform.srtp.SRTPCryptoContext.checkReplay=false
org.jitsi.videobridge.NAT_HARVESTER_LOCAL_ADDRESS=<Local.IP.Address>
org.jitsi.videobridge.NAT_HARVESTER_PUBLIC_ADDRESS=<Public.IP.Address>
```
# Hold your first conference
You are now all set and ready to have your first meet by going to http://jitsi.example.com
## Enabling recording
Currently recording is only supported for linux-64 and macos. To enable it, add
the following properties to sip-communicator.properties:
```
org.jitsi.videobridge.ENABLE_MEDIA_RECORDING=true
org.jitsi.videobridge.MEDIA_RECORDING_PATH=/path/to/recordings/dir
org.jitsi.videobridge.MEDIA_RECORDING_TOKEN=secret
```
where /path/to/recordings/dir is the path to a pre-existing directory where recordings
will be stored (needs to be writeable by the user running jitsi-videobridge),
and "secret" is a string which will be used for authentication.
Then, edit the Jitsi-Meet config.js file and set:
```
enableRecording: true
```
Restart jitsi-videobridge and start a new conference (making sure that the page
is reloaded with the new config.js) -- the organizer of the conference should
now have a "recoriding" button in the floating menu, near the "mute" button.
The MIT License (MIT)
Copyright (c) 2013 ESTOS GmbH
Copyright (c) 2013 BlueJimp SARL
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.
Jitsi Meet - Secure, Simple and Scalable Video Conferences
====
Jitsi Meet is an OpenSource (MIT) WebRTC JavaScript application that uses [Jitsi Videobridge](https://jitsi.org/videobridge) to provide high quality, scalable video conferences. You can see [Jitsi Meet in action](http://youtu.be/7vFUVClsNh0) here at the 482 session of the VoIP Users Conference.
You can also try it out yourself at https://meet.jit.si .
Jitsi Meet allows for very efficient collaboration. It allows users to stream their desktop or only some windows. It also supports shared document editing with Etherpad and remote presentations with Prezi.
## Install
Installing Jitsi Meet is quite a simple experience even though it requires installing a few other components first, such as Jitsi Videobridge, a web server such as Nginx and an XMPP one like Prosody.
You can find information on how to deploy Jitsi Meet in the [installation instructions](https://jitsi.org/meet/deploy)
You may also find it helpful to have a look at our sample [config files](https://github.com/jitsi/jitsi-meet/tree/master/doc/example-config-files/)
## Discuss
Please use the [Jitsi dev mailing list](http://lists.jitsi.org/pipermail/dev/) to discuss feature requests before opening an issue on github.
## Acknowledgements
Jitsi Meet started out as a sample conferencing application using Jitsi Videobridge. It was originally developed by Philipp Hancke who then contributed it to the community where development continues with joint forces!
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<servlet>
<servlet-name>proxy</servlet-name>
<servlet-class>org.jitsi.videobridge.openfire.HttpProxy</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>proxy</servlet-name>
<url-pattern>/proxy</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>config</servlet-name>
<servlet-class>org.jitsi.videobridge.openfire.Config</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>config</servlet-name>
<url-pattern>/config</url-pattern>
</servlet-mapping>
</web-app>
\ No newline at end of file
/**
* Google Analytics
*/
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-319188-14', 'jit.si');
ga('send', 'pageview');
\ No newline at end of file
/**
* Implements API class that communicates with external api class
* and provides interface to access Jitsi Meet features by external
* applications that embed Jitsi Meet
*/
var APIConnector = (function () {
function APIConnector() { }
/**
* List of the available commands.
* @type {{
* displayName: inputDisplayNameHandler,
* muteAudio: toggleAudio,
* muteVideo: toggleVideo,
* filmStrip: toggleFilmStrip
* }}
*/
var commands =
{
displayName: VideoLayout.inputDisplayNameHandler,
muteAudio: toggleAudio,
muteVideo: toggleVideo,
toggleFilmStrip: BottomToolbar.toggleFilmStrip,
toggleChat: BottomToolbar.toggleChat,
toggleContactList: BottomToolbar.toggleContactList
};
/**
* Maps the supported events and their status
* (true it the event is enabled and false if it is disabled)
* @type {{
* incomingMessage: boolean,
* outgoingMessage: boolean,
* displayNameChange: boolean,
* participantJoined: boolean,
* participantLeft: boolean
* }}
*/
var events =
{
incomingMessage: false,
outgoingMessage:false,
displayNameChange: false,
participantJoined: false,
participantLeft: false
};
/**
* Check whether the API should be enabled or not.
* @returns {boolean}
*/
APIConnector.isEnabled = function () {
var hash = location.hash;
if(hash && hash.indexOf("external") > -1 && window.postMessage)
return true;
return false;
};
/**
* Initializes the APIConnector. Setups message event listeners that will
* receive information from external applications that embed Jitsi Meet.
* It also sends a message to the external application that APIConnector
* is initialized.
*/
APIConnector.init = function () {
if (window.addEventListener)
{
window.addEventListener('message',
APIConnector.processMessage, false);
}
else
{
window.attachEvent('onmessage', APIConnector.processMessage);
}
APIConnector.sendMessage({type: "system", loaded: true});
};
/**
* Sends message to the external application.
* @param object
*/
APIConnector.sendMessage = function (object) {
window.parent.postMessage(JSON.stringify(object), "*");
};
/**
* Processes a message event from the external application
* @param event the message event
*/
APIConnector.processMessage = function(event)
{
var message;
try {
message = JSON.parse(event.data);
} catch (e) {}
if(!message.type)
return;
switch (message.type)
{
case "command":
APIConnector.processCommand(message);
break;
case "event":
APIConnector.processEvent(message);
break;
default:
console.error("Unknown type of the message");
return;
}
};
/**
* Processes commands from external applicaiton.
* @param message the object with the command
*/
APIConnector.processCommand = function (message)
{
if(message.action != "execute")
{
console.error("Unknown action of the message");
return;
}
for(var key in message)
{
if(commands[key])
commands[key].apply(null, message[key]);
}
};
/**
* Processes events objects from external applications
* @param event the event
*/
APIConnector.processEvent = function (event) {
if(!event.action)
{
console.error("Event with no action is received.");
return;
}
switch(event.action)
{
case "add":
for(var i = 0; i < event.events.length; i++)
{
events[event.events[i]] = true;
}
break;
case "remove":
for(var i = 0; i < event.events.length; i++)
{
events[event.events[i]] = false;
}
break;
default:
console.error("Unknown action for event.");
}
};
/**
* Checks whether the event is enabled ot not.
* @param name the name of the event.
* @returns {*}
*/
APIConnector.isEventEnabled = function (name) {
return events[name];
};
/**
* Sends event object to the external application that has been subscribed
* for that event.
* @param name the name event
* @param object data associated with the event
*/
APIConnector.triggerEvent = function (name, object) {
APIConnector.sendMessage({
type: "event", action: "result", event: name, result: object});
};
/**
* Removes the listeners.
*/
APIConnector.dispose = function () {
if(window.removeEventListener)
{
window.removeEventListener("message",
APIConnector.processMessage, false);
}
else
{
window.detachEvent('onmessage', APIConnector.processMessage);
}
};
return APIConnector;
})();
\ No newline at end of file
This diff is collapsed.
/**
* The audio Levels plugin.
*/
var AudioLevels = (function(my) {
var audioLevelCanvasCache = {};
my.LOCAL_LEVEL = 'local';
/**
* Updates the audio level canvas for the given peerJid. If the canvas
* didn't exist we create it.
*/
my.updateAudioLevelCanvas = function (peerJid) {
var resourceJid = null;
var videoSpanId = null;
if (!peerJid)
videoSpanId = 'localVideoContainer';
else {
resourceJid = Strophe.getResourceFromJid(peerJid);
videoSpanId = 'participant_' + resourceJid;
}
videoSpan = document.getElementById(videoSpanId);
if (!videoSpan) {
if (resourceJid)
console.error("No video element for jid", resourceJid);
else
console.error("No video element for local video.");
return;
}
var audioLevelCanvas = $('#' + videoSpanId + '>canvas');
var videoSpaceWidth = $('#remoteVideos').width();
var thumbnailSize
= VideoLayout.calculateThumbnailSize(videoSpaceWidth);
var thumbnailWidth = thumbnailSize[0];
var thumbnailHeight = thumbnailSize[1];
if (!audioLevelCanvas || audioLevelCanvas.length === 0) {
audioLevelCanvas = document.createElement('canvas');
audioLevelCanvas.className = "audiolevel";
audioLevelCanvas.style.bottom = "-" + interfaceConfig.CANVAS_EXTRA/2 + "px";
audioLevelCanvas.style.left = "-" + interfaceConfig.CANVAS_EXTRA/2 + "px";
resizeAudioLevelCanvas( audioLevelCanvas,
thumbnailWidth,
thumbnailHeight);
videoSpan.appendChild(audioLevelCanvas);
} else {
audioLevelCanvas = audioLevelCanvas.get(0);
resizeAudioLevelCanvas( audioLevelCanvas,
thumbnailWidth,
thumbnailHeight);
}
};
/**
* Updates the audio level UI for the given resourceJid.
*
* @param resourceJid the resource jid indicating the video element for
* which we draw the audio level
* @param audioLevel the newAudio level to render
*/
my.updateAudioLevel = function (resourceJid, audioLevel) {
drawAudioLevelCanvas(resourceJid, audioLevel);
var videoSpanId = getVideoSpanId(resourceJid);
var audioLevelCanvas = $('#' + videoSpanId + '>canvas').get(0);
if (!audioLevelCanvas)
return;
var drawContext = audioLevelCanvas.getContext('2d');
var canvasCache = audioLevelCanvasCache[resourceJid];
drawContext.clearRect (0, 0,
audioLevelCanvas.width, audioLevelCanvas.height);
drawContext.drawImage(canvasCache, 0, 0);
};
/**
* Resizes the given audio level canvas to match the given thumbnail size.
*/
function resizeAudioLevelCanvas(audioLevelCanvas,
thumbnailWidth,
thumbnailHeight) {
audioLevelCanvas.width = thumbnailWidth + interfaceConfig.CANVAS_EXTRA;
audioLevelCanvas.height = thumbnailHeight + interfaceConfig.CANVAS_EXTRA;
};
/**
* Draws the audio level canvas into the cached canvas object.
*
* @param resourceJid the resource jid indicating the video element for
* which we draw the audio level
* @param audioLevel the newAudio level to render
*/
function drawAudioLevelCanvas(resourceJid, audioLevel) {
if (!audioLevelCanvasCache[resourceJid]) {
var videoSpanId = getVideoSpanId(resourceJid);
var audioLevelCanvasOrig = $('#' + videoSpanId + '>canvas').get(0);
/*
* FIXME Testing has shown that audioLevelCanvasOrig may not exist.
* In such a case, the method CanvasUtil.cloneCanvas may throw an
* error. Since audio levels are frequently updated, the errors have
* been observed to pile into the console, strain the CPU.
*/
if (audioLevelCanvasOrig)
{
audioLevelCanvasCache[resourceJid]
= CanvasUtil.cloneCanvas(audioLevelCanvasOrig);
}
}
var canvas = audioLevelCanvasCache[resourceJid];
if (!canvas)
return;
var drawContext = canvas.getContext('2d');
drawContext.clearRect(0, 0, canvas.width, canvas.height);
var shadowLevel = getShadowLevel(audioLevel);
if (shadowLevel > 0)
// drawContext, x, y, w, h, r, shadowColor, shadowLevel
CanvasUtil.drawRoundRectGlow( drawContext,
interfaceConfig.CANVAS_EXTRA/2, interfaceConfig.CANVAS_EXTRA/2,
canvas.width - interfaceConfig.CANVAS_EXTRA,
canvas.height - interfaceConfig.CANVAS_EXTRA,
interfaceConfig.CANVAS_RADIUS,
interfaceConfig.SHADOW_COLOR,
shadowLevel);
};
/**
* Returns the shadow/glow level for the given audio level.
*
* @param audioLevel the audio level from which we determine the shadow
* level
*/
function getShadowLevel (audioLevel) {
var shadowLevel = 0;
if (audioLevel <= 0.3) {
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*(audioLevel/0.3));
}
else if (audioLevel <= 0.6) {
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3));
}
else {
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4));
}
return shadowLevel;
};
/**
* Returns the video span id corresponding to the given resourceJid or local
* user.
*/
function getVideoSpanId(resourceJid) {
var videoSpanId = null;
if (resourceJid === AudioLevels.LOCAL_LEVEL
|| (connection.emuc.myroomjid && resourceJid
=== Strophe.getResourceFromJid(connection.emuc.myroomjid)))
videoSpanId = 'localVideoContainer';
else
videoSpanId = 'participant_' + resourceJid;
return videoSpanId;
};
/**
* Indicates that the remote video has been resized.
*/
$(document).bind('remotevideo.resized', function (event, width, height) {
var resized = false;
$('#remoteVideos>span>canvas').each(function() {
var canvas = $(this).get(0);
if (canvas.width !== width + interfaceConfig.CANVAS_EXTRA) {
canvas.width = width + interfaceConfig.CANVAS_EXTRA;
resized = true;
}
if (canvas.heigh !== height + interfaceConfig.CANVAS_EXTRA) {
canvas.height = height + interfaceConfig.CANVAS_EXTRA;
resized = true;
}
});
if (resized)
Object.keys(audioLevelCanvasCache).forEach(function (resourceJid) {
audioLevelCanvasCache[resourceJid].width
= width + interfaceConfig.CANVAS_EXTRA;
audioLevelCanvasCache[resourceJid].height
= height + interfaceConfig.CANVAS_EXTRA;
});
});
return my;
})(AudioLevels || {});
var Avatar = (function(my) {
var users = {};
var activeSpeakerJid;
/**
* Sets the user's avatar in the settings menu(if local user), contact list
* and thumbnail
* @param jid jid of the user
* @param id email or userID to be used as a hash
*/
my.setUserAvatar = function(jid, id) {
if(id) {
if(users[jid] === id) {
return;
}
users[jid] = id;
}
var url = getGravatarUrl(users[jid] || jid);
var resourceJid = Strophe.getResourceFromJid(jid);
var thumbnail = $('#participant_' + resourceJid);
var avatar = $('#avatar_' + resourceJid);
// set the avatar in the settings menu if it is local user and get the
// local video container
if(jid === connection.emuc.myroomjid) {
$('#avatar').get(0).src = url;
thumbnail = $('#localVideoContainer');
}
// set the avatar in the contact list
var contact = $('#' + resourceJid + '>img');
if(contact && contact.length > 0) {
contact.get(0).src = url;
}
// set the avatar in the thumbnail
if(avatar && avatar.length > 0) {
avatar[0].src = url;
} else {
if (thumbnail && thumbnail.length > 0) {
avatar = document.createElement('img');
avatar.id = 'avatar_' + resourceJid;
avatar.className = 'userAvatar';
avatar.src = url;
thumbnail.append(avatar);
}
}
//if the user is the current active speaker - update the active speaker
// avatar
if(jid === activeSpeakerJid) {
Avatar.updateActiveSpeakerAvatarSrc(jid);
}
};
/**
* Hides or shows the user's avatar
* @param jid jid of the user
* @param show whether we should show the avatar or not
* video because there is no dominant speaker and no focused speaker
*/
my.showUserAvatar = function(jid, show) {
if(users[jid]) {
var resourceJid = Strophe.getResourceFromJid(jid);
var video = $('#participant_' + resourceJid + '>video');
var avatar = $('#avatar_' + resourceJid);
if(jid === connection.emuc.myroomjid) {
video = $('#localVideoWrapper>video');
}
if(show === undefined || show === null) {
show = isUserMuted(jid);
}
//if the user is the currently focused, the dominant speaker or if
//there is no focused and no dominant speaker
if (activeSpeakerJid === jid) {
setVisibility($("#largeVideo"), !show);
setVisibility($('#activeSpeakerAvatar'), show);
setVisibility(avatar, false);
setVisibility(video, false);
} else {
if (video && video.length > 0) {
setVisibility(video, !show);
setVisibility(avatar, show);
}
}
}
};
/**
* Updates the src of the active speaker avatar
* @param jid of the current active speaker
*/
my.updateActiveSpeakerAvatarSrc = function(jid) {
if(!jid) {
if (focusedVideoSrc) {
jid = getJidFromVideoSrc(focusedVideoSrc);
} else {
jid = connection.emuc.findJidFromResource(
VideoLayout.getDominantSpeakerResourceJid());
}
}
var avatar = $("#activeSpeakerAvatar")[0];
var url = getGravatarUrl(users[jid],
interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE);
if(jid === activeSpeakerJid && avatar.src === url) {
return;
}
activeSpeakerJid = jid;
var isMuted = isUserMuted(jid);
if(jid && isMuted !== null) {
avatar.src = url;
setVisibility($("#largeVideo"), !isMuted);
Avatar.showUserAvatar(jid, isMuted);
}
};
function setVisibility(selector, show) {
if (selector && selector.length > 0) {
selector.css("visibility", show ? "visible" : "hidden");
}
}
function isUserMuted(jid) {
if(!mediaStreams[jid] || !mediaStreams[jid][MediaStream.VIDEO_TYPE]) {
return null;
}
return mediaStreams[jid][MediaStream.VIDEO_TYPE].muted;
}
function getGravatarUrl(id, size) {
if(id === connection.emuc.myroomjid || !id) {
id = SettingsMenu.getUID();
}
return 'https://www.gravatar.com/avatar/' +
MD5.hexdigest(id.trim().toLowerCase()) +
"?d=retro&size=" + (size || "30");
}
return my;
}(Avatar || {}));
var BottomToolbar = (function (my) {
my.toggleChat = function() {
if (ContactList.isVisible()) {
buttonClick("#contactListButton", "active");
$('#contactlist').css('z-index', 4);
setTimeout(function() {
$('#contactlist').css('display', 'none');
$('#contactlist').css('z-index', 5);
}, 500);
}
Chat.toggleChat();
buttonClick("#chatBottomButton", "active");
};
my.toggleContactList = function() {
if (Chat.isVisible()) {
buttonClick("#chatBottomButton", "active");
setTimeout(function() {
$('#chatspace').css('display', 'none');
}, 500);
}
buttonClick("#contactListButton", "active");
ContactList.toggleContactList();
};
my.toggleFilmStrip = function() {
var filmstrip = $("#remoteVideos");
filmstrip.toggleClass("hidden");
};
$(document).bind("remotevideo.resized", function (event, width, height) {
var bottom = (height - $('#bottomToolbar').outerHeight())/2 + 18;
$('#bottomToolbar').css({bottom: bottom + 'px'});
});
return my;
}(BottomToolbar || {}));
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Candy - Chats are not dead yet</title>
<link rel="shortcut icon" href="candy/res/img/favicon.png" type="image/gif" />
<link rel="stylesheet" type="text/css" href="candy/res/default.css" />
<script src="/ofmeet/config"></script><!-- BAO -->
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript" src="candy/libs/libs.bundle.js"></script>
<script type="text/javascript" src="candy/libs/strophe.openfire.js"></script>
<script type="text/javascript" src="candy/candy.bundle.js"></script>
<script type="text/javascript" src="candy/candyshop/colors/candy.js"></script>
<link rel="stylesheet" type="text/css" href="candy/candyshop/colors/candy.css" />
<script type="text/javascript" src="candy/candyshop/notifications/candy.js"></script>
<script type="text/javascript" src="candy/candyshop/notifyme/candy.js"></script>
<link rel="stylesheet" type="text/css" href="candy/candyshop/notifyme/candy.css" />
<script type="text/javascript" src="candy/candyshop/ofmeet/candy.js"></script>
<link rel="stylesheet" type="text/css" href="candy/candyshop/ofmeet/candy.css" />
<script type="text/javascript" src="candy/candyshop/fastpath/candy.js"></script>
<script type="text/javascript" src="candy/candyshop/slash-commands/slash-commands.js"></script>
<script type="text/javascript" src="candy/candyshop/timeago/candy.js"></script>
<link rel="stylesheet" type="text/css" href="candy/candyshop/timeago/candy.css" />
<script type="text/javascript" src="candy/candyshop/typingnotifications/typingnotifications.js"></script>
<link rel="stylesheet" type="text/css" href="candy/candyshop/typingnotifications/typingnotifications.css" />
<script type="text/javascript">
$(document).ready(function() {
$("<div>").attr("id","candy").attr("style","width: 100%; height: 100%;").appendTo("body");
Candy.init(config.bosh, {
core: {
// only set this to true if developing / debugging errors
debug: false,
autojoin: true
},
view: { assets: 'candy/res/' }
});
var jid = config.hosts.domain;
if (config.userName)
{
jid = config.userName + "@" + config.hosts.domain;
}
CandyShop.OfMeet.init();
CandyShop.SlashCommands.defaultConferenceDomain = config.hosts.muc;
CandyShop.SlashCommands.init();
CandyShop.Timeago.init();
CandyShop.TypingNotifications.init();
CandyShop.Colors.init();
CandyShop.Notifications.init();
Candy.Core.connect(jid, "null");
CandyShop.NotifyMe.init();
CandyShop.Fastpath.init();
});
window.addEventListener("unload", function ()
{
});
</script>
</head>
<body>
</body>
</html>
# Contributing
## Team members
* Patrick Stadler &middot; [@pstadler](http://twitter.com/pstadler) &middot; <patrick.stadler@gmail.com>
* Michael Weibel &middot; [@weibelm](htps://twitter.com/weibelm) &middot; <michael.weibel@gmail.com>
## Learn & listen
* [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)
## Contributing
You want to help us? **Awesome!**
### How to contribute
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)
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
8. Create a [pull request](https://help.github.com/articles/using-pull-requests)
#### On your own machine
Please note that you should have a working XMPP server to test your changes (the vagrant way does already have a working XMPP server).
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)
In case you have any questions, don't hesitate to ask on the [Mailing list](http://groups.google.com/group/candy-chat).
Credits
=======
- [Special thanks to our contributors](https://github.com/candy-chat/candy/graphs/contributors)
- [famfamfam silk icons](http://www.famfamfam.com/lab/icons/silk/) is a smooth, free icon set, containing over 700 16-by-16 pixel icons.
- [Simple Smileys](http://simplesmileys.org) are beautifully simple emoticons.
- [Flash MP3 Player](http://flash-mp3-player.net/players/js) is a very simple flash audio player used by Candy for audio notifications.
- [Colin Snover](http://zetafleet.com/blog/javascript-dateparse-for-iso-8601) provides a fix for browsers not supporting latest Date.parse().
- [Ben Cherry](http://www.adequatelygood.com/2010/3/JavaScript-Module-Pattern-In-Depth) wrote a great article about the JS module pattern.
- [Amiado Group](http://www.amiadogroup.com) allowed us to make Candy freely available for everyone :)
\ No newline at end of file
'use strict';
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
jshint: {
all: ['Gruntfile.js', './src/**/*.js'],
options: {
jshintrc: "./.jshintrc",
reporter: require('jshint-stylish')
}
},
sync: {
options: {
include: [
'name', 'version', 'main',
'homepage', 'description',
'keywords', 'license',
'repository'
]
}
},
uglify: {
bundle: {
files: {
'candy.bundle.js': [
'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/view/template.js', 'src/view/translation.js'
]
},
options: {
sourceMap: true,
mangle: false,
compress: false,
beautify: true,
preserveComments: 'all'
}
},
min: {
files: {
'candy.min.js': ['candy.bundle.js']
},
options: {
sourceMap: true
}
},
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'
]
},
options: {
sourceMap: true,
mangle: false,
compress: false,
beautify: true,
preserveComments: 'all'
}
},
'libs-min': {
files: {
'libs/libs.min.js': ['libs/libs.bundle.js']
}
}
},
watch: {
bundle: {
files: ['src/*.js', 'src/**/*.js'],
tasks: ['jshint', 'uglify:bundle', 'uglify:min', 'notify:bundle']
},
libs: {
files: ['libs/*/**/*.js'],
tasks: ['uglify:libs', 'uglify:libs-min', 'notify:libs']
}
},
natural_docs: {
all: {
bin: process.env.NATURALDOCS_DIR + '/NaturalDocs',
flags: ['-r'],
inputs: ['./src'],
output: './docs',
project: './.ndproj'
}
},
clean: {
bundle: ['./candy.bundle.js', './candy.bundle.map', './candy.min.js'],
libs: ['./libs/libs.bundle.js', './libs/libs.bundle.map', './libs/libs.min.js'],
docs: ['./docs']
},
mkdir: {
docs: {
options: {
create: ['./docs']
}
}
},
notify: {
bundle: {
options: {
message: 'Bundle & Min updated'
}
},
libs: {
options: {
message: 'Libs updated'
}
},
docs: {
options: {
message: 'Docs done'
}
},
'default': {
options: {
message: 'JsHint & bundling done'
}
}
}
});
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-natural-docs');
grunt.loadNpmTasks('grunt-mkdir');
grunt.loadNpmTasks('grunt-notify');
grunt.loadNpmTasks('grunt-sync-pkg');
grunt.registerTask('default', [
'jshint', 'uglify:libs', 'uglify:libs-min',
'uglify:bundle', 'uglify:min', 'notify:default'
]);
grunt.registerTask('docs', ['mkdir:docs', 'natural_docs', 'notify:docs']);
};
\ No newline at end of file
Copyright (c) 2011 Amiado Group AG
Copyright (c) 2012-2014 Patrick Stadler & Michael Weibel
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.
Candy — a JavaScript-based multi-user chat client
==================================================
Visit the official project page: http://candy-chat.github.io/candy
Features
--------
- Focused on real-time multi-user chatting
- Easy to configure, easy to run, easy to use
- Highly customizable
- 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
Plugins
-------
If you wish to add new functionality (to your candy installation) or contribute plugins, take a look at our [plugin repository](http://github.com/candy-chat/candy-plugins).
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.
[![githalytics.com alpha](https://cruel-carlota.pagodabox.com/a41a8075608abeaf99db685d7ef29cf6 "githalytics.com")](http://githalytics.com/candy-chat/candy)
{
"name": "candy",
"version": "1.7.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",
"main": [
"candy.min.js"
],
"keywords": [
"xmpp",
"muc",
"multi-user",
"websocket",
"bosh",
"chat"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "git://github.com/candy-chat/candy.git"
},
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
]
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
# Colors
Send and receive colored messages.
This plugin uses an own format of messages (`foobar` becomes e.g. `|c:1|foobar`).
If you'd like to use XHTML for formatting messages (this ensures third-party clients could also
display it properly), please use [colors-html](https://github.com/candy-chat/candy-plugins/tree/master/colors-xhtml).
![Color Picker](screenshot.png)
## Usage
To enable *Colors* you have to include its JavaScript code and stylesheet
```HTML
<script type="text/javascript" src="candyshop/colors/candy.js"></script>
<link rel="stylesheet" type="text/css" href="candyshop/colors/candy.css" />
```
Call its `init()` method after Candy has been initialized:
```JavaScript
Candy.init('/http-bind/');
// enable Colors plugin (default: 8 colors)
CandyShop.Colors.init();
Candy.Core.connect();
```
To enable less or more colors just call `CandyShop.Colors.init(<number-of-colors>)`.
#colors-control {
background: no-repeat url('colors-control.png');
position: relative;
}
#colors-control-indicator {
display: inline-block;
height: 6px;
width: 6px;
border: 1px solid white;
position: absolute;
top: 100%;
left: 100%;
margin: -8px 0 0 -8px;
}
#context-menu .colors {
padding-left: 5px;
width: 89px;
white-space: normal;
}
#context-menu .colors:hover {
background-color: inherit;
}
#context-menu .colors span {
display: inline-block;
width: 14px;
height: 14px;
border: 1px solid white;
margin: 3px;
}
.message-pane span.colored {
background-color: transparent !important;
}
.color-0 {
color: #333;
background-color: #333;
}
.color-1 {
color: #c4322b;
background-color: #c4322b;
}
.color-2 {
color: #37991e;
background-color: #37991e;
}
.color-3 {
color: #1654c9;
background-color: #1654c9;
}
.color-4 {
color: #66379b;
background-color: #66379b;
}
.color-5 {
color: #ba7318;
background-color: #ba7318;
}
.color-6 {
color: #32938a;
background-color: #32938a;
}
.color-7 {
color: #9e2274;
background-color: #9e2274;
}
.color-8 {
color: #4C82E4;
background-color: #4C82E4;
}
.color-9 {
color: #7F140E;
background-color: #7F140E;
}
.color-10 {
color: #1C630A;
background-color: #1C630A;
}
.color-11 {
color: #CF55A4;
background-color: #CF55A4;
}
\ No newline at end of file
var CandyShop = (function(self) { return self; }(CandyShop || {}));
CandyShop.Colors = (function(self, Candy, $) {
var _numColors,
_currentColor = 0;
self.init = function(numColors) {
_numColors = numColors ? numColors : 8;
self.applyTranslations();
$(Candy).on('candy:view.message.before-send', function(e, args) {
if(_currentColor > 0 && $.trim(args.message) !== '') {
args.message = '|c:'+ _currentColor +'|' + args.message;
}
});
$(Candy).on('candy:view.message.before-render', function(e, args) {
args.templateData.message = args.templateData.message.replace(/^\|c:([0-9]{1,2})\|(.*)/gm, '<span class="colored color-$1">$2</span>');
});
if(Candy.Util.cookieExists('candyshop-colors-current')) {
var color = parseInt(Candy.Util.getCookie('candyshop-colors-current'), 10);
if(color > 0 && color < _numColors) {
_currentColor = color;
}
}
var html = '<li id="colors-control" data-tooltip="' + $.i18n._('candyshopColorsMessagecolor') + '"><span class="color-' + _currentColor + '" id="colors-control-indicator"></span></li>';
$('#emoticons-icon').after(html);
$('#colors-control').click(function(event) {
CandyShop.Colors.showPicker(this);
});
};
self.showPicker = function(elem) {
elem = $(elem);
var pos = elem.offset(),
menu = $('#context-menu'),
content = $('ul', menu),
colors = '',
i;
$('#tooltip').hide();
for(i = _numColors-1; i >= 0; i--) {
colors = '<span class="color-' + i + '" data-color="' + i + '"></span>' + colors;
}
content.html('<li class="colors">' + colors + '</li>');
content.find('span').click(function() {
_currentColor = $(this).attr('data-color');
$('#colors-control-indicator').attr('class', 'color-' + _currentColor);
Candy.Util.setCookie('candyshop-colors-current', _currentColor, 365);
Candy.View.Pane.Room.setFocusToForm(Candy.View.getCurrent().roomJid);
menu.hide();
});
var posLeft = Candy.Util.getPosLeftAccordingToWindowBounds(menu, pos.left),
posTop = Candy.Util.getPosTopAccordingToWindowBounds(menu, pos.top);
menu.css({'left': posLeft.px, 'top': posTop.px, backgroundPosition: posLeft.backgroundPositionAlignment + ' ' + posTop.backgroundPositionAlignment});
menu.fadeIn('fast');
return true;
};
self.applyTranslations = function() {
var translations = {
'en' : 'Message Color',
'ru' : 'Цвет сообщения',
'de' : 'Farbe für Nachrichten',
'fr' : 'Couleur des messages',
'nl' : 'Berichtkleur',
'es' : 'Color de los mensajes'
};
$.each(translations, function(k, v) {
if(Candy.View.Translation[k]) {
Candy.View.Translation[k].candyshopColorsMessagecolor = v;
}
});
};
return self;
}(CandyShop.Colors || {}, Candy, jQuery));
/**
* Fastpath plugin for Candy
*
*/
var CandyShop = (function(self) { return self; }(CandyShop || {}));
CandyShop.Fastpath = (function(self, Candy, $) {
var connection = null;
var nickname = null;
self.init = function()
{
//console.log("Fastpath.init");
$(Candy).on('candy:core.chat.connection', function(obj, data)
{
switch(data.status)
{
case Strophe.Status.CONNECTED:
connection = Candy.Core.getConnection();
Candy.Core.addHandler(fastpathIqCallback, "http://jabber.org/protocol/workgroup", 'iq', 'set');
Candy.Core.addHandler(fastpathPresCallback, null, 'presence');
Candy.Core.addHandler(fastpathMsgCallback, null, 'message');
nickname = Strophe.escapeNode(Candy.Core.getUser().getNick());
GetWorkgroups();
break;
}
return true;
});
}
self.acceptOffer = function()
{
//console.log('accepted', window.properties);
connection.send($iq({type: 'set', to: window.properties.workgroupJid}).c('offer-accept', {xmlns: "http://jabber.org/protocol/workgroup", jid: window.properties.jid, id: window.properties.id}));
Candy.View.Pane.Chat.Modal.hide();
}
self.rejectOffer = function()
{
//console.log('rejected');
connection.send($iq({type: 'set', to: window.properties.workgroupJid}).c('offer-reject', {xmlns: "http://jabber.org/protocol/workgroup", jid: window.properties.jid, id: window.properties.id}));
Candy.View.Pane.Chat.Modal.hide();
}
var GetWorkgroups = function()
{
var iq = $iq({type: 'get', to: "workgroup." + connection.domain}).c('workgroups', {jid: connection.jid, xmlns: "http://jabber.org/protocol/workgroup"});
connection.sendIQ(iq, function(response)
{
$(response).find('workgroup').each(function()
{
var current = $(this);
var jid = current.attr('jid');
var name = Strophe.getNodeFromJid(jid);
var chatRoom = 'workgroup-' + name + "@conference." + connection.domain;
connection.send($pres({to: jid}).c('agent-status', {'xmlns': "http://jabber.org/protocol/workgroup"}));
Candy.Core.Action.Jabber.Room.Join(chatRoom);
connection.send($pres({to: jid}).c("status").t("Online").up().c("priority").t("1"));
connection.sendIQ($iq({type: 'get', to: jid}).c('agent-status-request', {xmlns: "http://jabber.org/protocol/workgroup"}));
});
});
};
var fastpathIqCallback = function(iq)
{
//console.log('fastpathIqCallback', iq);
var iq = $(iq);
var workgroupJid = iq.attr('from');
var workgroup = Strophe.getNodeFromJid(workgroupJid);
connection.send($iq({type: 'result', to: iq.attr('from'), id: iq.attr('id')}));
iq.find('offer').each(function()
{
var id = $(this).attr('id');
var jid = $(this).attr('jid').toLowerCase();
var properties = {id: id, jid: jid, workgroupJid: workgroupJid};
iq.find('value').each(function()
{
var name = $(this).attr('name');
var value = $(this).text();
properties[name] = value;
});
//console.log("fastpathIqCallback offer", properties, workgroup);
acceptRejectOffer(workgroup, properties);
});
iq.find('offer-revoke').each(function()
{
id = $(this).attr('id');
jid = $(this).attr('jid').toLowerCase();
//console.log("fastpathIqCallback offer-revoke", workgroup);
});
return true;
}
var fastpathPresCallback = function(presence)
{
//console.log('fastpathPresCallback', presence);
var presence = $(presence);
if (presence.find('agent-status').length > 0 || presence.find('notify-queue-details').length > 0 || presence.find('notify-queue').length > 0)
{
var from = Candy.Util.unescapeJid(presence.attr('from'));
var nick = Strophe.getNodeFromJid(from);
var workGroup, maxChats, free = true;
presence.find('agent-status').each(function()
{
workGroup = 'workgroup-' + Strophe.getNodeFromJid($(this).attr('jid')) + "@conference." + connection.domain;
presence.find('max-chats').each(function()
{
maxChats = $(this).text();
});
presence.find('chat').each(function()
{
free = false;
var sessionID = $(this).attr('sessionID');
var sessionJid = sessionID + "@conference." + connection.domain;
var sessionHash = (sessionJid);
var userID = $(this).attr('userID');
var startTime = $(this).attr('startTime');
var question = $(this).attr('question');
var username = $(this).attr('username');
var email = $(this).attr('email');
if (workGroup)
{
//console.log('agent-status message to ' + workGroup);
var text = "Talking with " + username + " about " + question;
Candy.View.Pane.Message.show(workGroup, nick, text);
}
});
});
presence.find('notify-queue-details').each(function()
{
var workGroup = 'workgroup-' + nick + "@conference." + connection.domain;
var free = true;
presence.find('user').each(function()
{
var jid = $(this).attr('jid');
var position, time, joinTime
$(this).find('position').each(function()
{
position = $(this).text() == "0" ? "first": jQuery(this).text();
});
$(this).find('time').each(function()
{
time = $(this).text();
});
$(this).find('join-time').each(function()
{
joinTime = $(this).text();
});
if (position && time && joinTime)
{
free = false;
//console.log('notify-queue-details message to ' + workGroup);
var text = "A caller has been waiting for " + time + " secconds";
Candy.View.Pane.Message.show(workGroup, nick, text);
}
});
});
presence.find('notify-queue').each(function()
{
var workGroup = 'workgroup-' + nick + "@conference." + connection.domain;
var free = true;
var count, oldest, waitTime, status
var room = Candy.View.Pane.Room.getPane(workGroup, '.message-pane')
presence.find('count').each(function()
{
count = jQuery(this).text();
});
presence.find('oldest').each(function()
{
oldest = jQuery(this).text();
});
presence.find('time').each(function()
{
waitTime = jQuery(this).text();
});
presence.find('status').each(function()
{
status = jQuery(this).text();
});
if (count && oldest && waitTime && status)
{
free = false;
//console.log('notify-queue message to ' + workGroup);
var text = "There are " + count + " caller(s) waiting for as long as " + waitTime + " seconds";
if (room) Candy.View.Pane.Message.show(workGroup, nick, text);
}
if (free && room) Candy.View.Pane.Message.show(workGroup, nick, "No waiting conversations");
});
}
return true;
}
var fastpathMsgCallback = function(message)
{
//console.log('fastpathMsgCallback', message);
var msg = $(message);
msg.find('invite').each(function()
{
var roomJid = msg.attr("from");
var workgroupJid = $(this).attr('from');
msg.find('offer').each(function()
{
var contactJid = $(this).attr('jid');
//console.log("fastpathMsgCallback offer", workgroupJid, contactJid, roomJid);
if (CandyShop.OfMeet)
{
CandyShop.OfMeet.showOfMeet(roomJid);
} else {
Candy.Core.Action.Jabber.Room.Join(roomJid);
}
});
});
return true;
}
var acceptRejectOffer = function(workgroup, properties)
{
var form = '<strong>' + workgroup + '</strong>' + '<form class="accept-reject-offer-form">'
var props = Object.getOwnPropertyNames(properties)
for (var i=0; i< props.length; i++)
{
if (props[i] != "id" && props[i] != "jid" && props[i] != "workgroupJid")
form = form + props[i] + " - " + properties[props[i]] + "<p/>";
}
window.properties = properties;
form = form + '<input onclick="CandyShop.Fastpath.acceptOffer()" type="button" value="Accept" />'
+ '<input onclick="CandyShop.Fastpath.rejectOffer()" type="button" value="Reject" />'
+ '</form>'
Candy.View.Pane.Chat.Modal.show(form, true);
}
return self;
}(CandyShop.Fastpath || {}, Candy, jQuery));
# 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
});
```
\ No newline at end of file
/*
* 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.after-show', self.handleOnShow);
}
};
/** 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: handleOnShow
* Descriptions
*
* Parameters:
* (Array) args
*
* @return void
*/
self.handleOnShow = 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));
# 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`
### 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: '-'
});
\ No newline at end of file
.candy-notifyme-highlight {
background: #FFFF00;
}
\ No newline at end of file
/** File: candy.js
* Candy - Chats are not dead yet.
*
* Authors:
* - Troy McCabe <troy.mccabe@geeksquad.com>
*
* Copyright:
* (c) 2012 Geek Squad. All rights reserved.
*/
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
*/
var _options = {
nameIdentifier: '@',
playSound: true,
highlightInRoom: true
};
/** 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);
// get the nick from the current user
var nick = Candy.Core.getUser().getNick();
// make it what is searched
// search for <identifier>name in the whole message
var searchTerm = _options.nameIdentifier + nick;
// bind to the beforeShow event
$(Candy).on('candy:view.message.before-show', function(e, args) {
var searchRegExp = new RegExp('^(.*)(\s?' + searchTerm + ')', '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 != nick) {
// 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 searchRegExp = new RegExp('^(.*)(\s?' + searchTerm + ')', '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.templateData.message) && args.templateData.name != nick) {
// highlight if specified
if (_options.highlightInRoom) {
args.templateData.message = args.templateData.message.replace(searchRegExp, '$1<span class="candy-notifyme-highlight">' + searchTerm + '</span>');
}
}
});
};
return self;
}(CandyShop.NotifyMe || {}, Candy, jQuery));
\ No newline at end of file
#video-modal {
background: #eee;
width: 1024px;
height: 768px;
padding: 20px 5px;
color: #333;
font-size: 16px;
position: fixed;
left: 0px;
top: 0px;
margin-left: 0px;
margin-top: 0px;
text-align: center;
display: none;
z-index: 100;
border: 5px solid #888;
border-radius: 5px;
box-shadow: 0 0 5px black;
}
#video-modal-overlay {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 90;
background-image: url(/ofmeet/candy/res/img/overlay.png);
}
#video-modal iframe {
width: 100%;
height: 100%;
}
#video-modal video-modal-body {
width: 100%;
height: 100%;
}
#video-modal .close {
position: absolute;
right: 0;
display: none;
padding: 0 5px;
margin: -17px 3px 0 0;
color: #999;
border-radius: 3px;
}
#video-modal .close:hover {
color: #333;
background-color: #aaa;
}
\ No newline at end of file
var CandyShop = (function(self) { return self; }(CandyShop || {}));
CandyShop.OfMeet = (function(self, Candy, $) {
self.init = function() {
var html = '<li id="ofmeet-control-icon" data-tooltip="Openfire Meetings"><img id="ofmeet-control" src="/ofmeet/candy/candyshop/ofmeet/webcam.png"></span></li>';
$('#emoticons-icon').after(html);
$('#ofmeet-control-icon').click(function(event)
{
var roomJid = Candy.View.getCurrent().roomJid;
Candy.Core.Action.Jabber.Room.Leave(roomJid);
self.showOfMeet(roomJid);
});
var html2 = '<div id="video-modal"><a id="video-modal-cancel" class="close" href="#">×</a><span id="video-modal-body"></span></div><div id="video-modal-overlay"></div>';
$(html2).appendTo("body");
$("#video-modal").css("height", window.innerHeight - 40);
$("#video-modal").css("width", window.innerWidth - 20);
$("#video-modal-overlay").hide();
$(window).resize(function ()
{
$("#video-modal").css("height", window.innerHeight - 40);
$("#video-modal").css("width", window.innerWidth - 20);
});
};
self.showOfMeet = function(roomJid)
{
$("#video-modal-cancel").show().click(function(e)
{
$("#video-modal").fadeOut("fast", function() {
$("#video-modal-body").text("");
$("#video-modal-overlay").hide();
});
e.preventDefault();
window.location.reload();
});
var room = Strophe.getNodeFromJid(roomJid);
$("#video-modal").stop(false, true);
$("#video-modal").fadeIn("fast");
$("#video-modal-body").html('<iframe id="ofmeet" src="/ofmeet/index.html?r=' + room + '"></iframe>');
$("#video-modal-overlay").show();
return true;
};
return self;
}(CandyShop.OfMeet || {}, Candy, jQuery));
Copyright (C) 2014 Mojo Lingo LLC
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.
# Slash Commands Plugin
A plugin to provide a command-line interface to Candy actions.
## Examples
To use any of the following, just type them into the chat input text area. Note that any commands which are room-specific (`/topic`, `/kick`, etc) will work on/for the current room only.
### Room Management
* `/join room [password]` - Joins the MUC room "room" with an optional password
* `/part` - Leaves the current MUC room
* `/clear` - Clears the scrollback in the current room
* `/topic This will be the new topic` - Sets the topic for the current room to "This will be the new topic"
### Presence
* `/available`
* `/away`
* `/dnd` - Do Not Disturb
## Todo
* `/kick username` - Ejects the user "username" from the current room. Must be a MUC admin for this to work
* `/invite username[@domain]` - Invites the user "username" to the current room. If the optional domain is not provided, it is assumed to be the same domain as the current user
## Configuration
For the commands that work on rooms (such as `/join`) you can specify the default domain to be suffixed to the room name:
```JavaScript
CandyShop.SlashCommands.defaultConferenceDomain = 'muc.example.com';
```
If unset, it defaults to the user's XMPP domain prefixed with "conference."
## Usage
Include the JavaScript file::
```HTML
<script type="text/javascript" src="candyshop/slash-commands/slash-commands.js"></script>
```
To enable the Slash Commands Plugin, just add one of the ´init´ methods to your bootstrap:
```JavaScript
CandyShop.SlashCommands.init();
```
/** File: candy.js
* Make several Candy actions accessible via the message box when prefixed with a slash "/"
*
* Authors:
* - Ben Klang <bklang@mojolingo.com>
*
* Contributors:
* - Troy McCabe <troy.mccabe@geeksquad.com>
* - Jonatan Männchen <jonatan.maennchen@amiadogroup.com>
*
* Copyright:
* - (c) 2014 Mojo Lingo LLC. All rights reserved.
*/
var CandyShop = (function(self) { return self; }(CandyShop || {}));
CandyShop.SlashCommands = (function(self, Candy, $) {
/** Object: about
* About SlashCommands plugin
*
* Contains:
* (String) name - Candy Plugin SlashCommands
* (Float) version - andy Plugin Available Rooms version
*/
self.about = {
name: 'Candy Plugin SlashCommands',
version: '0.1.0'
};
self.commands = [
'join',
'part',
'clear',
'topic',
'available',
'away',
'dnd',
];
self.defaultConferenceDomain = null;
/** Function: init
* Initializes the Slash Commands plugin with the default settings.
*/
self.init = function(){
$(Candy).on('candy:view.connection.status-5', function() {
// When connected to the server, default the conference domain if unspecified
if (!self.defaultConferenceDomain) {
self.defaultConferenceDomain = "@conference." + Candy.Core.getConnection().domain;
}
// Ensure we have a leading "@"
if (self.defaultConferenceDomain.indexOf('@') == -1) {
self.defaultConferenceDomain = "@" + self.defaultConferenceDomain;
}
});
$(Candy).bind('candy:view.message.before-send', function(e, args) {
try {
// (strip colors)
var input = args.message.replace(/\|c:\d+\|/, '');
if (input[0] == '/') {
var match = input.match(/^\/([^\s]+)(?:\s+(.*))?$/m);
if (match !== null) {
var command = match[1];
var data = match[2];
// Match only whitelisted commands
if ($.inArray(command, self.commands) != -1) {
self[command](data);
} else {
// TODO: Better way to notify the user of the invalid command
alert("Invalid command: " + command);
}
}
args.message = '';
}
} catch (ex) {
// Without an exception catcher, the page will reload and the user will be logged out
Candy.Core.log(ex);
}
});
};
/** Function: join
* Joins a room
*
* Parameters:
* (String) args The name of the room and the optional password, separated by a space
*/
self.join = function(args) {
args = args.split(' ');
var room = args[0];
var password = args[1];
if(typeof room != 'undefined' && room !== '') {
if(room.indexOf("@") == -1) {
room += self.defaultConferenceDomain;
}
if (typeof password !== 'undefined' && password !== '') {
Candy.Core.Action.Jabber.Room.Join(room, password);
} else {
Candy.Core.Action.Jabber.Room.Join(room);
}
}
};
/** Function: part
* Exits the current chat room
*
*/
self.part = function() {
Candy.Core.Action.Jabber.Room.Leave(self.currentRoom());
};
/** Function: topic
* Sets the topic (subject) for the current chat room
*
* Parameters:
* (String) topic The new topic for the room
*/
self.topic = function(topic) {
Candy.Core.Action.Jabber.Room.Admin.SetSubject(self.currentRoom(), topic);
};
/** Function: clear
* Clear the current room's scrollback
*/
self.clear = function() {
$('.room-pane:visible').find('.message-pane').empty();
};
/** Function: available
* Change the current user's XMPP status to "available" with an optional message
* Parameters:
* (String) message Optional message to set with availability
*/
self.available = function(message) {
// TODO: The message field is currently unsupported by Candy.Core.Action.Jabber.Presence
Candy.Core.Action.Jabber.Presence();
};
/** Function: away
* Change the current user's XMPP status to "away" with an optional message
* Parameters:
* (String) message Optional message to set with availability
*/
self.away = function(message) {
// TODO: The message field is currently unsupported by Candy.Core.Action.Jabber.Presence
Candy.Core.Action.Jabber.Presence(null, $build('show', 'away'));
};
/** Function: dnd
* Change the current user's XMPP status to "dnd" with an optional message
* Parameters:
* (String) message Optional message to set with availability
*/
self.dnd = function(message) {
// TODO: The message field is currently unsupported by Candy.Core.Action.Jabber.Presence
Candy.Core.Action.Jabber.Presence(null, $build('show', 'dnd'));
};
/** Function: currentRoom
* Helper function to get the current room
*/
self.currentRoom = function() {
return Candy.View.getCurrent().roomJid;
};
return self;
}(CandyShop.SlashCommands || {}, Candy, jQuery));
#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();
```
\ No newline at end of file
.message-pane li abbr {
border-bottom: none;
}
\ No newline at end of file
/*
* 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.
*/
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));
\ No newline at end of file
# 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();
```
/**
* 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;
}
/** 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));
<!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="/candy/res/default.css" />
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript" src="/candy/libs/libs.bundle.js"></script>
<script type="text/javascript" src="/candy/libs/strophe.openfire.js"></script>
<script type="text/javascript" src="/candy/candy.bundle.js"></script>
<script type="text/javascript">
$(document).ready(function() {
Candy.init('http://tlvdly02:7070/http-bind/', {
core: {
// only set this to true if developing / debugging errors
debug: false,
autojoin: ['fxdesk@conference.tlvdly02']
},
view: { assets: '/candy/res/' }
});
Candy.Core.connect("tlvdly02");
});
</script>
</head>
<body>
<div id="candy"></div>
</body>
</html>
AddDefaultCharset UTF-8
Options +MultiViews
RewriteEngine On
RewriteRule http-bind/ http://localhost:5280/http-bind/ [P]
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment