Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/candy-chat/candy.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMichael Weibel <michael.weibel@gmail.com>2015-07-10 08:40:48 +0300
committerMichael Weibel <michael.weibel@gmail.com>2015-07-10 08:40:48 +0300
commitcd048fd58fc48e02c7192885b6b81804ef1f7850 (patch)
tree97a3420016396c07f38127ac7b9b47fcd09a873c /src
parenta9dcafe1a1995b61755986b56777fb2f44c832b2 (diff)
parent7e439479ccce1495ab51ed9c87d191ede195e725 (diff)
Merge branch 'dev'v2.0.0
Update version to 2.0.0
Diffstat (limited to 'src')
-rw-r--r--src/candy.js2
-rw-r--r--src/core.js139
-rw-r--r--src/core/action.js39
-rw-r--r--src/core/chatRoom.js126
-rw-r--r--src/core/chatRoster.js78
-rw-r--r--src/core/chatUser.js466
-rw-r--r--src/core/contact.js152
-rw-r--r--src/core/event.js697
-rw-r--r--src/util.js45
-rw-r--r--src/view.js7
-rw-r--r--src/view/observer.js26
-rw-r--r--src/view/pane.js2094
-rw-r--r--src/view/pane/chat.js1064
-rw-r--r--src/view/pane/message.js253
-rw-r--r--src/view/pane/privateRoom.js181
-rw-r--r--src/view/pane/room.js484
-rw-r--r--src/view/pane/roster.js295
-rw-r--r--src/view/pane/window.js117
-rw-r--r--src/view/template.js30
-rw-r--r--src/view/translation.js228
20 files changed, 3749 insertions, 2774 deletions
diff --git a/src/candy.js b/src/candy.js
index aacbcf4..2ec7011 100644
--- a/src/candy.js
+++ b/src/candy.js
@@ -30,7 +30,7 @@ var Candy = (function(self, $) {
*/
self.about = {
name: 'Candy',
- version: '1.7.1'
+ version: '2.0.0'
};
/** Function: init
diff --git a/src/core.js b/src/core.js
index 54407f2..2296205 100644
--- a/src/core.js
+++ b/src/core.js
@@ -34,6 +34,10 @@ Candy.Core = (function(self, Strophe, $) {
* Current user (me)
*/
_user = null,
+ /** PrivateVariable: _roster
+ * Main roster of contacts
+ */
+ _roster = null,
/** PrivateVariable: _rooms
* Opened rooms, containing instances of Candy.Core.ChatRooms
*/
@@ -47,9 +51,7 @@ Candy.Core = (function(self, Strophe, $) {
*/
_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.
+ * Config options
*/
_options = {
/** Boolean: autojoin
@@ -57,7 +59,46 @@ Candy.Core = (function(self, Strophe, $) {
* You may want to define an array of rooms to autojoin: `['room1@conference.host.tld', 'room2...]` (ejabberd, Openfire, ...)
*/
autojoin: undefined,
+ /** Boolean: disconnectWithoutTabs
+ * If you set to `false`, when you close all of the tabs, the service does not disconnect.
+ * Set to `true`, when you close all of the tabs, the service will disconnect.
+ */
+ disconnectWithoutTabs: true,
+ /** String: conferenceDomain
+ * Holds the prefix for an XMPP chat server's conference subdomain.
+ * If not set, assumes no specific subdomain.
+ */
+ conferenceDomain: undefined,
+ /** Boolean: debug
+ * Enable debug
+ */
debug: false,
+ /** List: domains
+ * If non-null, causes login form to offer this
+ * pre-set list of domains to choose between when
+ * logging in. Any user-provided domain is discarded
+ * and the selected domain is appended.
+ * For each list item, only characters up to the first
+ * whitespace are used, so you can append extra
+ * information to each item if desired.
+ */
+ domains: null,
+ /** Boolean: hideDomainList
+ * If true, the domain list defined above is suppressed.
+ * Without a selector displayed, the default domain
+ * (usually the first one listed) will be used as
+ * described above. Probably only makes sense with a
+ * single domain defined.
+ */
+ hideDomainList: false,
+ /** Boolean: disableCoreNotifications
+ * If set to `true`, the built-in notifications (sounds and badges) are disabled.
+ * This is useful if you are using a plugin to handle notifications.
+ */
+ disableCoreNotifications: false,
+ /** Boolean: disableWindowUnload
+ * Disable window unload handler which usually disconnects from XMPP
+ */
disableWindowUnload: false,
/** Integer: presencePriority
* Default priority for presence messages in order to receive messages across different resources
@@ -67,7 +108,20 @@ Candy.Core = (function(self, Strophe, $) {
* JID resource to use when connecting to the server.
* Specify `''` (an empty string) to request a random resource.
*/
- resource: Candy.about.name
+ resource: Candy.about.name,
+ /** Boolean: useParticipantRealJid
+ * If set true, will direct one-on-one chats to participant's real JID rather than their MUC jid
+ */
+ useParticipantRealJid: false,
+ /**
+ * Roster version we claim to already have. Used when loading a cached roster.
+ * Defaults to null, indicating we don't have the roster.
+ */
+ initialRosterVersion: null,
+ /**
+ * Initial roster items. Loaded from a cache, used to bootstrap displaying a roster prior to fetching updates.
+ */
+ initialRosterItems: []
},
/** PrivateFunction: _addNamespace
@@ -88,8 +142,10 @@ Candy.Core = (function(self, Strophe, $) {
_addNamespace('PRIVATE', 'jabber:iq:private');
_addNamespace('BOOKMARKS', 'storage:bookmarks');
_addNamespace('PRIVACY', 'jabber:iq:privacy');
- _addNamespace('DELAY', 'jabber:x:delay');
+ _addNamespace('DELAY', 'urn:xmpp:delay');
+ _addNamespace('JABBER_DELAY', 'jabber:x:delay');
_addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub');
+ _addNamespace('CARBONS', 'urn:xmpp:carbons:2');
},
_getEscapedJidFromJid = function(jid) {
@@ -122,11 +178,39 @@ Candy.Core = (function(self, Strophe, $) {
};
}
}
+ Strophe.log = function (level, message) {
+ var level_name, console_level;
+ switch (level) {
+ case Strophe.LogLevel.DEBUG:
+ level_name = 'DEBUG';
+ console_level = 'log';
+ break;
+ case Strophe.LogLevel.INFO:
+ level_name = 'INFO';
+ console_level = 'info';
+ break;
+ case Strophe.LogLevel.WARN:
+ level_name = 'WARN';
+ console_level = 'info';
+ break;
+ case Strophe.LogLevel.ERROR:
+ level_name = 'ERROR';
+ console_level = 'error';
+ break;
+ case Strophe.LogLevel.FATAL:
+ level_name = 'FATAL';
+ console_level = 'error';
+ break;
+ }
+ console[console_level]('[Strophe][' + level_name + ']: ' + message);
+ };
self.log('[Init] Debugging enabled');
}
_addNamespaces();
+ _roster = new Candy.Core.ChatRoster();
+
// Connect to BOSH/Websocket service
_connection = new Strophe.Connection(_service);
_connection.rawInput = self.rawInput.bind(self);
@@ -200,6 +284,12 @@ Candy.Core = (function(self, Strophe, $) {
_anonymousConnection = !_anonymousConnection ? jidOrHost && jidOrHost.indexOf("@") < 0 : true;
if(jidOrHost && password) {
+ // Respect the resource, if provided
+ var resource = Strophe.getResourceFromJid(jidOrHost);
+ if (resource) {
+ _options.resource = resource;
+ }
+
// authentication
_connection.connect(_getEscapedJidFromJid(jidOrHost) + '/' + _options.resource, password, Candy.Core.Event.Strophe.Connect);
if (nick) {
@@ -229,8 +319,14 @@ Candy.Core = (function(self, Strophe, $) {
* (Integer) sid - Session ID
* (Integer) rid - rid
*/
- self.attach = function(jid, sid, rid) {
- _user = new self.ChatUser(jid, Strophe.getNodeFromJid(jid));
+ self.attach = function(jid, sid, rid, nick) {
+ if (nick) {
+ _user = new self.ChatUser(jid, nick);
+ } else {
+ _user = new self.ChatUser(jid, Strophe.getNodeFromJid(jid));
+ }
+ // Reset before every connection attempt to make sure reconnections work after authfail, alltabsclosed, ...
+ _connection.reset();
self.registerEventHandlers();
_connection.attach(jid, sid, rid, Candy.Core.Event.Strophe.Connect);
};
@@ -240,9 +336,6 @@ Candy.Core = (function(self, Strophe, $) {
*/
self.disconnect = function() {
if(_connection.connected) {
- $.each(self.getRooms(), function() {
- Candy.Core.Action.Jabber.Room.Leave(this.getJid());
- });
_connection.disconnect();
}
};
@@ -266,6 +359,16 @@ Candy.Core = (function(self, Strophe, $) {
return _connection.addHandler(handler, ns, name, type, id, from, options);
};
+ /** Function: getRoster
+ * Gets main roster
+ *
+ * Returns:
+ * Instance of Candy.Core.ChatRoster
+ */
+ self.getRoster = function() {
+ return _roster;
+ };
+
/** Function: getUser
* Gets current user
*
@@ -411,5 +514,21 @@ Candy.Core = (function(self, Strophe, $) {
*/
self.log = function() {};
+ /** Function: warn
+ * Print a message to the browser's "info" log
+ * Enabled regardless of debug mode
+ */
+ self.warn = function() {
+ Function.prototype.apply.call(console.warn, console, arguments);
+ };
+
+ /** Function: error
+ * Print a message to the browser's "error" log
+ * Enabled regardless of debug mode
+ */
+ self.error = function() {
+ Function.prototype.apply.call(console.error, console, arguments);
+ };
+
return self;
}(Candy.Core || {}, Strophe, jQuery));
diff --git a/src/core/action.js b/src/core/action.js
index 2680934..b72a946 100644
--- a/src/core/action.js
+++ b/src/core/action.js
@@ -39,10 +39,11 @@ Candy.Core.Action = (function(self, Strophe, $) {
from: Candy.Util.escapeJid(msg.attr('to')),
id: msg.attr('id')
}).c('query', {
- name: Candy.about.name,
- version: Candy.about.version,
- os: navigator.userAgent
- }));
+ xmlns: Strophe.NS.VERSION
+ })
+ .c('name', Candy.about.name).up()
+ .c('version', Candy.about.version).up()
+ .c('os', navigator.userAgent));
},
/** Function: SetNickname
@@ -72,10 +73,20 @@ Candy.Core.Action = (function(self, Strophe, $) {
* 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());
+ var roster = Candy.Core.getConnection().roster,
+ options = Candy.Core.getOptions();
+ roster.registerCallback(Candy.Core.Event.Jabber.RosterPush);
+ $.each(options.initialRosterItems, function (i, item) {
+ // Blank out resources because their cached value is not relevant
+ item.resources = {};
+ });
+ roster.get(
+ Candy.Core.Event.Jabber.RosterFetch,
+ options.initialRosterVersion,
+ options.initialRosterItems
+ );
+ // Bootstrap our roster with cached items
+ Candy.Core.Event.Jabber.RosterLoad(roster.items);
},
/** Function: Presence
@@ -153,6 +164,17 @@ Candy.Core.Action = (function(self, Strophe, $) {
}
},
+ /** Function: EnableCarbons
+ * Enable message carbons (XEP-0280)
+ */
+ EnableCarbons: function() {
+ Candy.Core.getConnection().sendIQ($iq({
+ type: 'set'
+ })
+ .c('enable', {xmlns: Strophe.NS.CARBONS })
+ .tree());
+ },
+
/** Function: ResetIgnoreList
* Create new ignore privacy list (and reset the previous one, if it exists).
*/
@@ -253,7 +275,6 @@ Candy.Core.Action = (function(self, Strophe, $) {
*/
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() {});
}
diff --git a/src/core/chatRoom.js b/src/core/chatRoom.js
index ad37b4b..2d842bd 100644
--- a/src/core/chatRoom.js
+++ b/src/core/chatRoom.js
@@ -37,74 +37,74 @@ Candy.Core.ChatRoom = function(roomJid) {
* 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: setUser
+ * Set user of this room.
+ *
+ * Parameters:
+ * (Candy.Core.ChatUser) user - Chat user
+ */
+Candy.Core.ChatRoom.prototype.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: getUser
+ * Get current local user
+ *
+ * Returns:
+ * (Object) - Candy.Core.ChatUser instance or null
+ */
+Candy.Core.ChatRoom.prototype.getUser = function() {
+ return this.user;
+};
- /** Function: getJid
- * Get room jid
- *
- * Returns:
- * (String) - Room jid
- */
- this.getJid = function() {
- return this.room.jid;
- };
+/** Function: getJid
+ * Get room jid
+ *
+ * Returns:
+ * (String) - Room jid
+ */
+Candy.Core.ChatRoom.prototype.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: setName
+ * Set room name
+ *
+ * Parameters:
+ * (String) name - Room name
+ */
+Candy.Core.ChatRoom.prototype.setName = function(name) {
+ this.room.name = name;
+};
- /** Function: getName
- * Get room name
- *
- * Returns:
- * (String) - Room name
- */
- this.getName = function() {
- return this.room.name;
- };
+/** Function: getName
+ * Get room name
+ *
+ * Returns:
+ * (String) - Room name
+ */
+Candy.Core.ChatRoom.prototype.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: setRoster
+ * Set roster of room
+ *
+ * Parameters:
+ * (Candy.Core.ChatRoster) roster - Chat roster
+ */
+Candy.Core.ChatRoom.prototype.setRoster = function(roster) {
+ this.roster = roster;
+};
- /** Function: getRoster
- * Get roster
- *
- * Returns
- * (Candy.Core.ChatRoster) - instance
- */
- this.getRoster = function() {
- return this.roster;
- };
+/** Function: getRoster
+ * Get roster
+ *
+ * Returns
+ * (Candy.Core.ChatRoster) - instance
+ */
+Candy.Core.ChatRoom.prototype.getRoster = function() {
+ return this.roster;
};
diff --git a/src/core/chatRoster.js b/src/core/chatRoster.js
index 6e7546e..777ef2f 100644
--- a/src/core/chatRoster.js
+++ b/src/core/chatRoster.js
@@ -21,47 +21,47 @@ Candy.Core.ChatRoster = function () {
* 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: add
+ * Add user to roster
+ *
+ * Parameters:
+ * (Candy.Core.ChatUser) user - User to add
+ */
+Candy.Core.ChatRoster.prototype.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: remove
+ * Remove user from roster
+ *
+ * Parameters:
+ * (String) jid - User jid
+ */
+Candy.Core.ChatRoster.prototype.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: get
+ * Get user from roster
+ *
+ * Parameters:
+ * (String) jid - User jid
+ *
+ * Returns:
+ * (Candy.Core.ChatUser) - User
+ */
+Candy.Core.ChatRoster.prototype.get = function(jid) {
+ return this.items[jid];
+};
- /** Function: getAll
- * Get all items
- *
- * Returns:
- * (Object) - all roster items
- */
- this.getAll = function() {
- return this.items;
- };
+/** Function: getAll
+ * Get all items
+ *
+ * Returns:
+ * (Object) - all roster items
+ */
+Candy.Core.ChatRoster.prototype.getAll = function() {
+ return this.items;
};
diff --git a/src/core/chatUser.js b/src/core/chatUser.js
index aecc6c7..5067afb 100644
--- a/src/core/chatUser.js
+++ b/src/core/chatUser.js
@@ -16,7 +16,7 @@
/** Class: Candy.Core.ChatUser
* Chat User
*/
-Candy.Core.ChatUser = function(jid, nick, affiliation, role) {
+Candy.Core.ChatUser = function(jid, nick, affiliation, role, realJid) {
/** Constant: ROLE_MODERATOR
* Moderator role
*/
@@ -30,6 +30,7 @@ Candy.Core.ChatUser = function(jid, nick, affiliation, role) {
/** Object: data
* User data containing:
* - jid
+ * - realJid
* - nick
* - affiliation
* - role
@@ -38,228 +39,291 @@ Candy.Core.ChatUser = function(jid, nick, affiliation, role) {
*/
this.data = {
jid: jid,
+ realJid: realJid,
nick: Strophe.unescapeNode(nick),
affiliation: affiliation,
role: role,
privacyLists: {},
customData: {},
- previousNick: undefined
+ previousNick: undefined,
+ status: 'unavailable'
};
+};
- /** 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: getJid
+ * Gets an unescaped user jid
+ *
+ * See:
+ * <Candy.Util.unescapeJid>
+ *
+ * Returns:
+ * (String) - jid
+ */
+Candy.Core.ChatUser.prototype.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: getEscapedJid
+ * Escapes the user's jid (node & resource get escaped)
+ *
+ * See:
+ * <Candy.Util.escapeJid>
+ *
+ * Returns:
+ * (String) - escaped jid
+ */
+Candy.Core.ChatUser.prototype.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: setJid
+ * Sets a user's jid
+ *
+ * Parameters:
+ * (String) jid - New Jid
+ */
+Candy.Core.ChatUser.prototype.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: getRealJid
+ * Gets an unescaped real jid if known
+ *
+ * See:
+ * <Candy.Util.unescapeJid>
+ *
+ * Returns:
+ * (String) - realJid
+ */
+Candy.Core.ChatUser.prototype.getRealJid = function() {
+ if(this.data.realJid) {
+ return Candy.Util.unescapeJid(this.data.realJid);
+ }
+ return;
+};
- /** Function: setNick
- * Sets a user's nick
- *
- * Parameters:
- * (String) nick - New nick
- */
- this.setNick = function(nick) {
- this.data.nick = nick;
- };
+/** Function: getNick
+ * Gets user nick
+ *
+ * Returns:
+ * (String) - nick
+ */
+Candy.Core.ChatUser.prototype.getNick = function() {
+ return Strophe.unescapeNode(this.data.nick);
+};
- /** Function: getRole
- * Gets user role
- *
- * Returns:
- * (String) - role
- */
- this.getRole = function() {
- return this.data.role;
- };
+/** Function: setNick
+ * Sets a user's nick
+ *
+ * Parameters:
+ * (String) nick - New nick
+ */
+Candy.Core.ChatUser.prototype.setNick = function(nick) {
+ this.data.nick = nick;
+};
- /** Function: setRole
- * Sets user role
- *
- * Parameters:
- * (String) role - Role
- */
- this.setRole = function(role) {
- this.data.role = role;
- };
+/** Function: getName
+ * Gets user's name (from contact or nick)
+ *
+ * Returns:
+ * (String) - name
+ */
+Candy.Core.ChatUser.prototype.getName = function() {
+ var contact = this.getContact();
+ if (contact) {
+ return contact.getName();
+ } else {
+ return this.getNick();
+ }
+};
- /** Function: setAffiliation
- * Sets user affiliation
- *
- * Parameters:
- * (String) affiliation - new affiliation
- */
- this.setAffiliation = function(affiliation) {
- this.data.affiliation = affiliation;
- };
+/** Function: getRole
+ * Gets user role
+ *
+ * Returns:
+ * (String) - role
+ */
+Candy.Core.ChatUser.prototype.getRole = function() {
+ return this.data.role;
+};
- /** Function: getAffiliation
- * Gets user affiliation
- *
- * Returns:
- * (String) - affiliation
- */
- this.getAffiliation = function() {
- return this.data.affiliation;
- };
+/** Function: setRole
+ * Sets user role
+ *
+ * Parameters:
+ * (String) role - Role
+ */
+Candy.Core.ChatUser.prototype.setRole = function(role) {
+ this.data.role = role;
+};
- /** 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: setAffiliation
+ * Sets user affiliation
+ *
+ * Parameters:
+ * (String) affiliation - new affiliation
+ */
+Candy.Core.ChatUser.prototype.setAffiliation = function(affiliation) {
+ this.data.affiliation = affiliation;
+};
- /** 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: getAffiliation
+ * Gets user affiliation
+ *
+ * Returns:
+ * (String) - affiliation
+ */
+Candy.Core.ChatUser.prototype.getAffiliation = function() {
+ return this.data.affiliation;
+};
- /** 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: isModerator
+ * Check if user is moderator. Depends on the room.
+ *
+ * Returns:
+ * (Boolean) - true if user has role moderator or affiliation owner
+ */
+Candy.Core.ChatUser.prototype.isModerator = function() {
+ return this.getRole() === this.ROLE_MODERATOR || this.getAffiliation() === this.AFFILIATION_OWNER;
+};
- /** Function: setPrivacyLists
- * Sets privacy lists.
- *
- * Parameters:
- * (Object) lists - List object
- */
- this.setPrivacyLists = function(lists) {
- this.data.privacyLists = lists;
- };
+/** 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.
+ */
+Candy.Core.ChatUser.prototype.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: 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: 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
+ */
+Candy.Core.ChatUser.prototype.getPrivacyList = function(list) {
+ if (!this.data.privacyLists[list]) {
+ this.data.privacyLists[list] = [];
+ }
+ return this.data.privacyLists[list];
+};
- /** Function: setCustomData
- * Stores custom data
- *
- * Parameter:
- * (Object) data - Object containing custom data
- */
- this.setCustomData = function(data) {
- this.data.customData = data;
- };
+/** Function: setPrivacyLists
+ * Sets privacy lists.
+ *
+ * Parameters:
+ * (Object) lists - List object
+ */
+Candy.Core.ChatUser.prototype.setPrivacyLists = function(lists) {
+ this.data.privacyLists = lists;
+};
- /** Function: getCustomData
- * Retrieve custom data
- *
- * Returns:
- * (Object) - Object containing custom data
- */
- this.getCustomData = function() {
- return this.data.customData;
- };
+/** 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)
+ */
+Candy.Core.ChatUser.prototype.isInPrivacyList = function(list, jid) {
+ if (!this.data.privacyLists[list]) {
+ return false;
+ }
+ return this.data.privacyLists[list].indexOf(jid) !== -1;
+};
- /** 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: setCustomData
+ * Stores custom data
+ *
+ * Parameter:
+ * (Object) data - Object containing custom data
+ */
+Candy.Core.ChatUser.prototype.setCustomData = function(data) {
+ this.data.customData = data;
+};
- /** Function: hasNicknameChanged
- * Gets the previous nickname if available.
- *
- * Returns:
- * (String) - previous nickname
- */
- this.getPreviousNick = function() {
- return this.data.previousNick;
- };
+/** Function: getCustomData
+ * Retrieve custom data
+ *
+ * Returns:
+ * (Object) - Object containing custom data
+ */
+Candy.Core.ChatUser.prototype.getCustomData = function() {
+ return this.data.customData;
+};
+
+/** Function: setPreviousNick
+ * If user has nickname changed, set previous nickname.
+ *
+ * Parameters:
+ * (String) previousNick - the previous nickname
+ */
+Candy.Core.ChatUser.prototype.setPreviousNick = function(previousNick) {
+ this.data.previousNick = previousNick;
+};
+
+/** Function: hasNicknameChanged
+ * Gets the previous nickname if available.
+ *
+ * Returns:
+ * (String) - previous nickname
+ */
+Candy.Core.ChatUser.prototype.getPreviousNick = function() {
+ return this.data.previousNick;
+};
+
+/** Function: getContact
+ * Gets the contact matching this user from our roster
+ *
+ * Returns:
+ * (Candy.Core.Contact) - contact from roster
+ */
+Candy.Core.ChatUser.prototype.getContact = function() {
+ return Candy.Core.getRoster().get(Strophe.getBareJidFromJid(this.data.realJid));
+};
+
+/** Function: setStatus
+ * Set the user's status
+ *
+ * Parameters:
+ * (String) status - the new status
+ */
+Candy.Core.ChatUser.prototype.setStatus = function(status) {
+ this.data.status = status;
+};
+
+/** Function: getStatus
+ * Gets the user's status.
+ *
+ * Returns:
+ * (String) - status
+ */
+Candy.Core.ChatUser.prototype.getStatus = function() {
+ return this.data.status;
};
diff --git a/src/core/contact.js b/src/core/contact.js
new file mode 100644
index 0000000..04c3cf5
--- /dev/null
+++ b/src/core/contact.js
@@ -0,0 +1,152 @@
+/** File: contact.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.Contact
+ * Roster contact
+ */
+Candy.Core.Contact = function(stropheRosterItem) {
+ /** Object: data
+ * Strophe Roster plugin item model containing:
+ * - jid
+ * - name
+ * - subscription
+ * - groups
+ */
+ this.data = stropheRosterItem;
+};
+
+/** Function: getJid
+ * Gets an unescaped user jid
+ *
+ * See:
+ * <Candy.Util.unescapeJid>
+ *
+ * Returns:
+ * (String) - jid
+ */
+Candy.Core.Contact.prototype.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
+ */
+Candy.Core.Contact.prototype.getEscapedJid = function() {
+ return Candy.Util.escapeJid(this.data.jid);
+};
+
+/** Function: getName
+ * Gets user name
+ *
+ * Returns:
+ * (String) - name
+ */
+Candy.Core.Contact.prototype.getName = function() {
+ if (!this.data.name) {
+ return this.getJid();
+ }
+ return Strophe.unescapeNode(this.data.name);
+};
+
+/** Function: getNick
+ * Gets user name
+ *
+ * Returns:
+ * (String) - name
+ */
+Candy.Core.Contact.prototype.getNick = Candy.Core.Contact.prototype.getName;
+
+/** Function: getSubscription
+ * Gets user subscription
+ *
+ * Returns:
+ * (String) - subscription
+ */
+Candy.Core.Contact.prototype.getSubscription = function() {
+ if (!this.data.subscription) {
+ return 'none';
+ }
+ return this.data.subscription;
+};
+
+/** Function: getGroups
+ * Gets user groups
+ *
+ * Returns:
+ * (Array) - groups
+ */
+Candy.Core.Contact.prototype.getGroups = function() {
+ return this.data.groups;
+};
+
+/** Function: getStatus
+ * Gets user status as an aggregate of all resources
+ *
+ * Returns:
+ * (String) - aggregate status, one of chat|dnd|available|away|xa|unavailable
+ */
+Candy.Core.Contact.prototype.getStatus = function() {
+ var status = 'unavailable',
+ self = this,
+ highestResourcePriority;
+
+ $.each(this.data.resources, function(resource, obj) {
+ var resourcePriority = parseInt(obj.priority, 10);
+
+ if (obj.show === '' || obj.show === null || obj.show === undefined) {
+ // TODO: Submit this as a bugfix to strophejs-plugins' roster plugin
+ obj.show = 'available';
+ }
+
+ if (highestResourcePriority === undefined || highestResourcePriority < resourcePriority) {
+ // This resource is higher priority than the ones we've checked so far, override with this one
+ status = obj.show;
+ highestResourcePriority = resourcePriority;
+ } else if (highestResourcePriority === resourcePriority) {
+ // Two resources with the same priority means we have to weight their status
+ if (self._weightForStatus(status) > self._weightForStatus(obj.show)) {
+ status = obj.show;
+ }
+ }
+ });
+
+ return status;
+};
+
+Candy.Core.Contact.prototype._weightForStatus = function(status) {
+ switch (status) {
+ case 'chat':
+ case 'dnd':
+ return 1;
+ case 'available':
+ case '':
+ return 2;
+ case 'away':
+ return 3;
+ case 'xa':
+ return 4;
+ case 'unavailable':
+ return 5;
+ }
+};
diff --git a/src/core/event.js b/src/core/event.js
index e9cfe26..48e18b0 100644
--- a/src/core/event.js
+++ b/src/core/event.js
@@ -63,7 +63,11 @@ Candy.Core.Event = (function(self, Strophe, $) {
/* falls through */
case Strophe.Status.ATTACHED:
Candy.Core.log('[Connection] Attached');
- Candy.Core.Action.Jabber.Presence();
+ $(Candy).on('candy:core:roster:fetched', function () {
+ Candy.Core.Action.Jabber.Presence();
+ });
+ Candy.Core.Action.Jabber.Roster();
+ Candy.Core.Action.Jabber.EnableCarbons();
Candy.Core.Action.Jabber.Autojoin();
Candy.Core.Action.Jabber.GetIgnoreList();
break;
@@ -94,7 +98,7 @@ Candy.Core.Event = (function(self, Strophe, $) {
break;
default:
- Candy.Core.log('[Connection] What?!');
+ Candy.Core.warn('[Connection] Unknown status received:', status);
break;
}
/** Event: candy:core.chat.connection
@@ -160,6 +164,118 @@ Candy.Core.Event = (function(self, Strophe, $) {
return true;
},
+ /** Function: RosterLoad
+ * Acts on the result of loading roster items from a cache
+ *
+ * Parameters:
+ * (String) items - List of roster items
+ *
+ * Triggers:
+ * candy:core.roster.loaded
+ *
+ * Returns:
+ * (Boolean) - true
+ */
+ RosterLoad: function(items) {
+ self.Jabber._addRosterItems(items);
+
+ /** Event: candy:core.roster.loaded
+ * Notification of the roster having been loaded from cache
+ */
+ $(Candy).triggerHandler('candy:core:roster:loaded', {roster: Candy.Core.getRoster()});
+
+ return true;
+ },
+
+ /** Function: RosterFetch
+ * Acts on the result of a roster fetch
+ *
+ * Parameters:
+ * (String) items - List of roster items
+ *
+ * Triggers:
+ * candy:core.roster.fetched
+ *
+ * Returns:
+ * (Boolean) - true
+ */
+ RosterFetch: function(items) {
+ self.Jabber._addRosterItems(items);
+
+ /** Event: candy:core.roster.fetched
+ * Notification of the roster having been fetched
+ */
+ $(Candy).triggerHandler('candy:core:roster:fetched', {roster: Candy.Core.getRoster()});
+
+ return true;
+ },
+
+ /** Function: RosterPush
+ * Acts on a roster push
+ *
+ * Parameters:
+ * (String) stanza - Raw XML Message
+ *
+ * Triggers:
+ * candy:core.roster.added
+ * candy:core.roster.updated
+ * candy:core.roster.removed
+ *
+ * Returns:
+ * (Boolean) - true
+ */
+ RosterPush: function(items, updatedItem) {
+ if (!updatedItem) {
+ return true;
+ }
+
+ if (updatedItem.subscription === "remove") {
+ var contact = Candy.Core.getRoster().get(updatedItem.jid);
+ Candy.Core.getRoster().remove(updatedItem.jid);
+ /** Event: candy:core.roster.removed
+ * Notification of a roster entry having been removed
+ *
+ * Parameters:
+ * (Candy.Core.Contact) contact - The contact that was removed from the roster
+ */
+ $(Candy).triggerHandler('candy:core:roster:removed', {contact: contact});
+ } else {
+ var user = Candy.Core.getRoster().get(updatedItem.jid);
+ if (!user) {
+ user = self.Jabber._addRosterItem(updatedItem);
+ /** Event: candy:core.roster.added
+ * Notification of a roster entry having been added
+ *
+ * Parameters:
+ * (Candy.Core.Contact) contact - The contact that was added
+ */
+ $(Candy).triggerHandler('candy:core:roster:added', {contact: user});
+ } else {
+ /** Event: candy:core.roster.updated
+ * Notification of a roster entry having been updated
+ *
+ * Parameters:
+ * (Candy.Core.Contact) contact - The contact that was updated
+ */
+ $(Candy).triggerHandler('candy:core:roster:updated', {contact: user});
+ }
+ }
+
+ return true;
+ },
+
+ _addRosterItem: function(item) {
+ var user = new Candy.Core.Contact(item);
+ Candy.Core.getRoster().add(user);
+ return user;
+ },
+
+ _addRosterItems: function(items) {
+ $.each(items, function(i, item) {
+ self.Jabber._addRosterItem(item);
+ });
+ },
+
/** Function: Bookmarks
* Acts on a bookmarks event. When a bookmark has the attribute autojoin set, joins this room.
*
@@ -247,205 +363,142 @@ Candy.Core.Event = (function(self, Strophe, $) {
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();
+ var type = msg.attr('type') || 'normal';
+
+ switch (type) {
+ case 'normal':
+ var invite = self.Jabber._findInvite(msg);
+
+ if (invite) {
+ /** 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
+ * (String) password - Password for the room
+ * (String) continuedThread - The thread ID if this is a continuation of a 1-on-1 chat
+ */
+ $(Candy).triggerHandler('candy:core:chat:invite', invite);
}
- if(continueNode) {
- continuedThread = continueNode.attr('thread');
- }
-
- /** Event: candy:core:chat:invite
- * Incoming chat invite for a MUC.
+ /** 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) 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]
+ * (String) type - Type of the message
+ * (Object) message - Message object.
*/
- $(Candy).triggerHandler('candy:core:chat:invite', {
- roomJid: fromJid,
- from: mediatedInvite.attr('from') || 'undefined',
- reason: mediatedInvite.find('reason').html() || '',
- password: password,
- continuedThread: continuedThread
+ $(Candy).triggerHandler('candy:core:chat:message:normal', {
+ type: type,
+ message: msg
});
- }
-
- if(directInvite.length > 0) {
- /** Event: candy:core:chat:invite
- * Incoming chat invite for a MUC.
+ break;
+ case 'headline':
+ // Admin message
+ if(!msg.attr('to')) {
+ /** Event: candy:core.chat.message.admin
+ * Admin message
+ *
+ * Parameters:
+ * (String) type - Type of the message
+ * (String) message - Message text
+ */
+ $(Candy).triggerHandler('candy:core.chat.message.admin', {
+ type: type,
+ message: msg.children('body').text()
+ });
+ // Server Message
+ } else {
+ /** Event: candy:core.chat.message.server
+ * Server message (e.g. subject)
+ *
+ * Parameters:
+ * (String) type - Message type
+ * (String) subject - Subject text
+ * (String) message - Message text
+ */
+ $(Candy).triggerHandler('candy:core.chat.message.server', {
+ type: type,
+ subject: msg.children('subject').text(),
+ message: msg.children('body').text()
+ });
+ }
+ break;
+ case 'groupchat':
+ case 'chat':
+ case 'error':
+ // Room message
+ self.Jabber.Room.Message(msg);
+ break;
+ default:
+ /** 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) 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]
+ * (String) type - Type of the message [default: message]
+ * (Object) message - Message object.
*/
- $(Candy).triggerHandler('candy:core:chat:invite', {
- roomJid: directInvite.attr('jid'),
- from: fromJid,
- reason: directInvite.attr('reason') || '',
- password: directInvite.attr('password'),
- continuedThread: directInvite.attr('thread')
+ // Detect message with type normal or with no type.
+ $(Candy).triggerHandler('candy:core:chat:message:other', {
+ type: type,
+ message: msg
});
- }
-
- /** 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;
- }
+ _findInvite: function (msg) {
+ var mediatedInvite = msg.find('invite'),
+ directInvite = msg.find('x[xmlns="jabber:x:conference"]'),
+ invite;
- var roomName = Candy.Core.getRoom(roomJid).getName(),
- item = msg.find('item'),
- type = 'leave',
+ if(mediatedInvite.length > 0) {
+ var passwordNode = msg.find('password'),
+ password,
+ reasonNode = mediatedInvite.find('reason'),
reason,
- actor;
+ continueNode = mediatedInvite.find('continue');
- 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');
+ if(passwordNode.text() !== '') {
+ password = passwordNode.text();
}
- var user = new Candy.Core.ChatUser(from, Strophe.getResourceFromJid(from), item.attr('affiliation'), item.attr('role'));
+ if(reasonNode.text() !== '') {
+ reason = reasonNode.text();
+ }
- /** 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;
- },
+ invite = {
+ roomJid: msg.attr('from'),
+ from: mediatedInvite.attr('from'),
+ reason: reason,
+ password: password,
+ continuedThread: continueNode.attr('thread')
+ };
+ }
+
+ if(directInvite.length > 0) {
+ invite = {
+ roomJid: directInvite.attr('jid'),
+ from: msg.attr('from'),
+ reason: directInvite.attr('reason'),
+ password: directInvite.attr('password'),
+ continuedThread: directInvite.attr('thread')
+ };
+ }
+ return invite;
+ },
+
+ /** Class: Candy.Core.Event.Jabber.Room
+ * Room specific events
+ */
+ Room: {
/** Function: Disco
* Sets informations to rooms according to the disco info received.
*
@@ -502,22 +555,9 @@ Candy.Core.Event = (function(self, Strophe, $) {
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;
- }
- }
- }
+ isNewRoom = self.Jabber.Room._msgHasStatusCode(msg, 201),
+ nickAssign = self.Jabber.Room._msgHasStatusCode(msg, 210),
+ nickChange = self.Jabber.Room._msgHasStatusCode(msg, 303);
// Current User joined a room
var room = Candy.Core.getRoom(roomJid);
@@ -526,16 +566,11 @@ Candy.Core.Event = (function(self, Strophe, $) {
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(),
+ currentUser = room.getUser() ? room.getUser() : Candy.Core.getUser(),
action, user,
nick,
+ show = msg.find('show'),
item = msg.find('item');
// User joined a room
if(presenceType !== 'unavailable') {
@@ -549,23 +584,31 @@ Candy.Core.Event = (function(self, Strophe, $) {
user.setRole(role);
user.setAffiliation(affiliation);
+ user.setStatus("available");
+
// 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'));
+ user = new Candy.Core.ChatUser(from, nick, item.attr('affiliation'), item.attr('role'), item.attr('jid'));
// Room existed but client (myself) is not yet registered
if(room.getUser() === null && (Candy.Core.getUser().getNick() === nick || nickAssign)) {
room.setUser(user);
currentUser = user;
}
+ user.setStatus('available');
roster.add(user);
action = 'join';
}
+
+ if (show.length > 0) {
+ user.setStatus(show.text());
+ }
// User left a room
} else {
user = roster.get(from);
roster.remove(from);
+
if(nickChange) {
// user changed nick
nick = item.attr('nick');
@@ -577,12 +620,18 @@ Candy.Core.Event = (function(self, Strophe, $) {
} else {
action = 'leave';
if(item.attr('role') === 'none') {
- if(msg.find('status').attr('code') === '307') {
+ if(self.Jabber.Room._msgHasStatusCode(msg, 307)) {
action = 'kick';
- } else if(msg.find('status').attr('code') === '301') {
+ } else if(self.Jabber.Room._msgHasStatusCode(msg, 301)) {
action = 'ban';
}
}
+
+ if (Strophe.getResourceFromJid(from) === currentUser.getNick()) {
+ // Current User left a room
+ self.Jabber.Room._selfLeave(msg, from, roomJid, room.getName(), action);
+ return true;
+ }
}
}
/** Event: candy:core.presence.room
@@ -594,17 +643,62 @@ Candy.Core.Event = (function(self, Strophe, $) {
* (Candy.Core.ChatUser) user - User which does the presence update
* (String) action - Action [kick, ban, leave, join]
* (Candy.Core.ChatUser) currentUser - Current local user
+ * (Boolean) isNewRoom - Whether the room is new (has just been created)
*/
$(Candy).triggerHandler('candy:core.presence.room', {
'roomJid': roomJid,
'roomName': room.getName(),
'user': user,
'action': action,
- 'currentUser': currentUser
+ 'currentUser': currentUser,
+ 'isNewRoom': isNewRoom
});
return true;
},
+ _msgHasStatusCode: function (msg, code) {
+ return msg.find('status[code="' + code + '"]').length > 0;
+ },
+
+ _selfLeave: function(msg, from, roomJid, roomName, action) {
+ Candy.Core.log('[Jabber:Room] Leave');
+
+ Candy.Core.removeRoom(roomJid);
+
+ var item = msg.find('item'),
+ reason,
+ actor;
+
+ if(action === 'kick' || action === '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': action,
+ 'reason': reason,
+ 'actor': actor,
+ 'user': user
+ });
+ },
+
/** Function: PresenceError
* Acts when a presence of type error has been retrieved.
*
@@ -661,87 +755,116 @@ Candy.Core.Event = (function(self, Strophe, $) {
*/
Message: function(msg) {
Candy.Core.log('[Jabber:Room] Message');
+
+ var carbon = false,
+ partnerJid = Candy.Util.unescapeJid(msg.attr('from'));
+
+ if (msg.children('sent[xmlns="' + Strophe.NS.CARBONS + '"]').length > 0) {
+ carbon = true;
+ msg = $(msg.children('sent').children('forwarded').children('message'));
+ partnerJid = Candy.Util.unescapeJid(msg.attr('to'));
+ }
+
+ if (msg.children('received[xmlns="' + Strophe.NS.CARBONS + '"]').length > 0) {
+ carbon = true;
+ msg = $(msg.children('received').children('forwarded').children('message'));
+ partnerJid = Candy.Util.unescapeJid(msg.attr('from'));
+ }
+
// Room subject
- var roomJid, message, name;
+ var roomJid, roomName, from, message, name, room, sender;
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' };
+ roomJid = Candy.Util.unescapeJid(Strophe.getBareJidFromJid(partnerJid));
+ from = Candy.Util.unescapeJid(Strophe.getBareJidFromJid(msg.attr('from')));
+ roomName = Strophe.getNodeFromJid(roomJid);
+ message = { from: from, name: Strophe.getNodeFromJid(from), 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() };
+ roomJid = partnerJid;
+ roomName = Strophe.getNodeFromJid(roomJid);
+ message = { from: msg.attr('from'), 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);
+ from = Candy.Util.unescapeJid(msg.attr('from'));
+ var barePartner = Strophe.getBareJidFromJid(partnerJid),
+ bareFrom = Strophe.getBareJidFromJid(from),
+ isNoConferenceRoomJid = !Candy.Core.getRoom(barePartner);
+
+ if (isNoConferenceRoomJid) {
+ roomJid = barePartner;
+
+ var partner = Candy.Core.getRoster().get(barePartner);
+ if (partner) {
+ roomName = partner.getName();
+ } else {
+ roomName = Strophe.getNodeFromJid(barePartner);
+ }
- name = isNoConferenceRoomJid ? Strophe.getNodeFromJid(roomJid) : Strophe.getResourceFromJid(roomJid);
- message = { name: name, body: msg.children('body').text(), type: msg.attr('type'), isNoConferenceRoomJid: isNoConferenceRoomJid };
+ if (bareFrom === Candy.Core.getUser().getJid()) {
+ sender = Candy.Core.getUser();
+ } else {
+ sender = Candy.Core.getRoster().get(bareFrom);
+ }
+ if (sender) {
+ name = sender.getName();
+ } else {
+ name = Strophe.getNodeFromJid(from);
+ }
+ } else {
+ roomJid = partnerJid;
+ room = Candy.Core.getRoom(Candy.Util.unescapeJid(Strophe.getBareJidFromJid(from)));
+ sender = room.getRoster().get(from);
+ if (sender) {
+ name = sender.getName();
+ } else {
+ name = Strophe.getResourceFromJid(from);
+ }
+ roomName = name;
+ }
+ message = { from: from, 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'));
+ from = Candy.Util.unescapeJid(msg.attr('from'));
+ roomJid = Candy.Util.unescapeJid(Strophe.getBareJidFromJid(partnerJid));
+ var resource = Strophe.getResourceFromJid(partnerJid);
// Message from a user
if(resource) {
- resource = Strophe.unescapeNode(resource);
- message = { name: resource, body: msg.children('body').text(), type: msg.attr('type') };
+ room = Candy.Core.getRoom(roomJid);
+ roomName = room.getName();
+ if (resource === Candy.Core.getUser().getNick()) {
+ sender = Candy.Core.getUser();
+ } else {
+ sender = room.getRoster().get(from);
+ }
+ if (sender) {
+ name = sender.getName();
+ } else {
+ name = Strophe.unescapeNode(resource);
+ }
+ message = { from: roomJid, name: name, 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')]) {
+ if(!Candy.Core.getRooms()[partnerJid]) {
return true;
}
- message = { name: '', body: msg.children('body').text(), type: 'info' };
+ roomName = '';
+ message = { from: roomJid, 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();
+ if(xhtmlChild.length > 0) {
+ var xhtmlMessage = $($('<div>').append(xhtmlChild.children('body').first().contents()).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;
+
+ self.Jabber.Room._checkForChatStateNotification(msg, roomJid, name);
// Unhandled message
} else {
return true;
@@ -749,8 +872,19 @@ Candy.Core.Event = (function(self, Strophe, $) {
// 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;
+ var delay = msg.children('delay[xmlns="' + Strophe.NS.DELAY +'"]');
+
+ message.delay = false; // Default delay to being false.
+
+ if (delay.length < 1) {
+ // The jQuery xpath implementation doesn't support the or operator
+ delay = msg.children('x[xmlns="' + Strophe.NS.JABBER_DELAY +'"]');
+ } else {
+ // Add delay to the message object so that we can more easily tell if it's a delayed message or not.
+ message.delay = true;
+ }
+
+ var timestamp = delay.length > 0 ? delay.attr('stamp') : (new Date()).toISOString();
/** Event: candy:core.message
* Triggers on various message events (subject changed, private chat message, multi-user chat message).
@@ -761,7 +895,8 @@ Candy.Core.Event = (function(self, Strophe, $) {
* The following lists explain those parameters:
*
* Message Object Parameters:
- * (String) name - Room name
+ * (String) from - The unmodified JID that the stanza came from
+ * (String) name - Sender name
* (String) body - Message text
* (String) type - Message type ([normal, chat, groupchat])
* or 'info' which is used internally for displaying informational messages
@@ -769,9 +904,12 @@ Candy.Core.Event = (function(self, Strophe, $) {
* this user (not via the room) then the username is the node
* and not the resource.
* This flag tells if this is the case.
+ * (Boolean) delay - If there is a value for the delay element on a message it is a delayed message.
+ * This flag tells if this is the case.
*
* Parameters:
- * (String) roomJid - Room jid
+ * (String) roomJid - Room jid. For one-on-one messages, this is sanitized to the bare JID for indexing purposes.
+ * (String) roomName - Name of the contact
* (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}
@@ -779,16 +917,45 @@ Candy.Core.Event = (function(self, Strophe, $) {
* - 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
+ * (Boolean) carbon - Indication of wether or not the message was a carbon
+ * (String) stanza - The raw XML stanza
*
* TODO:
* Streamline those events sent and rename the parameters.
*/
$(Candy).triggerHandler('candy:core.message', {
roomJid: roomJid,
+ roomName: roomName,
message: message,
- timestamp: timestamp
+ timestamp: timestamp,
+ carbon: carbon,
+ stanza: msg
});
return true;
+ },
+
+ _checkForChatStateNotification: function (msg, roomJid, name) {
+ var chatStateElements = msg.children('*[xmlns="http://jabber.org/protocol/chatstates"]');
+ if (chatStateElements.length > 0) {
+ /** 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. ("active", "composing", "paused", "inactive", "gone")
+ *
+ */
+ $(Candy).triggerHandler('candy:core:message:chatstate', {
+ name: name,
+ roomJid: roomJid,
+ chatstate: chatStateElements[0].tagName
+ });
+ }
}
}
};
diff --git a/src/util.js b/src/util.js
index 69010c6..dfcf613 100644
--- a/src/util.js
+++ b/src/util.js
@@ -35,7 +35,7 @@ Candy.Util = (function(self, $){
};
/** Function: escapeJid
- * Escapes a jid (node & resource get escaped)
+ * Escapes a jid
*
* See:
* XEP-0106
@@ -243,7 +243,14 @@ Candy.Util = (function(self, $){
return undefined;
}
- var date = self.iso8601toDate(dateTime);
+ // See if we were passed a Date object
+ var date;
+ if (dateTime.toDateString) {
+ date = dateTime;
+ } else {
+ date = self.iso8601toDate(dateTime);
+ }
+
if(date.toDateString() === new Date().toDateString()) {
return date.format($.i18n._('timeFormat'));
} else {
@@ -351,10 +358,34 @@ Candy.Util = (function(self, $){
return ie;
};
+ /** Function: isMobile
+ * Checks to see if we're on a mobile device.
+ */
+ self.isMobile = function() {
+ var check = false;
+ (function(a){ if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od|ad)|android|ipad|playbook|silk|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) { check = true; } })(navigator.userAgent || navigator.vendor || window.opera);
+ return check;
+ };
+
/** Class: Candy.Util.Parser
* Parser for emoticons, links and also supports escaping.
*/
self.Parser = {
+ /** Function: jid
+ * Parse a JID into an object with each element
+ *
+ * Parameters:
+ * (String) jid - The string representation of a JID
+ */
+ jid: function (jid) {
+ var r = /^(([^@]+)@)?([^\/]+)(\/(.*))?$/i,
+ a = jid.match(r);
+
+ if (!a) { throw "not a valid jid (" + jid + ")"; }
+
+ return {node: a[2], domain: a[3], resource: a[4]};
+ },
+
/** PrivateVariable: _emoticonPath
* Path to emoticons.
*
@@ -472,13 +503,14 @@ Candy.Util = (function(self, $){
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');
+ text = text.replace(this.emoticons[i].regex, '$2<img class="emoticon" alt="$1" title="$1" src="' + this._emoticonPath + this.emoticons[i].image + '" />$3');
}
return text;
},
/** Function: linkify
* Replaces URLs with a HTML-link.
+ * big regex adapted from https://gist.github.com/dperini/729294 - Diego Perini, MIT license.
*
* Parameters:
* (String) text - Text to linkify
@@ -488,7 +520,9 @@ Candy.Util = (function(self, $){
*/
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>');
+ return text.replace(/(\b(?:(?:https?|ftp|file):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:1\d\d|2[01]\d|22[0-3]|[1-9]\d?)(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/\S*)?)/gi, function(matched, url) {
+ return '<a href="' + url + '" target="_blank">' + self.crop(url, Candy.View.getOptions().crop.message.url) + '</a>';
+ });
},
/** Function: escape
@@ -598,8 +632,7 @@ Candy.Util = (function(self, $){
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);
+ Candy.Core.warn("[Util:createHtml] Error while parsing XHTML:", e);
el = Strophe.xmlTextNode('');
}
} else {
diff --git a/src/view.js b/src/view.js
index f438670..ade660d 100644
--- a/src/view.js
+++ b/src/view.js
@@ -31,7 +31,7 @@ Candy.View = (function(self, $) {
* (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
+ * (Object) crop - crop if longer than defined: message.nickname=15, message.body=1000, message.url=undefined (not cropped), roster.nickname=15
* (Bool) enableXHTML - [default: false] enables XHTML messages sending & displaying
*/
_options = {
@@ -39,7 +39,7 @@ Candy.View = (function(self, $) {
assets: 'res/',
messages: { limit: 2000, remove: 500 },
crop: {
- message: { nickname: 15, body: 1000 },
+ message: { nickname: 15, body: 1000, url: undefined },
roster: { nickname: 15 }
},
enableXHTML: false
@@ -137,8 +137,7 @@ Candy.View = (function(self, $) {
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
+ toolbar: Candy.View.Template.Chat.toolbar
}));
// ... and let the elements dance.
diff --git a/src/view/observer.js b/src/view/observer.js
index 5ee456a..a2afcbc 100644
--- a/src/view/observer.js
+++ b/src/view/observer.js
@@ -215,6 +215,14 @@ Candy.View.Observer = (function(self, $) {
Candy.View.Pane.Roster.update(args.user.getJid(), args.user, args.action, args.currentUser);
Candy.View.Pane.PrivateRoom.setStatus(args.user.getJid(), args.action);
}
+ } else {
+ // Presence for a one-on-one chat
+ var bareJid = Strophe.getBareJidFromJid(args.from),
+ room = Candy.View.Pane.Chat.rooms[bareJid];
+ if(!room) {
+ return false;
+ }
+ room.targetJid = bareJid; // Reset the room's target JID
}
},
@@ -275,18 +283,28 @@ Candy.View.Observer = (function(self, $) {
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.init(args.roomJid, args.roomName);
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);
+ Candy.View.Pane.Chat.infoMessage(args.roomJid, null, 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.PrivateRoom.open(args.roomJid, args.roomName, false, args.message.isNoConferenceRoomJid);
+ }
+ var room = Candy.View.Pane.Chat.rooms[args.roomJid];
+ if (room.targetJid === args.roomJid && !args.carbon) {
+ // No messages yet received. Lock the room to this resource.
+ room.targetJid = args.message.from;
+ } else if (room.targetJid === args.message.from) {
+ // We're already locked to the correct resource.
+ } else {
+ // Message received from alternative resource. Release the resource lock.
+ room.targetJid = args.roomJid;
}
- Candy.View.Pane.Message.show(args.roomJid, args.message.name, args.message.body, args.message.xhtmlMessage, args.timestamp);
+ Candy.View.Pane.Message.show(args.roomJid, args.message.name, args.message.body, args.message.xhtmlMessage, args.timestamp, args.message.from, args.carbon, args.stanza);
}
};
diff --git a/src/view/pane.js b/src/view/pane.js
deleted file mode 100644
index 007d816..0000000
--- a/src/view/pane.js
+++ /dev/null
@@ -1,2094 +0,0 @@
-/** File: pane.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, document, Mustache, Strophe, Audio, jQuery */
-
-/** Class: Candy.View.Pane
- * Candy view pane handles everything regarding DOM updates etc.
- *
- * Parameters:
- * (Candy.View.Pane) self - itself
- * (jQuery) $ - jQuery
- */
-Candy.View.Pane = (function(self, $) {
-
- /** Class: Candy.View.Pane.Window
- * Window related view updates
- */
- self.Window = {
- /** PrivateVariable: _hasFocus
- * Window has focus
- */
- _hasFocus: true,
- /** PrivateVariable: _plainTitle
- * Document title
- */
- _plainTitle: document.title,
- /** PrivateVariable: _unreadMessagesCount
- * Unread messages count
- */
- _unreadMessagesCount: 0,
-
- /** Variable: autoscroll
- * Boolean whether autoscroll is enabled
- */
- autoscroll: true,
-
- /** Function: hasFocus
- * Checks if window has focus
- *
- * Returns:
- * (Boolean)
- */
- hasFocus: function() {
- return self.Window._hasFocus;
- },
-
- /** Function: increaseUnreadMessages
- * Increases unread message count in window title by one.
- */
- increaseUnreadMessages: function() {
- self.Window.renderUnreadMessages(++self.Window._unreadMessagesCount);
- },
-
- /** Function: reduceUnreadMessages
- * Reduce unread message count in window title by `num`.
- *
- * Parameters:
- * (Integer) num - Unread message count will be reduced by this value
- */
- reduceUnreadMessages: function(num) {
- self.Window._unreadMessagesCount -= num;
- if(self.Window._unreadMessagesCount <= 0) {
- self.Window.clearUnreadMessages();
- } else {
- self.Window.renderUnreadMessages(self.Window._unreadMessagesCount);
- }
- },
-
- /** Function: clearUnreadMessages
- * Clear unread message count in window title.
- */
- clearUnreadMessages: function() {
- self.Window._unreadMessagesCount = 0;
- document.title = self.Window._plainTitle;
- },
-
- /** Function: renderUnreadMessages
- * Update window title to show message count.
- *
- * Parameters:
- * (Integer) count - Number of unread messages to show in window title
- */
- renderUnreadMessages: function(count) {
- document.title = Candy.View.Template.Window.unreadmessages.replace('{{count}}', count).replace('{{title}}', self.Window._plainTitle);
- },
-
- /** Function: onFocus
- * Window focus event handler.
- */
- onFocus: function() {
- self.Window._hasFocus = true;
- if (Candy.View.getCurrent().roomJid) {
- self.Room.setFocusToForm(Candy.View.getCurrent().roomJid);
- self.Chat.clearUnreadMessages(Candy.View.getCurrent().roomJid);
- }
- },
-
- /** Function: onBlur
- * Window blur event handler.
- */
- onBlur: function() {
- self.Window._hasFocus = false;
- }
- };
-
- /** Class: Candy.View.Pane.Chat
- * Chat-View related view updates
- */
- self.Chat = {
- /** Variable: rooms
- * Contains opened room elements
- */
- rooms: [],
-
- /** Function: addTab
- * Add a tab to the chat pane.
- *
- * Parameters:
- * (String) roomJid - JID of room
- * (String) roomName - Tab label
- * (String) roomType - Type of room: `groupchat` or `chat`
- */
- addTab: function(roomJid, roomName, roomType) {
- var roomId = Candy.Util.jidToId(roomJid),
- html = Mustache.to_html(Candy.View.Template.Chat.tab, {
- roomJid: roomJid,
- roomId: roomId,
- name: roomName || Strophe.getNodeFromJid(roomJid),
- privateUserChat: function() {return roomType === 'chat';},
- roomType: roomType
- }),
- tab = $(html).appendTo('#chat-tabs');
-
- tab.click(self.Chat.tabClick);
- // TODO: maybe we find a better way to get the close element.
- $('a.close', tab).click(self.Chat.tabClose);
-
- self.Chat.fitTabs();
- },
-
- /** Function: getTab
- * Get tab by JID.
- *
- * Parameters:
- * (String) roomJid - JID of room
- *
- * Returns:
- * (jQuery object) - Tab element
- */
- getTab: function(roomJid) {
- return $('#chat-tabs').children('li[data-roomjid="' + roomJid + '"]');
- },
-
- /** Function: removeTab
- * Remove tab element.
- *
- * Parameters:
- * (String) roomJid - JID of room
- */
- removeTab: function(roomJid) {
- self.Chat.getTab(roomJid).remove();
- self.Chat.fitTabs();
- },
-
- /** Function: setActiveTab
- * Set the active tab.
- *
- * Add CSS classname `active` to the choosen tab and remove `active` from all other.
- *
- * Parameters:
- * (String) roomJid - JID of room
- */
- setActiveTab: function(roomJid) {
- $('#chat-tabs').children().each(function() {
- var tab = $(this);
- if(tab.attr('data-roomjid') === roomJid) {
- tab.addClass('active');
- } else {
- tab.removeClass('active');
- }
- });
- },
-
- /** Function: increaseUnreadMessages
- * Increase unread message count in a tab by one.
- *
- * Parameters:
- * (String) roomJid - JID of room
- *
- * Uses:
- * - <Window.increaseUnreadMessages>
- */
- increaseUnreadMessages: function(roomJid) {
- var unreadElem = this.getTab(roomJid).find('.unread');
- unreadElem.show().text(unreadElem.text() !== '' ? parseInt(unreadElem.text(), 10) + 1 : 1);
- // only increase window unread messages in private chats
- if (self.Chat.rooms[roomJid].type === 'chat') {
- self.Window.increaseUnreadMessages();
- }
- },
-
- /** Function: clearUnreadMessages
- * Clear unread message count in a tab.
- *
- * Parameters:
- * (String) roomJid - JID of room
- *
- * Uses:
- * - <Window.reduceUnreadMessages>
- */
- clearUnreadMessages: function(roomJid) {
- var unreadElem = self.Chat.getTab(roomJid).find('.unread');
- self.Window.reduceUnreadMessages(unreadElem.text());
- unreadElem.hide().text('');
- },
-
- /** Function: tabClick
- * Tab click event: show the room associated with the tab and stops the event from doing the default.
- */
- tabClick: function(e) {
- // remember scroll position of current room
- var currentRoomJid = Candy.View.getCurrent().roomJid;
- self.Chat.rooms[currentRoomJid].scrollPosition = self.Room.getPane(currentRoomJid, '.message-pane-wrapper').scrollTop();
-
- self.Room.show($(this).attr('data-roomjid'));
- e.preventDefault();
- },
-
- /** Function: tabClose
- * Tab close (click) event: Leave the room (groupchat) or simply close the tab (chat).
- *
- * Parameters:
- * (DOMEvent) e - Event triggered
- *
- * Returns:
- * (Boolean) - false, this will stop the event from bubbling
- */
- tabClose: function() {
- var roomJid = $(this).parent().attr('data-roomjid');
- // close private user tab
- if(self.Chat.rooms[roomJid].type === 'chat') {
- self.Room.close(roomJid);
- // close multi-user room tab
- } else {
- Candy.Core.Action.Jabber.Room.Leave(roomJid);
- }
- return false;
- },
-
- /** Function: allTabsClosed
- * All tabs closed event: Disconnect from service. Hide sound control.
- *
- * TODO: Handle window close
- *
- * Returns:
- * (Boolean) - false, this will stop the event from bubbling
- */
- allTabsClosed: function() {
- Candy.Core.disconnect();
- self.Chat.Toolbar.hide();
- return;
- },
-
- /** Function: fitTabs
- * Fit tab size according to window size
- */
- fitTabs: function() {
- var availableWidth = $('#chat-tabs').innerWidth(),
- tabsWidth = 0,
- tabs = $('#chat-tabs').children();
- tabs.each(function() {
- tabsWidth += $(this).css({width: 'auto', overflow: 'visible'}).outerWidth(true);
- });
- if(tabsWidth > availableWidth) {
- // tabs.[outer]Width() measures the first element in `tabs`. It's no very readable but nearly two times faster than using :first
- var tabDiffToRealWidth = tabs.outerWidth(true) - tabs.width(),
- tabWidth = Math.floor((availableWidth) / tabs.length) - tabDiffToRealWidth;
- tabs.css({width: tabWidth, overflow: 'hidden'});
- }
- },
-
- /** Function: adminMessage
- * Display admin message
- *
- * Parameters:
- * (String) subject - Admin message subject
- * (String) message - Message to be displayed
- *
- * Triggers:
- * candy:view.chat.admin-message using {subject, message}
- */
- adminMessage: function(subject, message) {
- if(Candy.View.getCurrent().roomJid) { // Simply dismiss admin message if no room joined so far. TODO: maybe we should show those messages on a dedicated pane?
- var html = Mustache.to_html(Candy.View.Template.Chat.adminMessage, {
- subject: subject,
- message: message,
- sender: $.i18n._('administratorMessageSubject'),
- time: Candy.Util.localizedTime(new Date().toGMTString())
- });
- $('#chat-rooms').children().each(function() {
- self.Room.appendToMessagePane($(this).attr('data-roomjid'), html);
- });
- self.Room.scrollToBottom(Candy.View.getCurrent().roomJid);
-
- /** Event: candy:view.chat.admin-message
- * After admin message display
- *
- * Parameters:
- * (String) presetJid - Preset user JID
- */
- $(Candy).triggerHandler('candy:view.chat.admin-message', {
- 'subject' : subject,
- 'message' : message
- });
- }
- },
-
- /** Function: infoMessage
- * Display info message. This is a wrapper for <onInfoMessage> to be able to disable certain info messages.
- *
- * Parameters:
- * (String) roomJid - Room JID
- * (String) subject - Subject
- * (String) message - Message
- */
- infoMessage: function(roomJid, subject, message) {
- self.Chat.onInfoMessage(roomJid, subject, message);
- },
-
- /** Function: onInfoMessage
- * Display info message. Used by <infoMessage> and several other functions which do not wish that their info message
- * can be disabled (such as kick/ban message or leave/join message in private chats).
- *
- * Parameters:
- * (String) roomJid - Room JID
- * (String) subject - Subject
- * (String) message - Message
- */
- onInfoMessage: function(roomJid, subject, message) {
- if(Candy.View.getCurrent().roomJid) { // Simply dismiss info message if no room joined so far. TODO: maybe we should show those messages on a dedicated pane?
- var html = Mustache.to_html(Candy.View.Template.Chat.infoMessage, {
- subject: subject,
- message: $.i18n._(message),
- time: Candy.Util.localizedTime(new Date().toGMTString())
- });
- self.Room.appendToMessagePane(roomJid, html);
- if (Candy.View.getCurrent().roomJid === roomJid) {
- self.Room.scrollToBottom(Candy.View.getCurrent().roomJid);
- }
- }
- },
-
- /** Class: Candy.View.Pane.Toolbar
- * Chat toolbar for things like emoticons toolbar, room management etc.
- */
- Toolbar: {
- _supportsNativeAudio: false,
-
- /** Function: init
- * Register handler and enable or disable sound and status messages.
- */
- init: function() {
- $('#emoticons-icon').click(function(e) {
- self.Chat.Context.showEmoticonsMenu(e.currentTarget);
- e.stopPropagation();
- });
- $('#chat-autoscroll-control').click(self.Chat.Toolbar.onAutoscrollControlClick);
-
- var a = document.createElement('audio');
- self.Chat.Toolbar._supportsNativeAudio = !!(a.canPlayType && a.canPlayType('audio/mpeg;').replace(/no/, ''));
- $('#chat-sound-control').click(self.Chat.Toolbar.onSoundControlClick);
- if(Candy.Util.cookieExists('candy-nosound')) {
- $('#chat-sound-control').click();
- }
- $('#chat-statusmessage-control').click(self.Chat.Toolbar.onStatusMessageControlClick);
- if(Candy.Util.cookieExists('candy-nostatusmessages')) {
- $('#chat-statusmessage-control').click();
- }
- },
-
- /** Function: show
- * Show toolbar.
- */
- show: function() {
- $('#chat-toolbar').show();
- },
-
- /** Function: hide
- * Hide toolbar.
- */
- hide: function() {
- $('#chat-toolbar').hide();
- },
-
- /* Function: update
- * Update toolbar for specific room
- */
- update: function(roomJid) {
- var context = $('#chat-toolbar').find('.context'),
- me = self.Room.getUser(roomJid);
- if(!me || !me.isModerator()) {
- context.hide();
- } else {
- context.show().click(function(e) {
- self.Chat.Context.show(e.currentTarget, roomJid);
- e.stopPropagation();
- });
- }
- self.Chat.Toolbar.updateUsercount(self.Chat.rooms[roomJid].usercount);
- },
-
- /** Function: playSound
- * Play sound (default method).
- */
- playSound: function() {
- self.Chat.Toolbar.onPlaySound();
- },
-
- /** Function: onPlaySound
- * Sound play event handler. Uses native (HTML5) audio if supported
- *
- * Don't call this method directly. Call `playSound()` instead.
- * `playSound()` will only call this method if sound is enabled.
- */
- onPlaySound: function() {
- try {
- if(self.Chat.Toolbar._supportsNativeAudio) {
- new Audio(Candy.View.getOptions().assets + 'notify.mp3').play();
- } else {
- var chatSoundPlayer = document.getElementById('chat-sound-player');
- chatSoundPlayer.SetVariable('method:stop', '');
- chatSoundPlayer.SetVariable('method:play', '');
- }
- } catch (e) {}
- },
-
- /** Function: onSoundControlClick
- * Sound control click event handler.
- *
- * Toggle sound (overwrite `playSound()`) and handle cookies.
- */
- onSoundControlClick: function() {
- var control = $('#chat-sound-control');
- if(control.hasClass('checked')) {
- self.Chat.Toolbar.playSound = function() {};
- Candy.Util.setCookie('candy-nosound', '1', 365);
- } else {
- self.Chat.Toolbar.playSound = function() {
- self.Chat.Toolbar.onPlaySound();
- };
- Candy.Util.deleteCookie('candy-nosound');
- }
- control.toggleClass('checked');
- },
-
- /** Function: onAutoscrollControlClick
- * Autoscroll control event handler.
- *
- * Toggle autoscroll
- */
- onAutoscrollControlClick: function() {
- var control = $('#chat-autoscroll-control');
- if(control.hasClass('checked')) {
- self.Room.scrollToBottom = function(roomJid) {
- self.Room.onScrollToStoredPosition(roomJid);
- };
- self.Window.autoscroll = false;
- } else {
- self.Room.scrollToBottom = function(roomJid) {
- self.Room.onScrollToBottom(roomJid);
- };
- self.Room.scrollToBottom(Candy.View.getCurrent().roomJid);
- self.Window.autoscroll = true;
- }
- control.toggleClass('checked');
- },
-
- /** Function: onStatusMessageControlClick
- * Status message control event handler.
- *
- * Toggle status message
- */
- onStatusMessageControlClick: function() {
- var control = $('#chat-statusmessage-control');
- if(control.hasClass('checked')) {
- self.Chat.infoMessage = function() {};
- Candy.Util.setCookie('candy-nostatusmessages', '1', 365);
- } else {
- self.Chat.infoMessage = function(roomJid, subject, message) {
- self.Chat.onInfoMessage(roomJid, subject, message);
- };
- Candy.Util.deleteCookie('candy-nostatusmessages');
- }
- control.toggleClass('checked');
- },
-
- /** Function: updateUserCount
- * Update usercount element with count.
- *
- * Parameters:
- * (Integer) count - Current usercount
- */
- updateUsercount: function(count) {
- $('#chat-usercount').text(count);
- }
- },
-
- /** Class: Candy.View.Pane.Modal
- * Modal window
- */
- Modal: {
- /** Function: show
- * Display modal window
- *
- * Parameters:
- * (String) html - HTML code to put into the modal window
- * (Boolean) showCloseControl - set to true if a close button should be displayed [default false]
- * (Boolean) showSpinner - set to true if a loading spinner should be shown [default false]
- */
- show: function(html, showCloseControl, showSpinner) {
- if(showCloseControl) {
- self.Chat.Modal.showCloseControl();
- } else {
- self.Chat.Modal.hideCloseControl();
- }
- if(showSpinner) {
- self.Chat.Modal.showSpinner();
- } else {
- self.Chat.Modal.hideSpinner();
- }
- $('#chat-modal').stop(false, true);
- $('#chat-modal-body').html(html);
- $('#chat-modal').fadeIn('fast');
- $('#chat-modal-overlay').show();
- },
-
- /** Function: hide
- * Hide modal window
- *
- * Parameters:
- * (Function) callback - Calls the specified function after modal window has been hidden.
- */
- hide: function(callback) {
- $('#chat-modal').fadeOut('fast', function() {
- $('#chat-modal-body').text('');
- $('#chat-modal-overlay').hide();
- });
- // restore initial esc handling
- $(document).keydown(function(e) {
- if(e.which === 27) {
- e.preventDefault();
- }
- });
- if (callback) {
- callback();
- }
- },
-
- /** Function: showSpinner
- * Show loading spinner
- */
- showSpinner: function() {
- $('#chat-modal-spinner').show();
- },
-
- /** Function: hideSpinner
- * Hide loading spinner
- */
- hideSpinner: function() {
- $('#chat-modal-spinner').hide();
- },
-
- /** Function: showCloseControl
- * Show a close button
- */
- showCloseControl: function() {
- $('#admin-message-cancel').show().click(function(e) {
- self.Chat.Modal.hide();
- // some strange behaviour on IE7 (and maybe other browsers) triggers onWindowUnload when clicking on the close button.
- // prevent this.
- e.preventDefault();
- });
-
- // enable esc to close modal
- $(document).keydown(function(e) {
- if(e.which === 27) {
- self.Chat.Modal.hide();
- e.preventDefault();
- }
- });
- },
-
- /** Function: hideCloseControl
- * Hide the close button
- */
- hideCloseControl: function() {
- $('#admin-message-cancel').hide().click(function() {});
- },
-
- /** Function: showLoginForm
- * Show the login form modal
- *
- * Parameters:
- * (String) message - optional message to display above the form
- * (String) presetJid - optional user jid. if set, the user will only be prompted for password.
- */
- showLoginForm: function(message, presetJid) {
- self.Chat.Modal.show((message ? message : '') + Mustache.to_html(Candy.View.Template.Login.form, {
- _labelNickname: $.i18n._('labelNickname'),
- _labelUsername: $.i18n._('labelUsername'),
- _labelPassword: $.i18n._('labelPassword'),
- _loginSubmit: $.i18n._('loginSubmit'),
- displayPassword: !Candy.Core.isAnonymousConnection(),
- displayUsername: !presetJid,
- displayNickname: Candy.Core.isAnonymousConnection(),
- presetJid: presetJid ? presetJid : false
- }));
- $('#login-form').children(':input:first').focus();
-
- // register submit handler
- $('#login-form').submit(function() {
- var username = $('#username').val(),
- password = $('#password').val();
-
- if (!Candy.Core.isAnonymousConnection()) {
- // guess the input and create a jid out of it
- var jid = Candy.Core.getUser() && username.indexOf("@") < 0 ?
- username + '@' + Strophe.getDomainFromJid(Candy.Core.getUser().getJid()) : username;
-
- if(jid.indexOf("@") < 0 && !Candy.Core.getUser()) {
- Candy.View.Pane.Chat.Modal.showLoginForm($.i18n._('loginInvalid'));
- } else {
- //Candy.View.Pane.Chat.Modal.hide();
- Candy.Core.connect(jid, password);
- }
- } else { // anonymous login
- Candy.Core.connect(presetJid, null, username);
- }
- return false;
- });
- },
-
- /** Function: showEnterPasswordForm
- * Shows a form for entering room password
- *
- * Parameters:
- * (String) roomJid - Room jid to join
- * (String) roomName - Room name
- * (String) message - [optional] Message to show as the label
- */
- showEnterPasswordForm: function(roomJid, roomName, message) {
- self.Chat.Modal.show(Mustache.to_html(Candy.View.Template.PresenceError.enterPasswordForm, {
- roomName: roomName,
- _labelPassword: $.i18n._('labelPassword'),
- _label: (message ? message : $.i18n._('enterRoomPassword', [roomName])),
- _joinSubmit: $.i18n._('enterRoomPasswordSubmit')
- }), true);
- $('#password').focus();
-
- // register submit handler
- $('#enter-password-form').submit(function() {
- var password = $('#password').val();
-
- self.Chat.Modal.hide(function() {
- Candy.Core.Action.Jabber.Room.Join(roomJid, password);
- });
- return false;
- });
- },
-
- /** Function: showNicknameConflictForm
- * Shows a form indicating that the nickname is already taken and
- * for chosing a new nickname
- *
- * Parameters:
- * (String) roomJid - Room jid to join
- */
- showNicknameConflictForm: function(roomJid) {
- self.Chat.Modal.show(Mustache.to_html(Candy.View.Template.PresenceError.nicknameConflictForm, {
- _labelNickname: $.i18n._('labelNickname'),
- _label: $.i18n._('nicknameConflict'),
- _loginSubmit: $.i18n._('loginSubmit')
- }));
- $('#nickname').focus();
-
- // register submit handler
- $('#nickname-conflict-form').submit(function() {
- var nickname = $('#nickname').val();
-
- self.Chat.Modal.hide(function() {
- Candy.Core.getUser().data.nick = nickname;
- Candy.Core.Action.Jabber.Room.Join(roomJid);
- });
- return false;
- });
- },
-
- /** Function: showError
- * Show modal containing error message
- *
- * Parameters:
- * (String) message - key of translation to display
- * (Array) replacements - array containing replacements for translation (%s)
- */
- showError: function(message, replacements) {
- self.Chat.Modal.show(Mustache.to_html(Candy.View.Template.PresenceError.displayError, {
- _error: $.i18n._(message, replacements)
- }), true);
- }
- },
-
- /** Class: Candy.View.Pane.Tooltip
- * Class to display tooltips over specific elements
- */
- Tooltip: {
- /** Function: show
- * Show a tooltip on event.currentTarget with content specified or content within the target's attribute data-tooltip.
- *
- * On mouseleave on the target, hide the tooltip.
- *
- * Parameters:
- * (Event) event - Triggered event
- * (String) content - Content to display [optional]
- */
- show: function(event, content) {
- var tooltip = $('#tooltip'),
- target = $(event.currentTarget);
-
- if(!content) {
- content = target.attr('data-tooltip');
- }
-
- if(tooltip.length === 0) {
- var html = Mustache.to_html(Candy.View.Template.Chat.tooltip);
- $('#chat-pane').append(html);
- tooltip = $('#tooltip');
- }
-
- $('#context-menu').hide();
-
- tooltip.stop(false, true);
- tooltip.children('div').html(content);
-
- var pos = target.offset(),
- posLeft = Candy.Util.getPosLeftAccordingToWindowBounds(tooltip, pos.left),
- posTop = Candy.Util.getPosTopAccordingToWindowBounds(tooltip, pos.top);
-
- tooltip
- .css({'left': posLeft.px, 'top': posTop.px})
- .removeClass('left-top left-bottom right-top right-bottom')
- .addClass(posLeft.backgroundPositionAlignment + '-' + posTop.backgroundPositionAlignment)
- .fadeIn('fast');
-
- target.mouseleave(function(event) {
- event.stopPropagation();
- $('#tooltip').stop(false, true).fadeOut('fast', function() {$(this).css({'top': 0, 'left': 0});});
- });
- }
- },
-
- /** Class: Candy.View.Pane.Context
- * Context menu for actions and settings
- */
- Context: {
- /** Function: init
- * Initialize context menu and setup mouseleave handler.
- */
- init: function() {
- if ($('#context-menu').length === 0) {
- var html = Mustache.to_html(Candy.View.Template.Chat.Context.menu);
- $('#chat-pane').append(html);
- $('#context-menu').mouseleave(function() {
- $(this).fadeOut('fast');
- });
- }
- },
-
- /** Function: show
- * Show context menu (positions it according to the window height/width)
- *
- * Parameters:
- * (Element) elem - On which element it should be shown
- * (String) roomJid - Room Jid of the room it should be shown
- * (Candy.Core.chatUser) user - User
- *
- * Uses:
- * <getMenuLinks> for getting menulinks the user has access to
- * <Candy.Util.getPosLeftAccordingToWindowBounds> for positioning
- * <Candy.Util.getPosTopAccordingToWindowBounds> for positioning
- *
- * Triggers:
- * candy:view.roster.after-context-menu using {roomJid, user, elements}
- */
- show: function(elem, roomJid, user) {
- elem = $(elem);
- var roomId = self.Chat.rooms[roomJid].id,
- menu = $('#context-menu'),
- links = $('ul li', menu);
-
- $('#tooltip').hide();
-
- // add specific context-user class if a user is available (when context menu should be opened next to a user)
- if(!user) {
- user = Candy.Core.getUser();
- }
-
- links.remove();
-
- var menulinks = this.getMenuLinks(roomJid, user, elem),
- id,
- clickHandler = function(roomJid, user) {
- return function(event) {
- event.data.callback(event, roomJid, user);
- $('#context-menu').hide();
- };
- };
-
- for(id in menulinks) {
- if(menulinks.hasOwnProperty(id)) {
- var link = menulinks[id],
- html = Mustache.to_html(Candy.View.Template.Chat.Context.menulinks, {
- 'roomId' : roomId,
- 'class' : link['class'],
- 'id' : id,
- 'label' : link.label
- });
- $('ul', menu).append(html);
- $('#context-menu-' + id).bind('click', link, clickHandler(roomJid, user));
- }
- }
- // if `id` is set the menu is not empty
- if(id) {
- var pos = elem.offset(),
- posLeft = Candy.Util.getPosLeftAccordingToWindowBounds(menu, pos.left),
- posTop = Candy.Util.getPosTopAccordingToWindowBounds(menu, pos.top);
-
- menu
- .css({'left': posLeft.px, 'top': posTop.px})
- .removeClass('left-top left-bottom right-top right-bottom')
- .addClass(posLeft.backgroundPositionAlignment + '-' + posTop.backgroundPositionAlignment)
- .fadeIn('fast');
-
- /** Event: candy:view.roster.after-context-menu
- * After context menu display
- *
- * Parameters:
- * (String) roomJid - room where the context menu has been triggered
- * (Candy.Core.ChatUser) user - User
- * (jQuery.Element) element - Menu element
- */
- $(Candy).triggerHandler('candy:view.roster.after-context-menu', {
- 'roomJid' : roomJid,
- 'user' : user,
- 'element': menu
- });
-
- return true;
- }
- },
-
- /** Function: getMenuLinks
- * Extends <initialMenuLinks> with menu links gathered from candy:view.roster.contextmenu
- *
- * Parameters:
- * (String) roomJid - Room in which the menu will be displayed
- * (Candy.Core.ChatUser) user - User
- * (jQuery.Element) elem - Parent element of the context menu
- *
- * Triggers:
- * candy:view.roster.context-menu using {roomJid, user, elem}
- *
- * Returns:
- * (Object) - object containing the extended menulinks.
- */
- getMenuLinks: function(roomJid, user, elem) {
- var menulinks, id;
-
- var evtData = {
- 'roomJid' : roomJid,
- 'user' : user,
- 'elem': elem,
- 'menulinks': this.initialMenuLinks(elem)
- };
-
- /** Event: candy:view.roster.context-menu
- * Modify existing menu links (add links)
- *
- * In order to modify the links you need to change the object passed with an additional
- * key "menulinks" containing the menulink object.
- *
- * Parameters:
- * (String) roomJid - Room on which the menu should be displayed
- * (Candy.Core.ChatUser) user - User
- * (jQuery.Element) elem - Parent element of the context menu
- */
- $(Candy).triggerHandler('candy:view.roster.context-menu', evtData);
-
- menulinks = evtData.menulinks;
-
- for(id in menulinks) {
- if(menulinks.hasOwnProperty(id) && menulinks[id].requiredPermission !== undefined && !menulinks[id].requiredPermission(user, self.Room.getUser(roomJid), elem)) {
- delete menulinks[id];
- }
- }
- return menulinks;
- },
-
- /** Function: initialMenuLinks
- * Returns initial menulinks. The following are initial:
- *
- * - Private Chat
- * - Ignore
- * - Unignore
- * - Kick
- * - Ban
- * - Change Subject
- *
- * Returns:
- * (Object) - object containing those menulinks
- */
- initialMenuLinks: function() {
- return {
- 'private': {
- requiredPermission: function(user, me) {
- return me.getNick() !== user.getNick() && Candy.Core.getRoom(Candy.View.getCurrent().roomJid) && !Candy.Core.getUser().isInPrivacyList('ignore', user.getJid());
- },
- 'class' : 'private',
- 'label' : $.i18n._('privateActionLabel'),
- 'callback' : function(e, roomJid, user) {
- $('#user-' + Candy.Util.jidToId(roomJid) + '-' + Candy.Util.jidToId(user.getJid())).click();
- }
- },
- 'ignore': {
- requiredPermission: function(user, me) {
- return me.getNick() !== user.getNick() && !Candy.Core.getUser().isInPrivacyList('ignore', user.getJid());
- },
- 'class' : 'ignore',
- 'label' : $.i18n._('ignoreActionLabel'),
- 'callback' : function(e, roomJid, user) {
- Candy.View.Pane.Room.ignoreUser(roomJid, user.getJid());
- }
- },
- 'unignore': {
- requiredPermission: function(user, me) {
- return me.getNick() !== user.getNick() && Candy.Core.getUser().isInPrivacyList('ignore', user.getJid());
- },
- 'class' : 'unignore',
- 'label' : $.i18n._('unignoreActionLabel'),
- 'callback' : function(e, roomJid, user) {
- Candy.View.Pane.Room.unignoreUser(roomJid, user.getJid());
- }
- },
- 'kick': {
- requiredPermission: function(user, me) {
- return me.getNick() !== user.getNick() && me.isModerator() && !user.isModerator();
- },
- 'class' : 'kick',
- 'label' : $.i18n._('kickActionLabel'),
- 'callback' : function(e, roomJid, user) {
- self.Chat.Modal.show(Mustache.to_html(Candy.View.Template.Chat.Context.contextModalForm, {
- _label: $.i18n._('reason'),
- _submit: $.i18n._('kickActionLabel')
- }), true);
- $('#context-modal-field').focus();
- $('#context-modal-form').submit(function() {
- Candy.Core.Action.Jabber.Room.Admin.UserAction(roomJid, user.getJid(), 'kick', $('#context-modal-field').val());
- self.Chat.Modal.hide();
- return false; // stop propagation & preventDefault, as otherwise you get disconnected (wtf?)
- });
- }
- },
- 'ban': {
- requiredPermission: function(user, me) {
- return me.getNick() !== user.getNick() && me.isModerator() && !user.isModerator();
- },
- 'class' : 'ban',
- 'label' : $.i18n._('banActionLabel'),
- 'callback' : function(e, roomJid, user) {
- self.Chat.Modal.show(Mustache.to_html(Candy.View.Template.Chat.Context.contextModalForm, {
- _label: $.i18n._('reason'),
- _submit: $.i18n._('banActionLabel')
- }), true);
- $('#context-modal-field').focus();
- $('#context-modal-form').submit(function() {
- Candy.Core.Action.Jabber.Room.Admin.UserAction(roomJid, user.getJid(), 'ban', $('#context-modal-field').val());
- self.Chat.Modal.hide();
- return false; // stop propagation & preventDefault, as otherwise you get disconnected (wtf?)
- });
- }
- },
- 'subject': {
- requiredPermission: function(user, me) {
- return me.getNick() === user.getNick() && me.isModerator();
- },
- 'class': 'subject',
- 'label' : $.i18n._('setSubjectActionLabel'),
- 'callback': function(e, roomJid) {
- self.Chat.Modal.show(Mustache.to_html(Candy.View.Template.Chat.Context.contextModalForm, {
- _label: $.i18n._('subject'),
- _submit: $.i18n._('setSubjectActionLabel')
- }), true);
- $('#context-modal-field').focus();
- $('#context-modal-form').submit(function(e) {
- Candy.Core.Action.Jabber.Room.Admin.SetSubject(roomJid, $('#context-modal-field').val());
- self.Chat.Modal.hide();
- e.preventDefault();
- });
- }
- }
- };
- },
-
- /** Function: showEmoticonsMenu
- * Shows the special emoticons menu
- *
- * Parameters:
- * (Element) elem - Element on which it should be positioned to.
- *
- * Returns:
- * (Boolean) - true
- */
- showEmoticonsMenu: function(elem) {
- elem = $(elem);
- var pos = elem.offset(),
- menu = $('#context-menu'),
- content = $('ul', menu),
- emoticons = '',
- i;
-
- $('#tooltip').hide();
-
- for(i = Candy.Util.Parser.emoticons.length-1; i >= 0; i--) {
- emoticons = '<img src="' + Candy.Util.Parser._emoticonPath + Candy.Util.Parser.emoticons[i].image + '" alt="' + Candy.Util.Parser.emoticons[i].plain + '" />' + emoticons;
- }
- content.html('<li class="emoticons">' + emoticons + '</li>');
- content.find('img').click(function() {
- var input = Candy.View.Pane.Room.getPane(Candy.View.getCurrent().roomJid, '.message-form').children('.field'),
- value = input.val(),
- emoticon = $(this).attr('alt') + ' ';
- input.val(value ? value + ' ' + emoticon : emoticon).focus();
- });
-
- var posLeft = Candy.Util.getPosLeftAccordingToWindowBounds(menu, pos.left),
- posTop = Candy.Util.getPosTopAccordingToWindowBounds(menu, pos.top);
-
- menu
- .css({'left': posLeft.px, 'top': posTop.px})
- .removeClass('left-top left-bottom right-top right-bottom')
- .addClass(posLeft.backgroundPositionAlignment + '-' + posTop.backgroundPositionAlignment)
- .fadeIn('fast');
-
- return true;
- }
- }
- };
-
- /** Class: Candy.View.Pane.Room
- * Everything which belongs to room view things belongs here.
- */
- self.Room = {
- /** Function: init
- * Initialize a new room and inserts the room html into the DOM
- *
- * Parameters:
- * (String) roomJid - Room JID
- * (String) roomName - Room name
- * (String) roomType - Type: either "groupchat" or "chat" (private chat)
- *
- * Uses:
- * - <Candy.Util.jidToId>
- * - <Candy.View.Pane.Chat.addTab>
- * - <getPane>
- *
- * Triggers:
- * candy:view.room.after-add using {roomJid, type, element}
- *
- * Returns:
- * (String) - the room id of the element created.
- */
- init: function(roomJid, roomName, roomType) {
- roomType = roomType || 'groupchat';
- roomJid = Candy.Util.unescapeJid(roomJid);
-
- var evtData = {
- roomJid: roomJid,
- type: roomType
- };
- /** Event: candy:view.room.before-add
- * Before initialising a room
- *
- * Parameters:
- * (String) roomJid - Room JID
- * (String) type - Room Type
- *
- * Returns:
- * Boolean - if you don't want to initialise the room, return false.
- */
- if($(Candy).triggerHandler('candy:view.room.before-add', evtData) === false) {
- return false;
- }
-
- // First room, show sound control
- if(Candy.Util.isEmptyObject(self.Chat.rooms)) {
- self.Chat.Toolbar.show();
- }
-
- var roomId = Candy.Util.jidToId(roomJid);
- self.Chat.rooms[roomJid] = {id: roomId, usercount: 0, name: roomName, type: roomType, messageCount: 0, scrollPosition: -1};
-
- $('#chat-rooms').append(Mustache.to_html(Candy.View.Template.Room.pane, {
- roomId: roomId,
- roomJid: roomJid,
- roomType: roomType,
- form: {
- _messageSubmit: $.i18n._('messageSubmit')
- },
- roster: {
- _userOnline: $.i18n._('userOnline')
- }
- }, {
- roster: Candy.View.Template.Roster.pane,
- messages: Candy.View.Template.Message.pane,
- form: Candy.View.Template.Room.form
- }));
- self.Chat.addTab(roomJid, roomName, roomType);
- self.Room.getPane(roomJid, '.message-form').submit(self.Message.submit);
-
- evtData.element = self.Room.getPane(roomJid);
-
- /** Event: candy:view.room.after-add
- * After initialising a room
- *
- * Parameters:
- * (String) roomJid - Room JID
- * (String) type - Room Type
- * (jQuery.Element) element - Room element
- */
- $(Candy).triggerHandler('candy:view.room.after-add', evtData);
-
- return roomId;
- },
-
- /** Function: show
- * Show a specific room and hides the other rooms (if there are any)
- *
- * Parameters:
- * (String) roomJid - room jid to show
- *
- * Triggers:
- * candy:view.room.after-show using {roomJid, element}
- * candy:view.room.after-hide using {roomJid, element}
- */
- show: function(roomJid) {
- var roomId = self.Chat.rooms[roomJid].id,
- evtData;
-
- $('.room-pane').each(function() {
- var elem = $(this);
- evtData = {
- 'roomJid': elem.attr('data-roomjid'),
- 'element' : elem
- };
-
- if(elem.attr('id') === ('chat-room-' + roomId)) {
- elem.show();
- Candy.View.getCurrent().roomJid = roomJid;
- self.Chat.setActiveTab(roomJid);
- self.Chat.Toolbar.update(roomJid);
- self.Chat.clearUnreadMessages(roomJid);
- self.Room.setFocusToForm(roomJid);
- self.Room.scrollToBottom(roomJid);
-
- /** Event: candy:view.room.after-show
- * After showing a room
- *
- * Parameters:
- * (String) roomJid - Room JID
- * (jQuery.Element) element - Room element
- */
- $(Candy).triggerHandler('candy:view.room.after-show', evtData);
-
- } else {
- elem.hide();
-
- /** Event: candy:view.room.after-hide
- * After hiding a room
- *
- * Parameters:
- * (String) roomJid - Room JID
- * (jQuery.Element) element - Room element
- */
- $(Candy).triggerHandler('candy:view.room.after-hide', evtData);
- }
- });
- },
-
- /** Function: setSubject
- * Called when someone changes the subject in the channel
- *
- * Triggers:
- * candy:view.room.after-subject-change using {roomJid, element, subject}
- *
- * Parameters:
- * (String) roomJid - Room Jid
- * (String) subject - The new subject
- */
- setSubject: function(roomJid, subject) {
- subject = Candy.Util.Parser.linkify(Candy.Util.Parser.escape(subject));
- var html = Mustache.to_html(Candy.View.Template.Room.subject, {
- subject: subject,
- roomName: self.Chat.rooms[roomJid].name,
- _roomSubject: $.i18n._('roomSubject'),
- time: Candy.Util.localizedTime(new Date().toGMTString())
- });
- self.Room.appendToMessagePane(roomJid, html);
- self.Room.scrollToBottom(roomJid);
-
- /** Event: candy:view.room.after-subject-change
- * After changing the subject of a room
- *
- * Parameters:
- * (String) roomJid - Room JID
- * (jQuery.Element) element - Room element
- * (String) subject - New subject
- */
- $(Candy).triggerHandler('candy:view.room.after-subject-change', {
- 'roomJid': roomJid,
- 'element' : self.Room.getPane(roomJid),
- 'subject' : subject
- });
- },
-
- /** Function: close
- * Close a room and remove everything in the DOM belonging to this room.
- *
- * NOTICE: There's a rendering bug in Opera when all rooms have been closed.
- * (Take a look in the source for a more detailed description)
- *
- * Triggers:
- * candy:view.room.after-close using {roomJid}
- *
- * Parameters:
- * (String) roomJid - Room to close
- */
- close: function(roomJid) {
- self.Chat.removeTab(roomJid);
- self.Window.clearUnreadMessages();
-
- /* TODO:
- There's a rendering bug in Opera which doesn't redraw (remove) the message form.
- Only a cosmetical issue (when all tabs are closed) but it's annoying...
- This happens when form has no focus too. Maybe it's because of CSS positioning.
- */
- self.Room.getPane(roomJid).remove();
- var openRooms = $('#chat-rooms').children();
- if(Candy.View.getCurrent().roomJid === roomJid) {
- Candy.View.getCurrent().roomJid = null;
- if(openRooms.length === 0) {
- self.Chat.allTabsClosed();
- } else {
- self.Room.show(openRooms.last().attr('data-roomjid'));
- }
- }
- delete self.Chat.rooms[roomJid];
-
- /** Event: candy:view.room.after-close
- * After closing a room
- *
- * Parameters:
- * (String) roomJid - Room JID
- */
- $(Candy).triggerHandler('candy:view.room.after-close', {
- 'roomJid' : roomJid
- });
- },
-
- /** Function: appendToMessagePane
- * Append a new message to the message pane.
- *
- * Parameters:
- * (String) roomJid - Room JID
- * (String) html - rendered message html
- */
- appendToMessagePane: function(roomJid, html) {
- self.Room.getPane(roomJid, '.message-pane').append(html);
- self.Chat.rooms[roomJid].messageCount++;
- self.Room.sliceMessagePane(roomJid);
- },
-
- /** Function: sliceMessagePane
- * Slices the message pane after the max amount of messages specified in the Candy View options (limit setting).
- *
- * This is done to hopefully prevent browsers from getting slow after a certain amount of messages in the DOM.
- *
- * The slice is only done when autoscroll is on, because otherwise someone might lose exactly the message he want to look for.
- *
- * Parameters:
- * (String) roomJid - Room JID
- */
- sliceMessagePane: function(roomJid) {
- // Only clean if autoscroll is enabled
- if(self.Window.autoscroll) {
- var options = Candy.View.getOptions().messages;
- if(self.Chat.rooms[roomJid].messageCount > options.limit) {
- self.Room.getPane(roomJid, '.message-pane').children().slice(0, options.remove).remove();
- self.Chat.rooms[roomJid].messageCount -= options.remove;
- }
- }
- },
-
- /** Function: scrollToBottom
- * Scroll to bottom wrapper for <onScrollToBottom> to be able to disable it by overwriting the function.
- *
- * Parameters:
- * (String) roomJid - Room JID
- *
- * Uses:
- * - <onScrollToBottom>
- */
- scrollToBottom: function(roomJid) {
- self.Room.onScrollToBottom(roomJid);
- },
-
- /** Function: onScrollToBottom
- * Scrolls to the latest message received/sent.
- *
- * Parameters:
- * (String) roomJid - Room JID
- */
- onScrollToBottom: function(roomJid) {
- var messagePane = self.Room.getPane(roomJid, '.message-pane-wrapper');
- messagePane.scrollTop(messagePane.prop('scrollHeight'));
- },
-
- /** Function: onScrollToStoredPosition
- * When autoscroll is off, the position where the scrollbar is has to be stored for each room, because it otherwise
- * goes to the top in the message window.
- *
- * Parameters:
- * (String) roomJid - Room JID
- */
- onScrollToStoredPosition: function(roomJid) {
- // This should only apply when entering a room...
- // ... therefore we set scrollPosition to -1 after execution.
- if(self.Chat.rooms[roomJid].scrollPosition > -1) {
- var messagePane = self.Room.getPane(roomJid, '.message-pane-wrapper');
- messagePane.scrollTop(self.Chat.rooms[roomJid].scrollPosition);
- self.Chat.rooms[roomJid].scrollPosition = -1;
- }
- },
-
- /** Function: setFocusToForm
- * Set focus to the message input field within the message form.
- *
- * Parameters:
- * (String) roomJid - Room JID
- */
- setFocusToForm: function(roomJid) {
- var pane = self.Room.getPane(roomJid, '.message-form');
- if (pane) {
- // IE8 will fail maybe, because the field isn't there yet.
- try {
- pane.children('.field')[0].focus();
- } catch(e) {
- // fail silently
- }
- }
- },
-
- /** Function: setUser
- * Sets or updates the current user in the specified room (called by <Candy.View.Pane.Roster.update>) and set specific informations
- * (roles and affiliations) on the room tab (chat-pane).
- *
- * Parameters:
- * (String) roomJid - Room in which the user is set to.
- * (Candy.Core.ChatUser) user - The user
- */
- setUser: function(roomJid, user) {
- self.Chat.rooms[roomJid].user = user;
- var roomPane = self.Room.getPane(roomJid),
- chatPane = $('#chat-pane');
-
- roomPane.attr('data-userjid', user.getJid());
- // Set classes based on user role / affiliation
- if(user.isModerator()) {
- if (user.getRole() === user.ROLE_MODERATOR) {
- chatPane.addClass('role-moderator');
- }
- if (user.getAffiliation() === user.AFFILIATION_OWNER) {
- chatPane.addClass('affiliation-owner');
- }
- } else {
- chatPane.removeClass('role-moderator affiliation-owner');
- }
- self.Chat.Context.init();
- },
-
- /** Function: getUser
- * Get the current user in the room specified with the jid
- *
- * Parameters:
- * (String) roomJid - Room of which the user should be returned from
- *
- * Returns:
- * (Candy.Core.ChatUser) - user
- */
- getUser: function(roomJid) {
- return self.Chat.rooms[roomJid].user;
- },
-
- /** Function: ignoreUser
- * Ignore specified user and add the ignore icon to the roster item of the user
- *
- * Parameters:
- * (String) roomJid - Room in which the user should be ignored
- * (String) userJid - User which should be ignored
- */
- ignoreUser: function(roomJid, userJid) {
- Candy.Core.Action.Jabber.Room.IgnoreUnignore(userJid);
- Candy.View.Pane.Room.addIgnoreIcon(roomJid, userJid);
- },
-
- /** Function: unignoreUser
- * Unignore an ignored user and remove the ignore icon of the roster item.
- *
- * Parameters:
- * (String) roomJid - Room in which the user should be unignored
- * (String) userJid - User which should be unignored
- */
- unignoreUser: function(roomJid, userJid) {
- Candy.Core.Action.Jabber.Room.IgnoreUnignore(userJid);
- Candy.View.Pane.Room.removeIgnoreIcon(roomJid, userJid);
- },
-
- /** Function: addIgnoreIcon
- * Add the ignore icon to the roster item of the specified user
- *
- * Parameters:
- * (String) roomJid - Room in which the roster item should be updated
- * (String) userJid - User of which the roster item should be updated
- */
- addIgnoreIcon: function(roomJid, userJid) {
- if (Candy.View.Pane.Chat.rooms[userJid]) {
- $('#user-' + Candy.View.Pane.Chat.rooms[userJid].id + '-' + Candy.Util.jidToId(userJid)).addClass('status-ignored');
- }
- if (Candy.View.Pane.Chat.rooms[Strophe.getBareJidFromJid(roomJid)]) {
- $('#user-' + Candy.View.Pane.Chat.rooms[Strophe.getBareJidFromJid(roomJid)].id + '-' + Candy.Util.jidToId(userJid)).addClass('status-ignored');
- }
- },
-
- /** Function: removeIgnoreIcon
- * Remove the ignore icon to the roster item of the specified user
- *
- * Parameters:
- * (String) roomJid - Room in which the roster item should be updated
- * (String) userJid - User of which the roster item should be updated
- */
- removeIgnoreIcon: function(roomJid, userJid) {
- if (Candy.View.Pane.Chat.rooms[userJid]) {
- $('#user-' + Candy.View.Pane.Chat.rooms[userJid].id + '-' + Candy.Util.jidToId(userJid)).removeClass('status-ignored');
- }
- if (Candy.View.Pane.Chat.rooms[Strophe.getBareJidFromJid(roomJid)]) {
- $('#user-' + Candy.View.Pane.Chat.rooms[Strophe.getBareJidFromJid(roomJid)].id + '-' + Candy.Util.jidToId(userJid)).removeClass('status-ignored');
- }
- },
-
- /** Function: getPane
- * Get the chat room pane or a subPane of it (if subPane is specified)
- *
- * Parameters:
- * (String) roomJid - Room in which the pane lies
- * (String) subPane - Sub pane of the chat room pane if needed [optional]
- */
- getPane: function(roomJid, subPane) {
- if (self.Chat.rooms[roomJid]) {
- if(subPane) {
- if(self.Chat.rooms[roomJid]['pane-' + subPane]) {
- return self.Chat.rooms[roomJid]['pane-' + subPane];
- } else {
- self.Chat.rooms[roomJid]['pane-' + subPane] = $('#chat-room-' + self.Chat.rooms[roomJid].id).find(subPane);
- return self.Chat.rooms[roomJid]['pane-' + subPane];
- }
- } else {
- return $('#chat-room-' + self.Chat.rooms[roomJid].id);
- }
- }
- },
-
- /** Function: changeDataUserJidIfUserIsMe
- * Changes the room's data-userjid attribute if the specified user is the current user.
- *
- * Parameters:
- * (String) roomId - Id of the room
- * (Candy.Core.ChatUser) user - User
- */
- changeDataUserJidIfUserIsMe: function(roomId, user) {
- if (user.getNick() === Candy.Core.getUser().getNick()) {
- var roomElement = $('#chat-room-' + roomId);
- roomElement.attr('data-userjid', Strophe.getBareJidFromJid(roomElement.attr('data-userjid')) + '/' + user.getNick());
- }
- }
- };
-
- /** Class: Candy.View.Pane.PrivateRoom
- * Private room handling
- */
- self.PrivateRoom = {
- /** Function: open
- * Opens a new private room
- *
- * Parameters:
- * (String) roomJid - Room jid to open
- * (String) roomName - Room name
- * (Boolean) switchToRoom - If true, displayed room switches automatically to this room
- * (e.g. when user clicks itself on another user to open a private chat)
- * (Boolean) isNoConferenceRoomJid - true 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 param addresses this case.
- *
- * Triggers:
- * candy:view.private-room.after-open using {roomJid, type, element}
- */
- open: function(roomJid, roomName, switchToRoom, isNoConferenceRoomJid) {
- var user = isNoConferenceRoomJid ? Candy.Core.getUser() : self.Room.getUser(Strophe.getBareJidFromJid(roomJid)),
- evtData = {
- 'roomJid': roomJid,
- 'roomName': roomName,
- 'type': 'chat',
- };
-
- /** Event: candy:view.private-room.before-open
- * Before opening a new private room
- *
- * Parameters:
- * (String) roomJid - Room JID
- * (String) roomName - Room name
- * (String) type - 'chat'
- *
- * Returns:
- * Boolean - if you don't want to open the private room, return false
- */
- if($(Candy).triggerHandler('candy:view.private-room.before-open', evtData) === false) {
- return false;
- }
-
- // if target user is in privacy list, don't open the private chat.
- if (Candy.Core.getUser().isInPrivacyList('ignore', roomJid)) {
- return false;
- }
- if(!self.Chat.rooms[roomJid]) {
- if(self.Room.init(roomJid, roomName, 'chat') === false) {
- return false;
- }
- }
- if(switchToRoom) {
- self.Room.show(roomJid);
- }
-
- self.Roster.update(roomJid, new Candy.Core.ChatUser(roomJid, roomName), 'join', user);
- self.Roster.update(roomJid, user, 'join', user);
- self.PrivateRoom.setStatus(roomJid, 'join');
-
-
-
- // We can't track the presence of a user if it's not a conference jid
- if(isNoConferenceRoomJid) {
- self.Chat.infoMessage(roomJid, $.i18n._('presenceUnknownWarningSubject'), $.i18n._('presenceUnknownWarning'));
- }
-
- evtData.element = self.Room.getPane(roomJid);
- /** Event: candy:view.private-room.after-open
- * After opening a new private room
- *
- * Parameters:
- * (String) roomJid - Room JID
- * (String) type - 'chat'
- * (jQuery.Element) element - User element
- */
- $(Candy).triggerHandler('candy:view.private-room.after-open', evtData);
- },
-
- /** Function: setStatus
- * Set offline or online status for private rooms (when one of the participants leaves the room)
- *
- * Parameters:
- * (String) roomJid - Private room jid
- * (String) status - "leave"/"join"
- */
- setStatus: function(roomJid, status) {
- var messageForm = self.Room.getPane(roomJid, '.message-form');
- if(status === 'join') {
- self.Chat.getTab(roomJid).addClass('online').removeClass('offline');
-
- messageForm.children('.field').removeAttr('disabled');
- messageForm.children('.submit').removeAttr('disabled');
-
- self.Chat.getTab(roomJid);
- } else if(status === 'leave') {
- self.Chat.getTab(roomJid).addClass('offline').removeClass('online');
-
- messageForm.children('.field').attr('disabled', true);
- messageForm.children('.submit').attr('disabled', true);
- }
- },
-
- /** Function: changeNick
- * Changes the nick for every private room opened with this roomJid.
- *
- * Parameters:
- * (String) roomJid - Public room jid
- * (Candy.Core.ChatUser) user - User which changes his nick
- */
- changeNick: function changeNick(roomJid, user) {
- Candy.Core.log('[View:Pane:PrivateRoom] changeNick');
-
- var previousPrivateRoomJid = roomJid + '/' + user.getPreviousNick(),
- newPrivateRoomJid = roomJid + '/' + user.getNick(),
- previousPrivateRoomId = Candy.Util.jidToId(previousPrivateRoomJid),
- newPrivateRoomId = Candy.Util.jidToId(newPrivateRoomJid),
- room = self.Chat.rooms[previousPrivateRoomJid],
- roomElement,
- roomTabElement;
-
- // it could happen that the new private room is already existing -> close it first.
- // if this is not done, errors appear as two rooms would have the same id
- if (self.Chat.rooms[newPrivateRoomJid]) {
- self.Room.close(newPrivateRoomJid);
- }
-
- if (room) { /* someone I talk with, changed nick */
- room.name = user.getNick();
- room.id = newPrivateRoomId;
-
- self.Chat.rooms[newPrivateRoomJid] = room;
- delete self.Chat.rooms[previousPrivateRoomJid];
-
- roomElement = $('#chat-room-' + previousPrivateRoomId);
- if (roomElement) {
- roomElement.attr('data-roomjid', newPrivateRoomJid);
- roomElement.attr('id', 'chat-room-' + newPrivateRoomId);
-
- roomTabElement = $('#chat-tabs li[data-roomjid="' + previousPrivateRoomJid + '"]');
- roomTabElement.attr('data-roomjid', newPrivateRoomJid);
-
- /* TODO: The '@' is defined in the template. Somehow we should
- * extract both things into our CSS or do something else to prevent that.
- */
- roomTabElement.children('a.label').text('@' + user.getNick());
-
- if (Candy.View.getCurrent().roomJid === previousPrivateRoomJid) {
- Candy.View.getCurrent().roomJid = newPrivateRoomJid;
- }
- }
- } else { /* I changed the nick */
- roomElement = $('.room-pane.roomtype-chat[data-userjid="' + previousPrivateRoomJid + '"]');
- if (roomElement.length) {
- previousPrivateRoomId = Candy.Util.jidToId(roomElement.attr('data-roomjid'));
- roomElement.attr('data-userjid', newPrivateRoomJid);
- }
- }
- if (roomElement && roomElement.length) {
- self.Roster.changeNick(previousPrivateRoomId, user);
- }
- }
- };
-
- /** Class Candy.View.Pane.Roster
- * Handles everyhing regarding roster updates.
- */
- self.Roster = {
- /** Function: update
- * Called by <Candy.View.Observer.Presence.update> to update the roster if needed.
- * Adds/removes users from the roster list or updates informations on their items (roles, affiliations etc.)
- *
- * TODO: Refactoring, this method has too much LOC.
- *
- * Parameters:
- * (String) roomJid - Room JID in which the update happens
- * (Candy.Core.ChatUser) user - User on which the update happens
- * (String) action - one of "join", "leave", "kick" and "ban"
- * (Candy.Core.ChatUser) currentUser - Current user
- *
- * Triggers:
- * candy:view.roster.before-update using {roomJid, user, action, element}
- * candy:view.roster.after-update using {roomJid, user, action, element}
- */
- update: function(roomJid, user, action, currentUser) {
- Candy.Core.log('[View:Pane:Roster] ' + action);
- var roomId = self.Chat.rooms[roomJid].id,
- userId = Candy.Util.jidToId(user.getJid()),
- usercountDiff = -1,
- userElem = $('#user-' + roomId + '-' + userId),
- evtData = {
- 'roomJid' : roomJid,
- 'user' : user,
- 'action': action,
- 'element': userElem
- };
-
- /** Event: candy:view.roster.before-update
- * Before updating the roster of a room
- *
- * Parameters:
- * (String) roomJid - Room JID
- * (Candy.Core.ChatUser) user - User
- * (String) action - [join, leave, kick, ban]
- * (jQuery.Element) element - User element
- */
- $(Candy).triggerHandler('candy:view.roster.before-update', evtData);
-
- // a user joined the room
- if(action === 'join') {
- usercountDiff = 1;
- var html = Mustache.to_html(Candy.View.Template.Roster.user, {
- roomId: roomId,
- userId : userId,
- userJid: user.getJid(),
- nick: user.getNick(),
- displayNick: Candy.Util.crop(user.getNick(), Candy.View.getOptions().crop.roster.nickname),
- role: user.getRole(),
- affiliation: user.getAffiliation(),
- me: currentUser !== undefined && user.getNick() === currentUser.getNick(),
- tooltipRole: $.i18n._('tooltipRole'),
- tooltipIgnored: $.i18n._('tooltipIgnored')
- });
-
- if(userElem.length < 1) {
- var userInserted = false,
- rosterPane = self.Room.getPane(roomJid, '.roster-pane');
-
- // there are already users in the roster
- if(rosterPane.children().length > 0) {
- // insert alphabetically
- var userSortCompare = user.getNick().toUpperCase();
- rosterPane.children().each(function() {
- var elem = $(this);
- if(elem.attr('data-nick').toUpperCase() > userSortCompare) {
- elem.before(html);
- userInserted = true;
- return false;
- }
- return true;
- });
- }
- // first user in roster
- if(!userInserted) {
- rosterPane.append(html);
- }
-
- self.Roster.showJoinAnimation(user, userId, roomId, roomJid, currentUser);
- // user is in room but maybe the affiliation/role has changed
- } else {
- usercountDiff = 0;
- userElem.replaceWith(html);
- $('#user-' + roomId + '-' + userId).css({opacity: 1}).show();
- // it's me, update the toolbar
- if(currentUser !== undefined && user.getNick() === currentUser.getNick() && self.Room.getUser(roomJid)) {
- self.Chat.Toolbar.update(roomJid);
- }
- }
-
- // Presence of client
- if (currentUser !== undefined && currentUser.getNick() === user.getNick()) {
- self.Room.setUser(roomJid, user);
- // add click handler for private chat
- } else {
- $('#user-' + roomId + '-' + userId).click(self.Roster.userClick);
- }
-
- $('#user-' + roomId + '-' + userId + ' .context').click(function(e) {
- self.Chat.Context.show(e.currentTarget, roomJid, user);
- e.stopPropagation();
- });
-
- // check if current user is ignoring the user who has joined.
- if (currentUser !== undefined && currentUser.isInPrivacyList('ignore', user.getJid())) {
- Candy.View.Pane.Room.addIgnoreIcon(roomJid, user.getJid());
- }
- // a user left the room
- } else if(action === 'leave') {
- self.Roster.leaveAnimation('user-' + roomId + '-' + userId);
- // always show leave message in private room, even if status messages have been disabled
- if (self.Chat.rooms[roomJid].type === 'chat') {
- self.Chat.onInfoMessage(roomJid, $.i18n._('userLeftRoom', [user.getNick()]));
- } else {
- self.Chat.infoMessage(roomJid, $.i18n._('userLeftRoom', [user.getNick()]));
- }
-
- } else if(action === 'nickchange') {
- usercountDiff = 0;
- self.Roster.changeNick(roomId, user);
- self.Room.changeDataUserJidIfUserIsMe(roomId, user);
- self.PrivateRoom.changeNick(roomJid, user);
- var infoMessage = $.i18n._('userChangedNick', [user.getPreviousNick(), user.getNick()]);
- self.Chat.onInfoMessage(roomJid, infoMessage);
- // user has been kicked
- } else if(action === 'kick') {
- self.Roster.leaveAnimation('user-' + roomId + '-' + userId);
- self.Chat.onInfoMessage(roomJid, $.i18n._('userHasBeenKickedFromRoom', [user.getNick()]));
- // user has been banned
- } else if(action === 'ban') {
- self.Roster.leaveAnimation('user-' + roomId + '-' + userId);
- self.Chat.onInfoMessage(roomJid, $.i18n._('userHasBeenBannedFromRoom', [user.getNick()]));
- }
-
- // Update user count
- Candy.View.Pane.Chat.rooms[roomJid].usercount += usercountDiff;
-
- if(roomJid === Candy.View.getCurrent().roomJid) {
- Candy.View.Pane.Chat.Toolbar.updateUsercount(Candy.View.Pane.Chat.rooms[roomJid].usercount);
- }
-
-
- // in case there's been a join, the element is now there (previously not)
- evtData.element = $('#user-' + roomId + '-' + userId);
- /** Event: candy:view.roster.after-update
- * After updating a room's roster
- *
- * Parameters:
- * (String) roomJid - Room JID
- * (Candy.Core.ChatUser) user - User
- * (String) action - [join, leave, kick, ban]
- * (jQuery.Element) element - User element
- */
- $(Candy).triggerHandler('candy:view.roster.after-update', evtData);
- },
-
- /** Function: userClick
- * Click handler for opening a private room
- */
- userClick: function() {
- var elem = $(this);
- self.PrivateRoom.open(elem.attr('data-jid'), elem.attr('data-nick'), true);
- },
-
- /** Function: showJoinAnimation
- * Shows join animation if needed
- *
- * FIXME: Refactor. Part of this will be done by the big room improvements
- */
- showJoinAnimation: function(user, userId, roomId, roomJid, currentUser) {
- // don't show if the user has recently changed the nickname.
- var rosterUserId = 'user-' + roomId + '-' + userId,
- $rosterUserElem = $('#' + rosterUserId);
- if (!user.getPreviousNick() || !$rosterUserElem || $rosterUserElem.is(':visible') === false) {
- self.Roster.joinAnimation(rosterUserId);
- // only show other users joining & don't show if there's no message in the room.
- if(currentUser !== undefined && user.getNick() !== currentUser.getNick() && self.Room.getUser(roomJid)) {
- // always show join message in private room, even if status messages have been disabled
- if (self.Chat.rooms[roomJid].type === 'chat') {
- self.Chat.onInfoMessage(roomJid, $.i18n._('userJoinedRoom', [user.getNick()]));
- } else {
- self.Chat.infoMessage(roomJid, $.i18n._('userJoinedRoom', [user.getNick()]));
- }
- }
- }
- },
-
- /** Function: joinAnimation
- * Animates specified elementId on join
- *
- * Parameters:
- * (String) elementId - Specific element to do the animation on
- */
- joinAnimation: function(elementId) {
- $('#' + elementId).stop(true).slideDown('normal', function() {
- $(this).animate({opacity: 1});
- });
- },
-
- /** Function: leaveAnimation
- * Leave animation for specified element id and removes the DOM element on completion.
- *
- * Parameters:
- * (String) elementId - Specific element to do the animation on
- */
- leaveAnimation: function(elementId) {
- $('#' + elementId).stop(true).attr('id', '#' + elementId + '-leaving').animate({opacity: 0}, {
- complete: function() {
- $(this).slideUp('normal', function() {
- $(this).remove();
- });
- }
- });
- },
-
- /** Function: changeNick
- * Change nick of an existing user in the roster
- *
- * UserId has to be recalculated from the user because at the time of this call,
- * the user is already set with the new jid & nick.
- *
- * Parameters:
- * (String) roomId - Id of the room
- * (Candy.Core.ChatUser) user - User object
- */
- changeNick: function(roomId, user) {
- Candy.Core.log('[View:Pane:Roster] changeNick');
- var previousUserJid = Strophe.getBareJidFromJid(user.getJid()) + '/' + user.getPreviousNick(),
- elementId = 'user-' + roomId + '-' + Candy.Util.jidToId(previousUserJid),
- el = $('#' + elementId);
-
- el.attr('data-nick', user.getNick());
- el.attr('data-jid', user.getJid());
- el.children('div.label').text(user.getNick());
- el.attr('id', 'user-' + roomId + '-' + Candy.Util.jidToId(user.getJid()));
- }
- };
-
- /** Class: Candy.View.Pane.Message
- * Message submit/show handling
- */
- self.Message = {
- /** Function: submit
- * on submit handler for message field sends the message to the server and if it's a private chat, shows the message
- * immediately because the server doesn't send back those message.
- *
- * Parameters:
- * (Event) event - Triggered event
- *
- * Triggers:
- * candy:view.message.before-send using {message}
- *
- * FIXME: as everywhere, `roomJid` might be slightly incorrect in this case
- * - maybe rename this as part of a refactoring.
- */
- submit: function(event) {
- var roomJid = Candy.View.getCurrent().roomJid,
- roomType = Candy.View.Pane.Chat.rooms[roomJid].type,
- message = $(this).children('.field').val().substring(0, Candy.View.getOptions().crop.message.body),
- xhtmlMessage,
- evtData = {
- roomJid: roomJid,
- message: message,
- xhtmlMessage: xhtmlMessage
- };
-
- /** Event: candy:view.message.before-send
- * Before sending a message
- *
- * Parameters:
- * (String) roomJid - room to which the message should be sent
- * (String) message - Message text
- * (String) xhtmlMessage - XHTML formatted message [default: undefined]
- *
- * Returns:
- * Boolean|undefined - if you like to stop sending the message, return false.
- */
- if($(Candy).triggerHandler('candy:view.message.before-send', evtData) === false) {
- event.preventDefault();
- return;
- }
-
- message = evtData.message;
- xhtmlMessage = evtData.xhtmlMessage;
-
- Candy.Core.Action.Jabber.Room.Message(roomJid, message, roomType, xhtmlMessage);
- // Private user chat. Jabber won't notify the user who has sent the message. Just show it as the user hits the button...
- if(roomType === 'chat' && message) {
- self.Message.show(roomJid, self.Room.getUser(roomJid).getNick(), message);
- }
- // Clear input and set focus to it
- $(this).children('.field').val('').focus();
- event.preventDefault();
- },
-
- /** Function: show
- * Show a message in the message pane
- *
- * Parameters:
- * (String) roomJid - room in which the message has been sent to
- * (String) name - Name of the user which sent the message
- * (String) message - Message
- * (String) xhtmlMessage - XHTML formatted message [if options enableXHTML is true]
- * (String) timestamp - [optional] Timestamp of the message, if not present, current date.
- *
- * Triggers:
- * candy:view.message.before-show using {roomJid, name, message}
- * candy.view.message.before-render using {template, templateData}
- * candy:view.message.after-show using {roomJid, name, message, element}
- */
- show: function(roomJid, name, message, xhtmlMessage, timestamp) {
- message = Candy.Util.Parser.all(message.substring(0, Candy.View.getOptions().crop.message.body));
- if(xhtmlMessage) {
- xhtmlMessage = Candy.Util.parseAndCropXhtml(xhtmlMessage, Candy.View.getOptions().crop.message.body);
- }
-
- var evtData = {
- 'roomJid': roomJid,
- 'name': name,
- 'message': message,
- 'xhtmlMessage': xhtmlMessage
- };
-
- /** Event: candy:view.message.before-show
- * Before showing a new message
- *
- * Parameters:
- * (String) roomJid - Room JID
- * (String) name - Name of the sending user
- * (String) message - Message text
- *
- * Returns:
- * Boolean - if you don't want to show the message, return false
- */
- if($(Candy).triggerHandler('candy:view.message.before-show', evtData) === false) {
- return;
- }
-
- message = evtData.message;
- xhtmlMessage = evtData.xhtmlMessage;
- if(xhtmlMessage !== undefined && xhtmlMessage.length > 0) {
- message = xhtmlMessage;
- }
-
- if(!message) {
- return;
- }
-
- var renderEvtData = {
- template: Candy.View.Template.Message.item,
- templateData: {
- name: name,
- displayName: Candy.Util.crop(name, Candy.View.getOptions().crop.message.nickname),
- message: message,
- time: Candy.Util.localizedTime(timestamp || new Date().toGMTString())
- }
- };
-
- /** Event: candy:view.message.before-render
- * Before rendering the message element
- *
- * Parameters:
- * (String) template - Template to use
- * (Object) templateData - Template data consists of:
- * - (String) name - Name of the sending user
- * - (String) displayName - Cropped name of the sending user
- * - (String) message - Message text
- * - (String) time - Localized time
- */
- $(Candy).triggerHandler('candy:view.message.before-render', renderEvtData);
-
- var html = Mustache.to_html(renderEvtData.template, renderEvtData.templateData);
- self.Room.appendToMessagePane(roomJid, html);
- var elem = self.Room.getPane(roomJid, '.message-pane').children().last();
- // click on username opens private chat
- elem.find('a.label').click(function(event) {
- event.preventDefault();
- // Check if user is online and not myself
- var room = Candy.Core.getRoom(roomJid);
- if(room && name !== self.Room.getUser(Candy.View.getCurrent().roomJid).getNick() && room.getRoster().get(roomJid + '/' + name)) {
- if(Candy.View.Pane.PrivateRoom.open(roomJid + '/' + name, name, true) === false) {
- return false;
- }
- }
- });
-
- // Notify the user about a new private message
- if(Candy.View.getCurrent().roomJid !== roomJid || !self.Window.hasFocus()) {
- self.Chat.increaseUnreadMessages(roomJid);
- if(Candy.View.Pane.Chat.rooms[roomJid].type === 'chat' && !self.Window.hasFocus()) {
- self.Chat.Toolbar.playSound();
- }
- }
- if(Candy.View.getCurrent().roomJid === roomJid) {
- self.Room.scrollToBottom(roomJid);
- }
-
- evtData.element = elem;
-
- /** Event: candy:view.message.after-show
- * Triggered after showing a message
- *
- * Parameters:
- * (String) roomJid - Room JID
- * (jQuery.Element) element - User element
- * (String) name - Name of the sending user
- * (String) message - Message text
- */
- $(Candy).triggerHandler('candy:view.message.after-show', evtData);
- }
- };
-
- return self;
-}(Candy.View.Pane || {}, jQuery));
diff --git a/src/view/pane/chat.js b/src/view/pane/chat.js
new file mode 100644
index 0000000..61d7692
--- /dev/null
+++ b/src/view/pane/chat.js
@@ -0,0 +1,1064 @@
+/** File: chat.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, document, Mustache, Strophe, Audio, jQuery */
+
+/** Class: Candy.View.Pane
+ * Candy view pane handles everything regarding DOM updates etc.
+ *
+ * Parameters:
+ * (Candy.View.Pane) self - itself
+ * (jQuery) $ - jQuery
+ */
+Candy.View.Pane = (function(self, $) {
+
+ /** Class: Candy.View.Pane.Chat
+ * Chat-View related view updates
+ */
+ self.Chat = {
+ /** Variable: rooms
+ * Contains opened room elements
+ */
+ rooms: [],
+
+ /** Function: addTab
+ * Add a tab to the chat pane.
+ *
+ * Parameters:
+ * (String) roomJid - JID of room
+ * (String) roomName - Tab label
+ * (String) roomType - Type of room: `groupchat` or `chat`
+ */
+ addTab: function(roomJid, roomName, roomType) {
+ var roomId = Candy.Util.jidToId(roomJid);
+
+ var evtData = {
+ roomJid: roomJid,
+ roomName: roomName,
+ roomType: roomType,
+ roomId: roomId
+ };
+
+ /** Event: candy:view.pane.before-tab
+ * Before sending a message
+ *
+ * Parameters:
+ * (String) roomJid - JID of the room the tab is for.
+ * (String) roomName - Name of the room.
+ * (String) roomType - What type of room: `groupchat` or `chat`
+ *
+ * Returns:
+ * Boolean|undefined - If you want to handle displaying the tab on your own, return false.
+ */
+ if ($(Candy).triggerHandler('candy:view.pane.before-tab', evtData) === false) {
+ event.preventDefault();
+ return;
+ }
+
+ var html = Mustache.to_html(Candy.View.Template.Chat.tab, {
+ roomJid: roomJid,
+ roomId: roomId,
+ name: roomName || Strophe.getNodeFromJid(roomJid),
+ privateUserChat: function() {return roomType === 'chat';},
+ roomType: roomType
+ }),
+ tab = $(html).appendTo('#chat-tabs');
+
+ tab.click(self.Chat.tabClick);
+ // TODO: maybe we find a better way to get the close element.
+ $('a.close', tab).click(self.Chat.tabClose);
+
+ self.Chat.fitTabs();
+ },
+
+ /** Function: getTab
+ * Get tab by JID.
+ *
+ * Parameters:
+ * (String) roomJid - JID of room
+ *
+ * Returns:
+ * (jQuery object) - Tab element
+ */
+ getTab: function(roomJid) {
+ return $('#chat-tabs').children('li[data-roomjid="' + roomJid + '"]');
+ },
+
+ /** Function: removeTab
+ * Remove tab element.
+ *
+ * Parameters:
+ * (String) roomJid - JID of room
+ */
+ removeTab: function(roomJid) {
+ self.Chat.getTab(roomJid).remove();
+ self.Chat.fitTabs();
+ },
+
+ /** Function: setActiveTab
+ * Set the active tab.
+ *
+ * Add CSS classname `active` to the choosen tab and remove `active` from all other.
+ *
+ * Parameters:
+ * (String) roomJid - JID of room
+ */
+ setActiveTab: function(roomJid) {
+ $('#chat-tabs').children().each(function() {
+ var tab = $(this);
+ if(tab.attr('data-roomjid') === roomJid) {
+ tab.addClass('active');
+ } else {
+ tab.removeClass('active');
+ }
+ });
+ },
+
+ /** Function: increaseUnreadMessages
+ * Increase unread message count in a tab by one.
+ *
+ * Parameters:
+ * (String) roomJid - JID of room
+ *
+ * Uses:
+ * - <Window.increaseUnreadMessages>
+ */
+ increaseUnreadMessages: function(roomJid) {
+ var unreadElem = this.getTab(roomJid).find('.unread');
+ unreadElem.show().text(unreadElem.text() !== '' ? parseInt(unreadElem.text(), 10) + 1 : 1);
+ // only increase window unread messages in private chats
+ if (self.Chat.rooms[roomJid].type === 'chat' || Candy.View.getOptions().updateWindowOnAllMessages === true) {
+ self.Window.increaseUnreadMessages();
+ }
+ },
+
+ /** Function: clearUnreadMessages
+ * Clear unread message count in a tab.
+ *
+ * Parameters:
+ * (String) roomJid - JID of room
+ *
+ * Uses:
+ * - <Window.reduceUnreadMessages>
+ */
+ clearUnreadMessages: function(roomJid) {
+ var unreadElem = self.Chat.getTab(roomJid).find('.unread');
+ self.Window.reduceUnreadMessages(unreadElem.text());
+ unreadElem.hide().text('');
+ },
+
+ /** Function: tabClick
+ * Tab click event: show the room associated with the tab and stops the event from doing the default.
+ */
+ tabClick: function(e) {
+ // remember scroll position of current room
+ var currentRoomJid = Candy.View.getCurrent().roomJid;
+ var roomPane = self.Room.getPane(currentRoomJid, '.message-pane');
+ if (roomPane) {
+ self.Chat.rooms[currentRoomJid].scrollPosition = roomPane.scrollTop();
+ }
+
+ self.Room.show($(this).attr('data-roomjid'));
+ e.preventDefault();
+ },
+
+ /** Function: tabClose
+ * Tab close (click) event: Leave the room (groupchat) or simply close the tab (chat).
+ *
+ * Parameters:
+ * (DOMEvent) e - Event triggered
+ *
+ * Returns:
+ * (Boolean) - false, this will stop the event from bubbling
+ */
+ tabClose: function() {
+ var roomJid = $(this).parent().attr('data-roomjid');
+ // close private user tab
+ if(self.Chat.rooms[roomJid].type === 'chat') {
+ self.Room.close(roomJid);
+ // close multi-user room tab
+ } else {
+ Candy.Core.Action.Jabber.Room.Leave(roomJid);
+ }
+ return false;
+ },
+
+ /** Function: allTabsClosed
+ * All tabs closed event: Disconnect from service. Hide sound control.
+ *
+ * TODO: Handle window close
+ *
+ * Returns:
+ * (Boolean) - false, this will stop the event from bubbling
+ */
+ allTabsClosed: function() {
+ if (Candy.Core.getOptions().disconnectWithoutTabs) {
+ Candy.Core.disconnect();
+ self.Chat.Toolbar.hide();
+ return;
+ }
+ },
+
+ /** Function: fitTabs
+ * Fit tab size according to window size
+ */
+ fitTabs: function() {
+ var availableWidth = $('#chat-tabs').innerWidth(),
+ tabsWidth = 0,
+ tabs = $('#chat-tabs').children();
+ tabs.each(function() {
+ tabsWidth += $(this).css({width: 'auto', overflow: 'visible'}).outerWidth(true);
+ });
+ if(tabsWidth > availableWidth) {
+ // tabs.[outer]Width() measures the first element in `tabs`. It's no very readable but nearly two times faster than using :first
+ var tabDiffToRealWidth = tabs.outerWidth(true) - tabs.width(),
+ tabWidth = Math.floor((availableWidth) / tabs.length) - tabDiffToRealWidth;
+ tabs.css({width: tabWidth, overflow: 'hidden'});
+ }
+ },
+
+ /** Function: adminMessage
+ * Display admin message
+ *
+ * Parameters:
+ * (String) subject - Admin message subject
+ * (String) message - Message to be displayed
+ *
+ * Triggers:
+ * candy:view.chat.admin-message using {subject, message}
+ */
+ adminMessage: function(subject, message) {
+ if(Candy.View.getCurrent().roomJid) { // Simply dismiss admin message if no room joined so far. TODO: maybe we should show those messages on a dedicated pane?
+ message = Candy.Util.Parser.all(message.substring(0, Candy.View.getOptions().crop.message.body));
+ if(Candy.View.getOptions().enableXHTML === true) {
+ message = Candy.Util.parseAndCropXhtml(message, Candy.View.getOptions().crop.message.body);
+ }
+ var timestamp = new Date();
+ var html = Mustache.to_html(Candy.View.Template.Chat.adminMessage, {
+ subject: subject,
+ message: message,
+ sender: $.i18n._('administratorMessageSubject'),
+ time: Candy.Util.localizedTime(timestamp),
+ timestamp: timestamp.toISOString()
+ });
+ $('#chat-rooms').children().each(function() {
+ self.Room.appendToMessagePane($(this).attr('data-roomjid'), html);
+ });
+ self.Room.scrollToBottom(Candy.View.getCurrent().roomJid);
+
+ /** Event: candy:view.chat.admin-message
+ * After admin message display
+ *
+ * Parameters:
+ * (String) presetJid - Preset user JID
+ */
+ $(Candy).triggerHandler('candy:view.chat.admin-message', {
+ 'subject' : subject,
+ 'message' : message
+ });
+ }
+ },
+
+ /** Function: infoMessage
+ * Display info message. This is a wrapper for <onInfoMessage> to be able to disable certain info messages.
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ * (String) subject - Subject
+ * (String) message - Message
+ */
+ infoMessage: function(roomJid, subject, message) {
+ self.Chat.onInfoMessage(roomJid, subject, message);
+ },
+
+ /** Function: onInfoMessage
+ * Display info message. Used by <infoMessage> and several other functions which do not wish that their info message
+ * can be disabled (such as kick/ban message or leave/join message in private chats).
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ * (String) subject - Subject
+ * (String) message - Message
+ */
+ onInfoMessage: function(roomJid, subject, message) {
+ message = message || '';
+ if(Candy.View.getCurrent().roomJid && self.Chat.rooms[roomJid]) { // Simply dismiss info message if no room joined so far. TODO: maybe we should show those messages on a dedicated pane?
+ message = Candy.Util.Parser.all(message.substring(0, Candy.View.getOptions().crop.message.body));
+ if(Candy.View.getOptions().enableXHTML === true) {
+ message = Candy.Util.parseAndCropXhtml(message, Candy.View.getOptions().crop.message.body);
+ }
+ var timestamp = new Date();
+ var html = Mustache.to_html(Candy.View.Template.Chat.infoMessage, {
+ subject: subject,
+ message: $.i18n._(message),
+ time: Candy.Util.localizedTime(timestamp),
+ timestamp: timestamp.toISOString()
+ });
+ self.Room.appendToMessagePane(roomJid, html);
+ if (Candy.View.getCurrent().roomJid === roomJid) {
+ self.Room.scrollToBottom(Candy.View.getCurrent().roomJid);
+ }
+ }
+ },
+
+ /** Class: Candy.View.Pane.Toolbar
+ * Chat toolbar for things like emoticons toolbar, room management etc.
+ */
+ Toolbar: {
+ _supportsNativeAudio: null,
+
+ /** Function: init
+ * Register handler and enable or disable sound and status messages.
+ */
+ init: function() {
+ $('#emoticons-icon').click(function(e) {
+ self.Chat.Context.showEmoticonsMenu(e.currentTarget);
+ e.stopPropagation();
+ });
+ $('#chat-autoscroll-control').click(self.Chat.Toolbar.onAutoscrollControlClick);
+ try {
+ if( !!document.createElement('audio').canPlayType ) {
+ var a = document.createElement('audio');
+ if( !!(a.canPlayType('audio/mpeg;').replace(/no/, '')) ) {
+ self.Chat.Toolbar._supportsNativeAudio = "mp3";
+ }
+ else if( !!(a.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, '')) ) {
+ self.Chat.Toolbar._supportsNativeAudio = "ogg";
+ }
+ else if ( !!(a.canPlayType('audio/mp4; codecs="mp4a.40.2"').replace(/no/, '')) ) {
+ self.Chat.Toolbar._supportsNativeAudio = "m4a";
+ }
+ }
+ } catch(e){ }
+ $('#chat-sound-control').click(self.Chat.Toolbar.onSoundControlClick);
+ if(Candy.Util.cookieExists('candy-nosound')) {
+ $('#chat-sound-control').click();
+ }
+ $('#chat-statusmessage-control').click(self.Chat.Toolbar.onStatusMessageControlClick);
+ if(Candy.Util.cookieExists('candy-nostatusmessages')) {
+ $('#chat-statusmessage-control').click();
+ }
+ },
+
+ /** Function: show
+ * Show toolbar.
+ */
+ show: function() {
+ $('#chat-toolbar').show();
+ },
+
+ /** Function: hide
+ * Hide toolbar.
+ */
+ hide: function() {
+ $('#chat-toolbar').hide();
+ },
+
+ /* Function: update
+ * Update toolbar for specific room
+ */
+ update: function(roomJid) {
+ var context = $('#chat-toolbar').find('.context'),
+ me = self.Room.getUser(roomJid);
+ if(!me || !me.isModerator()) {
+ context.hide();
+ } else {
+ context.show().click(function(e) {
+ self.Chat.Context.show(e.currentTarget, roomJid);
+ e.stopPropagation();
+ });
+ }
+ self.Chat.Toolbar.updateUsercount(self.Chat.rooms[roomJid].usercount);
+ },
+
+ /** Function: playSound
+ * Play sound (default method).
+ */
+ playSound: function() {
+ self.Chat.Toolbar.onPlaySound();
+ },
+
+ /** Function: onPlaySound
+ * Sound play event handler. Uses native (HTML5) audio if supported,
+ * otherwise it will attempt to use bgsound with autostart.
+ *
+ * Don't call this method directly. Call `playSound()` instead.
+ * `playSound()` will only call this method if sound is enabled.
+ */
+ onPlaySound: function() {
+ try {
+ if(self.Chat.Toolbar._supportsNativeAudio !== null) {
+ new Audio(Candy.View.getOptions().assets + 'notify.' + self.Chat.Toolbar._supportsNativeAudio).play();
+ } else {
+ $('#chat-sound-control bgsound').remove();
+ $('<bgsound/>').attr({ src: Candy.View.getOptions().assets + 'notify.mp3', loop: 1, autostart: true }).appendTo("#chat-sound-control");
+ }
+ } catch (e) {}
+ },
+
+ /** Function: onSoundControlClick
+ * Sound control click event handler.
+ *
+ * Toggle sound (overwrite `playSound()`) and handle cookies.
+ */
+ onSoundControlClick: function() {
+ var control = $('#chat-sound-control');
+ if(control.hasClass('checked')) {
+ self.Chat.Toolbar.playSound = function() {};
+ Candy.Util.setCookie('candy-nosound', '1', 365);
+ } else {
+ self.Chat.Toolbar.playSound = function() {
+ self.Chat.Toolbar.onPlaySound();
+ };
+ Candy.Util.deleteCookie('candy-nosound');
+ }
+ control.toggleClass('checked');
+ },
+
+ /** Function: onAutoscrollControlClick
+ * Autoscroll control event handler.
+ *
+ * Toggle autoscroll
+ */
+ onAutoscrollControlClick: function() {
+ var control = $('#chat-autoscroll-control');
+ if(control.hasClass('checked')) {
+ self.Room.scrollToBottom = function(roomJid) {
+ self.Room.onScrollToStoredPosition(roomJid);
+ };
+ self.Window.autoscroll = false;
+ } else {
+ self.Room.scrollToBottom = function(roomJid) {
+ self.Room.onScrollToBottom(roomJid);
+ };
+ self.Room.scrollToBottom(Candy.View.getCurrent().roomJid);
+ self.Window.autoscroll = true;
+ }
+ control.toggleClass('checked');
+ },
+
+ /** Function: onStatusMessageControlClick
+ * Status message control event handler.
+ *
+ * Toggle status message
+ */
+ onStatusMessageControlClick: function() {
+ var control = $('#chat-statusmessage-control');
+ if(control.hasClass('checked')) {
+ self.Chat.infoMessage = function() {};
+ Candy.Util.setCookie('candy-nostatusmessages', '1', 365);
+ } else {
+ self.Chat.infoMessage = function(roomJid, subject, message) {
+ self.Chat.onInfoMessage(roomJid, subject, message);
+ };
+ Candy.Util.deleteCookie('candy-nostatusmessages');
+ }
+ control.toggleClass('checked');
+ },
+
+ /** Function: updateUserCount
+ * Update usercount element with count.
+ *
+ * Parameters:
+ * (Integer) count - Current usercount
+ */
+ updateUsercount: function(count) {
+ $('#chat-usercount').text(count);
+ }
+ },
+
+ /** Class: Candy.View.Pane.Modal
+ * Modal window
+ */
+ Modal: {
+ /** Function: show
+ * Display modal window
+ *
+ * Parameters:
+ * (String) html - HTML code to put into the modal window
+ * (Boolean) showCloseControl - set to true if a close button should be displayed [default false]
+ * (Boolean) showSpinner - set to true if a loading spinner should be shown [default false]
+ * (String) modalClass - custom class (or space-separate classes) to attach to the modal
+ */
+ show: function(html, showCloseControl, showSpinner, modalClass) {
+ if(showCloseControl) {
+ self.Chat.Modal.showCloseControl();
+ } else {
+ self.Chat.Modal.hideCloseControl();
+ }
+ if(showSpinner) {
+ self.Chat.Modal.showSpinner();
+ } else {
+ self.Chat.Modal.hideSpinner();
+ }
+ // Reset classes to 'modal-common' only in case .show() is called
+ // with different arguments before .hide() can remove the last applied
+ // custom class
+ $('#chat-modal').removeClass().addClass('modal-common');
+ if( modalClass ) {
+ $('#chat-modal').addClass(modalClass);
+ }
+ $('#chat-modal').stop(false, true);
+ $('#chat-modal-body').html(html);
+ $('#chat-modal').fadeIn('fast');
+ $('#chat-modal-overlay').show();
+ },
+
+ /** Function: hide
+ * Hide modal window
+ *
+ * Parameters:
+ * (Function) callback - Calls the specified function after modal window has been hidden.
+ */
+ hide: function(callback) {
+ // Reset classes to include only `modal-common`.
+ $('#chat-modal').removeClass().addClass('modal-common');
+ $('#chat-modal').fadeOut('fast', function() {
+ $('#chat-modal-body').text('');
+ $('#chat-modal-overlay').hide();
+ });
+ // restore initial esc handling
+ $(document).keydown(function(e) {
+ if(e.which === 27) {
+ e.preventDefault();
+ }
+ });
+ if (callback) {
+ callback();
+ }
+ },
+
+ /** Function: showSpinner
+ * Show loading spinner
+ */
+ showSpinner: function() {
+ $('#chat-modal-spinner').show();
+ },
+
+ /** Function: hideSpinner
+ * Hide loading spinner
+ */
+ hideSpinner: function() {
+ $('#chat-modal-spinner').hide();
+ },
+
+ /** Function: showCloseControl
+ * Show a close button
+ */
+ showCloseControl: function() {
+ $('#admin-message-cancel').show().click(function(e) {
+ self.Chat.Modal.hide();
+ // some strange behaviour on IE7 (and maybe other browsers) triggers onWindowUnload when clicking on the close button.
+ // prevent this.
+ e.preventDefault();
+ });
+
+ // enable esc to close modal
+ $(document).keydown(function(e) {
+ if(e.which === 27) {
+ self.Chat.Modal.hide();
+ e.preventDefault();
+ }
+ });
+ },
+
+ /** Function: hideCloseControl
+ * Hide the close button
+ */
+ hideCloseControl: function() {
+ $('#admin-message-cancel').hide().click(function() {});
+ },
+
+ /** Function: showLoginForm
+ * Show the login form modal
+ *
+ * Parameters:
+ * (String) message - optional message to display above the form
+ * (String) presetJid - optional user jid. if set, the user will only be prompted for password.
+ */
+ showLoginForm: function(message, presetJid) {
+ var domains = Candy.Core.getOptions().domains;
+ var hideDomainList = Candy.Core.getOptions().hideDomainList;
+ domains = domains ? domains.map( function(d) {return {'domain':d};} )
+ : null;
+ var customClass = domains && !hideDomainList ? 'login-with-domains'
+ : null;
+ self.Chat.Modal.show((message ? message : '') + Mustache.to_html(Candy.View.Template.Login.form, {
+ _labelNickname: $.i18n._('labelNickname'),
+ _labelUsername: $.i18n._('labelUsername'),
+ domains: domains,
+ _labelPassword: $.i18n._('labelPassword'),
+ _loginSubmit: $.i18n._('loginSubmit'),
+ displayPassword: !Candy.Core.isAnonymousConnection(),
+ displayUsername: !presetJid,
+ displayDomain: domains ? true : false,
+ displayNickname: Candy.Core.isAnonymousConnection(),
+ presetJid: presetJid ? presetJid : false
+ }), null, null, customClass);
+ if(hideDomainList) {
+ $('#domain').hide();
+ $('.at-symbol').hide();
+ }
+ $('#login-form').children(':input:first').focus();
+
+ // register submit handler
+ $('#login-form').submit(function() {
+ var username = $('#username').val(),
+ password = $('#password').val(),
+ domain = $('#domain');
+ domain = domain.length ? domain.val().split(' ')[0] : null;
+
+ if (!Candy.Core.isAnonymousConnection()) {
+ var jid;
+ if(domain) { // domain is stipulated
+ // Ensure there is no domain part in username
+ username = username.split('@')[0];
+ jid = username + '@' + domain;
+ } else { // domain not stipulated
+ // guess the input and create a jid out of it
+ jid = Candy.Core.getUser() && username.indexOf("@") < 0 ?
+ username + '@' + Strophe.getDomainFromJid(Candy.Core.getUser().getJid()) : username;
+ }
+
+ if(jid.indexOf("@") < 0 && !Candy.Core.getUser()) {
+ Candy.View.Pane.Chat.Modal.showLoginForm($.i18n._('loginInvalid'));
+ } else {
+ //Candy.View.Pane.Chat.Modal.hide();
+ Candy.Core.connect(jid, password);
+ }
+ } else { // anonymous login
+ Candy.Core.connect(presetJid, null, username);
+ }
+ return false;
+ });
+ },
+
+ /** Function: showEnterPasswordForm
+ * Shows a form for entering room password
+ *
+ * Parameters:
+ * (String) roomJid - Room jid to join
+ * (String) roomName - Room name
+ * (String) message - [optional] Message to show as the label
+ */
+ showEnterPasswordForm: function(roomJid, roomName, message) {
+ self.Chat.Modal.show(Mustache.to_html(Candy.View.Template.PresenceError.enterPasswordForm, {
+ roomName: roomName,
+ _labelPassword: $.i18n._('labelPassword'),
+ _label: (message ? message : $.i18n._('enterRoomPassword', [roomName])),
+ _joinSubmit: $.i18n._('enterRoomPasswordSubmit')
+ }), true);
+ $('#password').focus();
+
+ // register submit handler
+ $('#enter-password-form').submit(function() {
+ var password = $('#password').val();
+
+ self.Chat.Modal.hide(function() {
+ Candy.Core.Action.Jabber.Room.Join(roomJid, password);
+ });
+ return false;
+ });
+ },
+
+ /** Function: showNicknameConflictForm
+ * Shows a form indicating that the nickname is already taken and
+ * for chosing a new nickname
+ *
+ * Parameters:
+ * (String) roomJid - Room jid to join
+ */
+ showNicknameConflictForm: function(roomJid) {
+ self.Chat.Modal.show(Mustache.to_html(Candy.View.Template.PresenceError.nicknameConflictForm, {
+ _labelNickname: $.i18n._('labelNickname'),
+ _label: $.i18n._('nicknameConflict'),
+ _loginSubmit: $.i18n._('loginSubmit')
+ }));
+ $('#nickname').focus();
+
+ // register submit handler
+ $('#nickname-conflict-form').submit(function() {
+ var nickname = $('#nickname').val();
+
+ self.Chat.Modal.hide(function() {
+ Candy.Core.getUser().data.nick = nickname;
+ Candy.Core.Action.Jabber.Room.Join(roomJid);
+ });
+ return false;
+ });
+ },
+
+ /** Function: showError
+ * Show modal containing error message
+ *
+ * Parameters:
+ * (String) message - key of translation to display
+ * (Array) replacements - array containing replacements for translation (%s)
+ */
+ showError: function(message, replacements) {
+ self.Chat.Modal.show(Mustache.to_html(Candy.View.Template.PresenceError.displayError, {
+ _error: $.i18n._(message, replacements)
+ }), true);
+ }
+ },
+
+ /** Class: Candy.View.Pane.Tooltip
+ * Class to display tooltips over specific elements
+ */
+ Tooltip: {
+ /** Function: show
+ * Show a tooltip on event.currentTarget with content specified or content within the target's attribute data-tooltip.
+ *
+ * On mouseleave on the target, hide the tooltip.
+ *
+ * Parameters:
+ * (Event) event - Triggered event
+ * (String) content - Content to display [optional]
+ */
+ show: function(event, content) {
+ var tooltip = $('#tooltip'),
+ target = $(event.currentTarget);
+
+ if(!content) {
+ content = target.attr('data-tooltip');
+ }
+
+ if(tooltip.length === 0) {
+ var html = Mustache.to_html(Candy.View.Template.Chat.tooltip);
+ $('#chat-pane').append(html);
+ tooltip = $('#tooltip');
+ }
+
+ $('#context-menu').hide();
+
+ tooltip.stop(false, true);
+ tooltip.children('div').html(content);
+
+ var pos = target.offset(),
+ posLeft = Candy.Util.getPosLeftAccordingToWindowBounds(tooltip, pos.left),
+ posTop = Candy.Util.getPosTopAccordingToWindowBounds(tooltip, pos.top);
+
+ tooltip
+ .css({'left': posLeft.px, 'top': posTop.px})
+ .removeClass('left-top left-bottom right-top right-bottom')
+ .addClass(posLeft.backgroundPositionAlignment + '-' + posTop.backgroundPositionAlignment)
+ .fadeIn('fast');
+
+ target.mouseleave(function(event) {
+ event.stopPropagation();
+ $('#tooltip').stop(false, true).fadeOut('fast', function() {$(this).css({'top': 0, 'left': 0});});
+ });
+ }
+ },
+
+ /** Class: Candy.View.Pane.Context
+ * Context menu for actions and settings
+ */
+ Context: {
+ /** Function: init
+ * Initialize context menu and setup mouseleave handler.
+ */
+ init: function() {
+ if ($('#context-menu').length === 0) {
+ var html = Mustache.to_html(Candy.View.Template.Chat.Context.menu);
+ $('#chat-pane').append(html);
+ $('#context-menu').mouseleave(function() {
+ $(this).fadeOut('fast');
+ });
+ }
+ },
+
+ /** Function: show
+ * Show context menu (positions it according to the window height/width)
+ *
+ * Parameters:
+ * (Element) elem - On which element it should be shown
+ * (String) roomJid - Room Jid of the room it should be shown
+ * (Candy.Core.chatUser) user - User
+ *
+ * Uses:
+ * <getMenuLinks> for getting menulinks the user has access to
+ * <Candy.Util.getPosLeftAccordingToWindowBounds> for positioning
+ * <Candy.Util.getPosTopAccordingToWindowBounds> for positioning
+ *
+ * Triggers:
+ * candy:view.roster.after-context-menu using {roomJid, user, elements}
+ */
+ show: function(elem, roomJid, user) {
+ elem = $(elem);
+ var roomId = self.Chat.rooms[roomJid].id,
+ menu = $('#context-menu'),
+ links = $('ul li', menu);
+
+ $('#tooltip').hide();
+
+ // add specific context-user class if a user is available (when context menu should be opened next to a user)
+ if(!user) {
+ user = Candy.Core.getUser();
+ }
+
+ links.remove();
+
+ var menulinks = this.getMenuLinks(roomJid, user, elem),
+ id,
+ clickHandler = function(roomJid, user) {
+ return function(event) {
+ event.data.callback(event, roomJid, user);
+ $('#context-menu').hide();
+ };
+ };
+
+ for(id in menulinks) {
+ if(menulinks.hasOwnProperty(id)) {
+ var link = menulinks[id],
+ html = Mustache.to_html(Candy.View.Template.Chat.Context.menulinks, {
+ 'roomId' : roomId,
+ 'class' : link['class'],
+ 'id' : id,
+ 'label' : link.label
+ });
+ $('ul', menu).append(html);
+ $('#context-menu-' + id).bind('click', link, clickHandler(roomJid, user));
+ }
+ }
+ // if `id` is set the menu is not empty
+ if(id) {
+ var pos = elem.offset(),
+ posLeft = Candy.Util.getPosLeftAccordingToWindowBounds(menu, pos.left),
+ posTop = Candy.Util.getPosTopAccordingToWindowBounds(menu, pos.top);
+
+ menu
+ .css({'left': posLeft.px, 'top': posTop.px})
+ .removeClass('left-top left-bottom right-top right-bottom')
+ .addClass(posLeft.backgroundPositionAlignment + '-' + posTop.backgroundPositionAlignment)
+ .fadeIn('fast');
+
+ /** Event: candy:view.roster.after-context-menu
+ * After context menu display
+ *
+ * Parameters:
+ * (String) roomJid - room where the context menu has been triggered
+ * (Candy.Core.ChatUser) user - User
+ * (jQuery.Element) element - Menu element
+ */
+ $(Candy).triggerHandler('candy:view.roster.after-context-menu', {
+ 'roomJid' : roomJid,
+ 'user' : user,
+ 'element': menu
+ });
+
+ return true;
+ }
+ },
+
+ /** Function: getMenuLinks
+ * Extends <initialMenuLinks> with menu links gathered from candy:view.roster.contextmenu
+ *
+ * Parameters:
+ * (String) roomJid - Room in which the menu will be displayed
+ * (Candy.Core.ChatUser) user - User
+ * (jQuery.Element) elem - Parent element of the context menu
+ *
+ * Triggers:
+ * candy:view.roster.context-menu using {roomJid, user, elem}
+ *
+ * Returns:
+ * (Object) - object containing the extended menulinks.
+ */
+ getMenuLinks: function(roomJid, user, elem) {
+ var menulinks, id;
+
+ var evtData = {
+ 'roomJid' : roomJid,
+ 'user' : user,
+ 'elem': elem,
+ 'menulinks': this.initialMenuLinks(elem)
+ };
+
+ /** Event: candy:view.roster.context-menu
+ * Modify existing menu links (add links)
+ *
+ * In order to modify the links you need to change the object passed with an additional
+ * key "menulinks" containing the menulink object.
+ *
+ * Parameters:
+ * (String) roomJid - Room on which the menu should be displayed
+ * (Candy.Core.ChatUser) user - User
+ * (jQuery.Element) elem - Parent element of the context menu
+ */
+ $(Candy).triggerHandler('candy:view.roster.context-menu', evtData);
+
+ menulinks = evtData.menulinks;
+
+ for(id in menulinks) {
+ if(menulinks.hasOwnProperty(id) && menulinks[id].requiredPermission !== undefined && !menulinks[id].requiredPermission(user, self.Room.getUser(roomJid), elem)) {
+ delete menulinks[id];
+ }
+ }
+ return menulinks;
+ },
+
+ /** Function: initialMenuLinks
+ * Returns initial menulinks. The following are initial:
+ *
+ * - Private Chat
+ * - Ignore
+ * - Unignore
+ * - Kick
+ * - Ban
+ * - Change Subject
+ *
+ * Returns:
+ * (Object) - object containing those menulinks
+ */
+ initialMenuLinks: function() {
+ return {
+ 'private': {
+ requiredPermission: function(user, me) {
+ return me.getNick() !== user.getNick() && Candy.Core.getRoom(Candy.View.getCurrent().roomJid) && !Candy.Core.getUser().isInPrivacyList('ignore', user.getJid());
+ },
+ 'class' : 'private',
+ 'label' : $.i18n._('privateActionLabel'),
+ 'callback' : function(e, roomJid, user) {
+ $('#user-' + Candy.Util.jidToId(roomJid) + '-' + Candy.Util.jidToId(user.getJid())).click();
+ }
+ },
+ 'ignore': {
+ requiredPermission: function(user, me) {
+ return me.getNick() !== user.getNick() && !Candy.Core.getUser().isInPrivacyList('ignore', user.getJid());
+ },
+ 'class' : 'ignore',
+ 'label' : $.i18n._('ignoreActionLabel'),
+ 'callback' : function(e, roomJid, user) {
+ Candy.View.Pane.Room.ignoreUser(roomJid, user.getJid());
+ }
+ },
+ 'unignore': {
+ requiredPermission: function(user, me) {
+ return me.getNick() !== user.getNick() && Candy.Core.getUser().isInPrivacyList('ignore', user.getJid());
+ },
+ 'class' : 'unignore',
+ 'label' : $.i18n._('unignoreActionLabel'),
+ 'callback' : function(e, roomJid, user) {
+ Candy.View.Pane.Room.unignoreUser(roomJid, user.getJid());
+ }
+ },
+ 'kick': {
+ requiredPermission: function(user, me) {
+ return me.getNick() !== user.getNick() && me.isModerator() && !user.isModerator();
+ },
+ 'class' : 'kick',
+ 'label' : $.i18n._('kickActionLabel'),
+ 'callback' : function(e, roomJid, user) {
+ self.Chat.Modal.show(Mustache.to_html(Candy.View.Template.Chat.Context.contextModalForm, {
+ _label: $.i18n._('reason'),
+ _submit: $.i18n._('kickActionLabel')
+ }), true);
+ $('#context-modal-field').focus();
+ $('#context-modal-form').submit(function() {
+ Candy.Core.Action.Jabber.Room.Admin.UserAction(roomJid, user.getJid(), 'kick', $('#context-modal-field').val());
+ self.Chat.Modal.hide();
+ return false; // stop propagation & preventDefault, as otherwise you get disconnected (wtf?)
+ });
+ }
+ },
+ 'ban': {
+ requiredPermission: function(user, me) {
+ return me.getNick() !== user.getNick() && me.isModerator() && !user.isModerator();
+ },
+ 'class' : 'ban',
+ 'label' : $.i18n._('banActionLabel'),
+ 'callback' : function(e, roomJid, user) {
+ self.Chat.Modal.show(Mustache.to_html(Candy.View.Template.Chat.Context.contextModalForm, {
+ _label: $.i18n._('reason'),
+ _submit: $.i18n._('banActionLabel')
+ }), true);
+ $('#context-modal-field').focus();
+ $('#context-modal-form').submit(function() {
+ Candy.Core.Action.Jabber.Room.Admin.UserAction(roomJid, user.getJid(), 'ban', $('#context-modal-field').val());
+ self.Chat.Modal.hide();
+ return false; // stop propagation & preventDefault, as otherwise you get disconnected (wtf?)
+ });
+ }
+ },
+ 'subject': {
+ requiredPermission: function(user, me) {
+ return me.getNick() === user.getNick() && me.isModerator();
+ },
+ 'class': 'subject',
+ 'label' : $.i18n._('setSubjectActionLabel'),
+ 'callback': function(e, roomJid) {
+ self.Chat.Modal.show(Mustache.to_html(Candy.View.Template.Chat.Context.contextModalForm, {
+ _label: $.i18n._('subject'),
+ _submit: $.i18n._('setSubjectActionLabel')
+ }), true);
+ $('#context-modal-field').focus();
+ $('#context-modal-form').submit(function(e) {
+ Candy.Core.Action.Jabber.Room.Admin.SetSubject(roomJid, $('#context-modal-field').val());
+ self.Chat.Modal.hide();
+ e.preventDefault();
+ });
+ }
+ }
+ };
+ },
+
+ /** Function: showEmoticonsMenu
+ * Shows the special emoticons menu
+ *
+ * Parameters:
+ * (Element) elem - Element on which it should be positioned to.
+ *
+ * Returns:
+ * (Boolean) - true
+ */
+ showEmoticonsMenu: function(elem) {
+ elem = $(elem);
+ var pos = elem.offset(),
+ menu = $('#context-menu'),
+ content = $('ul', menu),
+ emoticons = '',
+ i;
+
+ $('#tooltip').hide();
+
+ for(i = Candy.Util.Parser.emoticons.length-1; i >= 0; i--) {
+ emoticons = '<img src="' + Candy.Util.Parser._emoticonPath + Candy.Util.Parser.emoticons[i].image + '" alt="' + Candy.Util.Parser.emoticons[i].plain + '" />' + emoticons;
+ }
+ content.html('<li class="emoticons">' + emoticons + '</li>');
+ content.find('img').click(function() {
+ var input = Candy.View.Pane.Room.getPane(Candy.View.getCurrent().roomJid, '.message-form').children('.field'),
+ value = input.val(),
+ emoticon = $(this).attr('alt') + ' ';
+ input.val(value ? value + ' ' + emoticon : emoticon).focus();
+
+ // Once you make a selction, hide the menu.
+ 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})
+ .removeClass('left-top left-bottom right-top right-bottom')
+ .addClass(posLeft.backgroundPositionAlignment + '-' + posTop.backgroundPositionAlignment)
+ .fadeIn('fast');
+
+ return true;
+ }
+ }
+ };
+
+ return self;
+}(Candy.View.Pane || {}, jQuery));
diff --git a/src/view/pane/message.js b/src/view/pane/message.js
new file mode 100644
index 0000000..b4bcbb6
--- /dev/null
+++ b/src/view/pane/message.js
@@ -0,0 +1,253 @@
+/** File: message.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, Mustache, jQuery */
+
+/** Class: Candy.View.Pane
+ * Candy view pane handles everything regarding DOM updates etc.
+ *
+ * Parameters:
+ * (Candy.View.Pane) self - itself
+ * (jQuery) $ - jQuery
+ */
+Candy.View.Pane = (function(self, $) {
+
+ /** Class: Candy.View.Pane.Message
+ * Message submit/show handling
+ */
+ self.Message = {
+ /** Function: submit
+ * on submit handler for message field sends the message to the server and if it's a private chat, shows the message
+ * immediately because the server doesn't send back those message.
+ *
+ * Parameters:
+ * (Event) event - Triggered event
+ *
+ * Triggers:
+ * candy:view.message.before-send using {message}
+ *
+ * FIXME: as everywhere, `roomJid` might be slightly incorrect in this case
+ * - maybe rename this as part of a refactoring.
+ */
+ submit: function(event) {
+ var roomJid = Candy.View.getCurrent().roomJid,
+ room = Candy.View.Pane.Chat.rooms[roomJid],
+ roomType = room.type,
+ targetJid = room.targetJid,
+ message = $(this).children('.field').val().substring(0, Candy.View.getOptions().crop.message.body),
+ xhtmlMessage,
+ evtData = {
+ roomJid: roomJid,
+ message: message,
+ xhtmlMessage: xhtmlMessage
+ };
+
+ /** Event: candy:view.message.before-send
+ * Before sending a message
+ *
+ * Parameters:
+ * (String) roomJid - room to which the message should be sent
+ * (String) message - Message text
+ * (String) xhtmlMessage - XHTML formatted message [default: undefined]
+ *
+ * Returns:
+ * Boolean|undefined - if you like to stop sending the message, return false.
+ */
+ if($(Candy).triggerHandler('candy:view.message.before-send', evtData) === false) {
+ event.preventDefault();
+ return;
+ }
+
+ message = evtData.message;
+ xhtmlMessage = evtData.xhtmlMessage;
+
+ Candy.Core.Action.Jabber.Room.Message(targetJid, message, roomType, xhtmlMessage);
+ // Private user chat. Jabber won't notify the user who has sent the message. Just show it as the user hits the button...
+ if(roomType === 'chat' && message) {
+ self.Message.show(roomJid, self.Room.getUser(roomJid).getNick(), message, xhtmlMessage, undefined, Candy.Core.getUser().getJid());
+ }
+ // Clear input and set focus to it
+ $(this).children('.field').val('').focus();
+ event.preventDefault();
+ },
+
+ /** Function: show
+ * Show a message in the message pane
+ *
+ * Parameters:
+ * (String) roomJid - room in which the message has been sent to
+ * (String) name - Name of the user which sent the message
+ * (String) message - Message
+ * (String) xhtmlMessage - XHTML formatted message [if options enableXHTML is true]
+ * (String) timestamp - [optional] Timestamp of the message, if not present, current date.
+ * (Boolean) carbon - [optional] Indication of wether or not the message was a carbon
+ *
+ * Triggers:
+ * candy:view.message.before-show using {roomJid, name, message}
+ * candy.view.message.before-render using {template, templateData}
+ * candy:view.message.after-show using {roomJid, name, message, element}
+ */
+ show: function(roomJid, name, message, xhtmlMessage, timestamp, from, carbon, stanza) {
+ message = Candy.Util.Parser.all(message.substring(0, Candy.View.getOptions().crop.message.body));
+ if(Candy.View.getOptions().enableXHTML === true && xhtmlMessage) {
+ xhtmlMessage = Candy.Util.parseAndCropXhtml(xhtmlMessage, Candy.View.getOptions().crop.message.body);
+ }
+
+ timestamp = timestamp || new Date();
+
+ // Assume we have an ISO-8601 date string and convert it to a Date object
+ if (!timestamp.toDateString) {
+ timestamp = Candy.Util.iso8601toDate(timestamp);
+ }
+
+ // Before we add the new message, check to see if we should be automatically scrolling or not.
+ var messagePane = self.Room.getPane(roomJid, '.message-pane');
+ var enableScroll = ((messagePane.scrollTop() + messagePane.outerHeight()) === messagePane.prop('scrollHeight')) || !$(messagePane).is(':visible');
+ Candy.View.Pane.Chat.rooms[roomJid].enableScroll = enableScroll;
+
+ var evtData = {
+ 'roomJid': roomJid,
+ 'name': name,
+ 'message': message,
+ 'xhtmlMessage': xhtmlMessage,
+ 'from': from,
+ 'stanza': stanza
+ };
+
+ /** Event: candy:view.message.before-show
+ * Before showing a new message
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ * (String) name - Name of the sending user
+ * (String) message - Message text
+ *
+ * Returns:
+ * Boolean - if you don't want to show the message, return false
+ */
+ if($(Candy).triggerHandler('candy:view.message.before-show', evtData) === false) {
+ return;
+ }
+
+ message = evtData.message;
+ xhtmlMessage = evtData.xhtmlMessage;
+ if(xhtmlMessage !== undefined && xhtmlMessage.length > 0) {
+ message = xhtmlMessage;
+ }
+
+ if(!message) {
+ return;
+ }
+
+ var renderEvtData = {
+ template: Candy.View.Template.Message.item,
+ templateData: {
+ name: name,
+ displayName: Candy.Util.crop(name, Candy.View.getOptions().crop.message.nickname),
+ message: message,
+ time: Candy.Util.localizedTime(timestamp),
+ timestamp: timestamp.toISOString(),
+ roomjid: roomJid,
+ from: from
+ },
+ stanza: stanza
+ };
+
+ /** Event: candy:view.message.before-render
+ * Before rendering the message element
+ *
+ * Parameters:
+ * (String) template - Template to use
+ * (Object) templateData - Template data consists of:
+ * - (String) name - Name of the sending user
+ * - (String) displayName - Cropped name of the sending user
+ * - (String) message - Message text
+ * - (String) time - Localized time of message
+ * - (String) timestamp - ISO formatted timestamp of message
+ */
+ $(Candy).triggerHandler('candy:view.message.before-render', renderEvtData);
+
+ var html = Mustache.to_html(renderEvtData.template, renderEvtData.templateData);
+ self.Room.appendToMessagePane(roomJid, html);
+ var elem = self.Room.getPane(roomJid, '.message-pane').children().last();
+ // click on username opens private chat
+ elem.find('a.label').click(function(event) {
+ event.preventDefault();
+ // Check if user is online and not myself
+ var room = Candy.Core.getRoom(roomJid);
+ if(room && name !== self.Room.getUser(Candy.View.getCurrent().roomJid).getNick() && room.getRoster().get(roomJid + '/' + name)) {
+ if(Candy.View.Pane.PrivateRoom.open(roomJid + '/' + name, name, true) === false) {
+ return false;
+ }
+ }
+ });
+
+ if (!carbon) {
+ var notifyEvtData = {
+ name: name,
+ displayName: Candy.Util.crop(name, Candy.View.getOptions().crop.message.nickname),
+ roomJid: roomJid,
+ message: message,
+ time: Candy.Util.localizedTime(timestamp),
+ timestamp: timestamp.toISOString()
+ };
+ /** Event: candy:view.message.notify
+ * Notify the user (optionally) that a new message has been received
+ *
+ * Parameters:
+ * (Object) templateData - Template data consists of:
+ * - (String) name - Name of the sending user
+ * - (String) displayName - Cropped name of the sending user
+ * - (String) roomJid - JID into which the message was sent
+ * - (String) message - Message text
+ * - (String) time - Localized time of message
+ * - (String) timestamp - ISO formatted timestamp of message
+ * - (Boolean) carbon - Indication of wether or not the message was a carbon
+ */
+ $(Candy).triggerHandler('candy:view.message.notify', notifyEvtData);
+
+ // Check to see if in-core notifications are disabled
+ if(!Candy.Core.getOptions().disableCoreNotifications) {
+ if(Candy.View.getCurrent().roomJid !== roomJid || !self.Window.hasFocus()) {
+ self.Chat.increaseUnreadMessages(roomJid);
+ if(!self.Window.hasFocus()) {
+ // Notify the user about a new private message OR on all messages if configured
+ if(Candy.View.Pane.Chat.rooms[roomJid].type === 'chat' || Candy.View.getOptions().updateWindowOnAllMessages === true) {
+ self.Chat.Toolbar.playSound();
+ }
+ }
+ }
+ }
+
+ if(Candy.View.getCurrent().roomJid === roomJid) {
+ self.Room.scrollToBottom(roomJid);
+ }
+ }
+
+ evtData.element = elem;
+
+ /** Event: candy:view.message.after-show
+ * Triggered after showing a message
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ * (jQuery.Element) element - User element
+ * (String) name - Name of the sending user
+ * (String) message - Message text
+ */
+ $(Candy).triggerHandler('candy:view.message.after-show', evtData);
+ }
+ };
+
+ return self;
+}(Candy.View.Pane || {}, jQuery));
diff --git a/src/view/pane/privateRoom.js b/src/view/pane/privateRoom.js
new file mode 100644
index 0000000..67ef58d
--- /dev/null
+++ b/src/view/pane/privateRoom.js
@@ -0,0 +1,181 @@
+/** File: privateRoom.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.View.Pane
+ * Candy view pane handles everything regarding DOM updates etc.
+ *
+ * Parameters:
+ * (Candy.View.Pane) self - itself
+ * (jQuery) $ - jQuery
+ */
+Candy.View.Pane = (function(self, $) {
+
+ /** Class: Candy.View.Pane.PrivateRoom
+ * Private room handling
+ */
+ self.PrivateRoom = {
+ /** Function: open
+ * Opens a new private room
+ *
+ * Parameters:
+ * (String) roomJid - Room jid to open
+ * (String) roomName - Room name
+ * (Boolean) switchToRoom - If true, displayed room switches automatically to this room
+ * (e.g. when user clicks itself on another user to open a private chat)
+ * (Boolean) isNoConferenceRoomJid - true 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 param addresses this case.
+ *
+ * Triggers:
+ * candy:view.private-room.after-open using {roomJid, type, element}
+ */
+ open: function(roomJid, roomName, switchToRoom, isNoConferenceRoomJid) {
+ var user = isNoConferenceRoomJid ? Candy.Core.getUser() : self.Room.getUser(Strophe.getBareJidFromJid(roomJid)),
+ evtData = {
+ 'roomJid': roomJid,
+ 'roomName': roomName,
+ 'type': 'chat',
+ };
+
+ /** Event: candy:view.private-room.before-open
+ * Before opening a new private room
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ * (String) roomName - Room name
+ * (String) type - 'chat'
+ *
+ * Returns:
+ * Boolean - if you don't want to open the private room, return false
+ */
+ if($(Candy).triggerHandler('candy:view.private-room.before-open', evtData) === false) {
+ return false;
+ }
+
+ // if target user is in privacy list, don't open the private chat.
+ if (Candy.Core.getUser().isInPrivacyList('ignore', roomJid)) {
+ return false;
+ }
+ if(!self.Chat.rooms[roomJid]) {
+ if(self.Room.init(roomJid, roomName, 'chat') === false) {
+ return false;
+ }
+ }
+ if(switchToRoom) {
+ self.Room.show(roomJid);
+ }
+
+ self.Roster.update(roomJid, new Candy.Core.ChatUser(roomJid, roomName), 'join', user);
+ self.Roster.update(roomJid, user, 'join', user);
+ self.PrivateRoom.setStatus(roomJid, 'join');
+
+ evtData.element = self.Room.getPane(roomJid);
+ /** Event: candy:view.private-room.after-open
+ * After opening a new private room
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ * (String) type - 'chat'
+ * (jQuery.Element) element - User element
+ */
+ $(Candy).triggerHandler('candy:view.private-room.after-open', evtData);
+ },
+
+ /** Function: setStatus
+ * Set offline or online status for private rooms (when one of the participants leaves the room)
+ *
+ * Parameters:
+ * (String) roomJid - Private room jid
+ * (String) status - "leave"/"join"
+ */
+ setStatus: function(roomJid, status) {
+ var messageForm = self.Room.getPane(roomJid, '.message-form');
+ if(status === 'join') {
+ self.Chat.getTab(roomJid).addClass('online').removeClass('offline');
+
+ messageForm.children('.field').removeAttr('disabled');
+ messageForm.children('.submit').removeAttr('disabled');
+
+ self.Chat.getTab(roomJid);
+ } else if(status === 'leave') {
+ self.Chat.getTab(roomJid).addClass('offline').removeClass('online');
+
+ messageForm.children('.field').attr('disabled', true);
+ messageForm.children('.submit').attr('disabled', true);
+ }
+ },
+
+ /** Function: changeNick
+ * Changes the nick for every private room opened with this roomJid.
+ *
+ * Parameters:
+ * (String) roomJid - Public room jid
+ * (Candy.Core.ChatUser) user - User which changes his nick
+ */
+ changeNick: function changeNick(roomJid, user) {
+ Candy.Core.log('[View:Pane:PrivateRoom] changeNick');
+
+ var previousPrivateRoomJid = roomJid + '/' + user.getPreviousNick(),
+ newPrivateRoomJid = roomJid + '/' + user.getNick(),
+ previousPrivateRoomId = Candy.Util.jidToId(previousPrivateRoomJid),
+ newPrivateRoomId = Candy.Util.jidToId(newPrivateRoomJid),
+ room = self.Chat.rooms[previousPrivateRoomJid],
+ roomElement,
+ roomTabElement;
+
+ // it could happen that the new private room is already existing -> close it first.
+ // if this is not done, errors appear as two rooms would have the same id
+ if (self.Chat.rooms[newPrivateRoomJid]) {
+ self.Room.close(newPrivateRoomJid);
+ }
+
+ if (room) { /* someone I talk with, changed nick */
+ room.name = user.getNick();
+ room.id = newPrivateRoomId;
+
+ self.Chat.rooms[newPrivateRoomJid] = room;
+ delete self.Chat.rooms[previousPrivateRoomJid];
+
+ roomElement = $('#chat-room-' + previousPrivateRoomId);
+ if (roomElement) {
+ roomElement.attr('data-roomjid', newPrivateRoomJid);
+ roomElement.attr('id', 'chat-room-' + newPrivateRoomId);
+
+ roomTabElement = $('#chat-tabs li[data-roomjid="' + previousPrivateRoomJid + '"]');
+ roomTabElement.attr('data-roomjid', newPrivateRoomJid);
+
+ /* TODO: The '@' is defined in the template. Somehow we should
+ * extract both things into our CSS or do something else to prevent that.
+ */
+ roomTabElement.children('a.label').text('@' + user.getNick());
+
+ if (Candy.View.getCurrent().roomJid === previousPrivateRoomJid) {
+ Candy.View.getCurrent().roomJid = newPrivateRoomJid;
+ }
+ }
+ } else { /* I changed the nick */
+ roomElement = $('.room-pane.roomtype-chat[data-userjid="' + previousPrivateRoomJid + '"]');
+ if (roomElement.length) {
+ previousPrivateRoomId = Candy.Util.jidToId(roomElement.attr('data-roomjid'));
+ roomElement.attr('data-userjid', newPrivateRoomJid);
+ }
+ }
+ if (roomElement && roomElement.length) {
+ self.Roster.changeNick(previousPrivateRoomId, user);
+ }
+ }
+ };
+
+ return self;
+}(Candy.View.Pane || {}, jQuery));
diff --git a/src/view/pane/room.js b/src/view/pane/room.js
new file mode 100644
index 0000000..bfb95f0
--- /dev/null
+++ b/src/view/pane/room.js
@@ -0,0 +1,484 @@
+/** File: room.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, Mustache, Strophe, jQuery */
+
+/** Class: Candy.View.Pane
+ * Candy view pane handles everything regarding DOM updates etc.
+ *
+ * Parameters:
+ * (Candy.View.Pane) self - itself
+ * (jQuery) $ - jQuery
+ */
+Candy.View.Pane = (function(self, $) {
+
+ /** Class: Candy.View.Pane.Room
+ * Everything which belongs to room view things belongs here.
+ */
+ self.Room = {
+ /** Function: init
+ * Initialize a new room and inserts the room html into the DOM
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ * (String) roomName - Room name
+ * (String) roomType - Type: either "groupchat" or "chat" (private chat)
+ *
+ * Uses:
+ * - <Candy.Util.jidToId>
+ * - <Candy.View.Pane.Chat.addTab>
+ * - <getPane>
+ *
+ * Triggers:
+ * candy:view.room.after-add using {roomJid, type, element}
+ *
+ * Returns:
+ * (String) - the room id of the element created.
+ */
+ init: function(roomJid, roomName, roomType) {
+ roomType = roomType || 'groupchat';
+ roomJid = Candy.Util.unescapeJid(roomJid);
+
+ var evtData = {
+ roomJid: roomJid,
+ type: roomType
+ };
+ /** Event: candy:view.room.before-add
+ * Before initialising a room
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ * (String) type - Room Type
+ *
+ * Returns:
+ * Boolean - if you don't want to initialise the room, return false.
+ */
+ if($(Candy).triggerHandler('candy:view.room.before-add', evtData) === false) {
+ return false;
+ }
+
+ // First room, show sound control
+ if(Candy.Util.isEmptyObject(self.Chat.rooms)) {
+ self.Chat.Toolbar.show();
+ }
+
+ var roomId = Candy.Util.jidToId(roomJid);
+ self.Chat.rooms[roomJid] = {id: roomId, usercount: 0, name: roomName, type: roomType, messageCount: 0, scrollPosition: -1, targetJid: roomJid};
+
+ $('#chat-rooms').append(Mustache.to_html(Candy.View.Template.Room.pane, {
+ roomId: roomId,
+ roomJid: roomJid,
+ roomType: roomType,
+ form: {
+ _messageSubmit: $.i18n._('messageSubmit')
+ },
+ roster: {
+ _userOnline: $.i18n._('userOnline')
+ }
+ }, {
+ roster: Candy.View.Template.Roster.pane,
+ messages: Candy.View.Template.Message.pane,
+ form: Candy.View.Template.Room.form
+ }));
+ self.Chat.addTab(roomJid, roomName, roomType);
+ self.Room.getPane(roomJid, '.message-form').submit(self.Message.submit);
+ self.Room.scrollToBottom(roomJid);
+
+ evtData.element = self.Room.getPane(roomJid);
+
+ /** Event: candy:view.room.after-add
+ * After initialising a room
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ * (String) type - Room Type
+ * (jQuery.Element) element - Room element
+ */
+ $(Candy).triggerHandler('candy:view.room.after-add', evtData);
+
+ return roomId;
+ },
+
+ /** Function: show
+ * Show a specific room and hides the other rooms (if there are any)
+ *
+ * Parameters:
+ * (String) roomJid - room jid to show
+ *
+ * Triggers:
+ * candy:view.room.after-show using {roomJid, element}
+ * candy:view.room.after-hide using {roomJid, element}
+ */
+ show: function(roomJid) {
+ var roomId = self.Chat.rooms[roomJid].id,
+ evtData;
+
+ $('.room-pane').each(function() {
+ var elem = $(this);
+ evtData = {
+ 'roomJid': elem.attr('data-roomjid'),
+ 'element' : elem
+ };
+
+ if(elem.attr('id') === ('chat-room-' + roomId)) {
+ elem.show();
+ Candy.View.getCurrent().roomJid = roomJid;
+ self.Chat.setActiveTab(roomJid);
+ self.Chat.Toolbar.update(roomJid);
+ self.Chat.clearUnreadMessages(roomJid);
+ self.Room.setFocusToForm(roomJid);
+ self.Room.scrollToBottom(roomJid);
+
+ /** Event: candy:view.room.after-show
+ * After showing a room
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ * (jQuery.Element) element - Room element
+ */
+ $(Candy).triggerHandler('candy:view.room.after-show', evtData);
+
+ } else {
+ elem.hide();
+
+ /** Event: candy:view.room.after-hide
+ * After hiding a room
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ * (jQuery.Element) element - Room element
+ */
+ $(Candy).triggerHandler('candy:view.room.after-hide', evtData);
+ }
+ });
+ },
+
+ /** Function: setSubject
+ * Called when someone changes the subject in the channel
+ *
+ * Triggers:
+ * candy:view.room.after-subject-change using {roomJid, element, subject}
+ *
+ * Parameters:
+ * (String) roomJid - Room Jid
+ * (String) subject - The new subject
+ */
+ setSubject: function(roomJid, subject) {
+ subject = Candy.Util.Parser.linkify(Candy.Util.Parser.escape(subject));
+ var timestamp = new Date();
+ var html = Mustache.to_html(Candy.View.Template.Room.subject, {
+ subject: subject,
+ roomName: self.Chat.rooms[roomJid].name,
+ _roomSubject: $.i18n._('roomSubject'),
+ time: Candy.Util.localizedTime(timestamp),
+ timestamp: timestamp.toISOString()
+ });
+ self.Room.appendToMessagePane(roomJid, html);
+ self.Room.scrollToBottom(roomJid);
+
+ /** Event: candy:view.room.after-subject-change
+ * After changing the subject of a room
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ * (jQuery.Element) element - Room element
+ * (String) subject - New subject
+ */
+ $(Candy).triggerHandler('candy:view.room.after-subject-change', {
+ 'roomJid': roomJid,
+ 'element' : self.Room.getPane(roomJid),
+ 'subject' : subject
+ });
+ },
+
+ /** Function: close
+ * Close a room and remove everything in the DOM belonging to this room.
+ *
+ * NOTICE: There's a rendering bug in Opera when all rooms have been closed.
+ * (Take a look in the source for a more detailed description)
+ *
+ * Triggers:
+ * candy:view.room.after-close using {roomJid}
+ *
+ * Parameters:
+ * (String) roomJid - Room to close
+ */
+ close: function(roomJid) {
+ self.Chat.removeTab(roomJid);
+ self.Window.clearUnreadMessages();
+
+ /* TODO:
+ There's a rendering bug in Opera which doesn't redraw (remove) the message form.
+ Only a cosmetical issue (when all tabs are closed) but it's annoying...
+ This happens when form has no focus too. Maybe it's because of CSS positioning.
+ */
+ self.Room.getPane(roomJid).remove();
+ var openRooms = $('#chat-rooms').children();
+ if(Candy.View.getCurrent().roomJid === roomJid) {
+ Candy.View.getCurrent().roomJid = null;
+ if(openRooms.length === 0) {
+ self.Chat.allTabsClosed();
+ } else {
+ self.Room.show(openRooms.last().attr('data-roomjid'));
+ }
+ }
+ delete self.Chat.rooms[roomJid];
+
+ /** Event: candy:view.room.after-close
+ * After closing a room
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ */
+ $(Candy).triggerHandler('candy:view.room.after-close', {
+ 'roomJid' : roomJid
+ });
+ },
+
+ /** Function: appendToMessagePane
+ * Append a new message to the message pane.
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ * (String) html - rendered message html
+ */
+ appendToMessagePane: function(roomJid, html) {
+ self.Room.getPane(roomJid, '.message-pane').append(html);
+ self.Chat.rooms[roomJid].messageCount++;
+ self.Room.sliceMessagePane(roomJid);
+ },
+
+ /** Function: sliceMessagePane
+ * Slices the message pane after the max amount of messages specified in the Candy View options (limit setting).
+ *
+ * This is done to hopefully prevent browsers from getting slow after a certain amount of messages in the DOM.
+ *
+ * The slice is only done when autoscroll is on, because otherwise someone might lose exactly the message he want to look for.
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ */
+ sliceMessagePane: function(roomJid) {
+ // Only clean if autoscroll is enabled
+ if(self.Window.autoscroll) {
+ var options = Candy.View.getOptions().messages;
+ if(self.Chat.rooms[roomJid].messageCount > options.limit) {
+ self.Room.getPane(roomJid, '.message-pane').children().slice(0, options.remove).remove();
+ self.Chat.rooms[roomJid].messageCount -= options.remove;
+ }
+ }
+ },
+
+ /** Function: scrollToBottom
+ * Scroll to bottom wrapper for <onScrollToBottom> to be able to disable it by overwriting the function.
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ *
+ * Uses:
+ * - <onScrollToBottom>
+ */
+ scrollToBottom: function(roomJid) {
+ self.Room.onScrollToBottom(roomJid);
+ },
+
+ /** Function: onScrollToBottom
+ * Scrolls to the latest message received/sent.
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ */
+ onScrollToBottom: function(roomJid) {
+ var messagePane = self.Room.getPane(roomJid, '.message-pane');
+
+ if (Candy.View.Pane.Chat.rooms[roomJid].enableScroll === true) {
+ messagePane.scrollTop(messagePane.prop('scrollHeight'));
+ } else {
+ return false;
+ }
+ },
+
+ /** Function: onScrollToStoredPosition
+ * When autoscroll is off, the position where the scrollbar is has to be stored for each room, because it otherwise
+ * goes to the top in the message window.
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ */
+ onScrollToStoredPosition: function(roomJid) {
+ // This should only apply when entering a room...
+ // ... therefore we set scrollPosition to -1 after execution.
+ if(self.Chat.rooms[roomJid].scrollPosition > -1) {
+ var messagePane = self.Room.getPane(roomJid, '.message-pane-wrapper');
+ messagePane.scrollTop(self.Chat.rooms[roomJid].scrollPosition);
+ self.Chat.rooms[roomJid].scrollPosition = -1;
+ }
+ },
+
+ /** Function: setFocusToForm
+ * Set focus to the message input field within the message form.
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ */
+ setFocusToForm: function(roomJid) {
+ // If we're on mobile, don't focus the input field.
+ if (Candy.Util.isMobile()) { return true; }
+
+ var pane = self.Room.getPane(roomJid, '.message-form');
+ if (pane) {
+ // IE8 will fail maybe, because the field isn't there yet.
+ try {
+ pane.children('.field')[0].focus();
+ } catch(e) {
+ // fail silently
+ }
+ }
+ },
+
+ /** Function: setUser
+ * Sets or updates the current user in the specified room (called by <Candy.View.Pane.Roster.update>) and set specific informations
+ * (roles and affiliations) on the room tab (chat-pane).
+ *
+ * Parameters:
+ * (String) roomJid - Room in which the user is set to.
+ * (Candy.Core.ChatUser) user - The user
+ */
+ setUser: function(roomJid, user) {
+ self.Chat.rooms[roomJid].user = user;
+ var roomPane = self.Room.getPane(roomJid),
+ chatPane = $('#chat-pane');
+
+ roomPane.attr('data-userjid', user.getJid());
+ // Set classes based on user role / affiliation
+ if(user.isModerator()) {
+ if (user.getRole() === user.ROLE_MODERATOR) {
+ chatPane.addClass('role-moderator');
+ }
+ if (user.getAffiliation() === user.AFFILIATION_OWNER) {
+ chatPane.addClass('affiliation-owner');
+ }
+ } else {
+ chatPane.removeClass('role-moderator affiliation-owner');
+ }
+ self.Chat.Context.init();
+ },
+
+ /** Function: getUser
+ * Get the current user in the room specified with the jid
+ *
+ * Parameters:
+ * (String) roomJid - Room of which the user should be returned from
+ *
+ * Returns:
+ * (Candy.Core.ChatUser) - user
+ */
+ getUser: function(roomJid) {
+ return self.Chat.rooms[roomJid].user;
+ },
+
+ /** Function: ignoreUser
+ * Ignore specified user and add the ignore icon to the roster item of the user
+ *
+ * Parameters:
+ * (String) roomJid - Room in which the user should be ignored
+ * (String) userJid - User which should be ignored
+ */
+ ignoreUser: function(roomJid, userJid) {
+ Candy.Core.Action.Jabber.Room.IgnoreUnignore(userJid);
+ Candy.View.Pane.Room.addIgnoreIcon(roomJid, userJid);
+ },
+
+ /** Function: unignoreUser
+ * Unignore an ignored user and remove the ignore icon of the roster item.
+ *
+ * Parameters:
+ * (String) roomJid - Room in which the user should be unignored
+ * (String) userJid - User which should be unignored
+ */
+ unignoreUser: function(roomJid, userJid) {
+ Candy.Core.Action.Jabber.Room.IgnoreUnignore(userJid);
+ Candy.View.Pane.Room.removeIgnoreIcon(roomJid, userJid);
+ },
+
+ /** Function: addIgnoreIcon
+ * Add the ignore icon to the roster item of the specified user
+ *
+ * Parameters:
+ * (String) roomJid - Room in which the roster item should be updated
+ * (String) userJid - User of which the roster item should be updated
+ */
+ addIgnoreIcon: function(roomJid, userJid) {
+ if (Candy.View.Pane.Chat.rooms[userJid]) {
+ $('#user-' + Candy.View.Pane.Chat.rooms[userJid].id + '-' + Candy.Util.jidToId(userJid)).addClass('status-ignored');
+ }
+ if (Candy.View.Pane.Chat.rooms[Strophe.getBareJidFromJid(roomJid)]) {
+ $('#user-' + Candy.View.Pane.Chat.rooms[Strophe.getBareJidFromJid(roomJid)].id + '-' + Candy.Util.jidToId(userJid)).addClass('status-ignored');
+ }
+ },
+
+ /** Function: removeIgnoreIcon
+ * Remove the ignore icon to the roster item of the specified user
+ *
+ * Parameters:
+ * (String) roomJid - Room in which the roster item should be updated
+ * (String) userJid - User of which the roster item should be updated
+ */
+ removeIgnoreIcon: function(roomJid, userJid) {
+ if (Candy.View.Pane.Chat.rooms[userJid]) {
+ $('#user-' + Candy.View.Pane.Chat.rooms[userJid].id + '-' + Candy.Util.jidToId(userJid)).removeClass('status-ignored');
+ }
+ if (Candy.View.Pane.Chat.rooms[Strophe.getBareJidFromJid(roomJid)]) {
+ $('#user-' + Candy.View.Pane.Chat.rooms[Strophe.getBareJidFromJid(roomJid)].id + '-' + Candy.Util.jidToId(userJid)).removeClass('status-ignored');
+ }
+ },
+
+ /** Function: getPane
+ * Get the chat room pane or a subPane of it (if subPane is specified)
+ *
+ * Parameters:
+ * (String) roomJid - Room in which the pane lies
+ * (String) subPane - Sub pane of the chat room pane if needed [optional]
+ */
+ getPane: function(roomJid, subPane) {
+ if (self.Chat.rooms[roomJid]) {
+ if(subPane) {
+ if(self.Chat.rooms[roomJid]['pane-' + subPane]) {
+ return self.Chat.rooms[roomJid]['pane-' + subPane];
+ } else {
+ self.Chat.rooms[roomJid]['pane-' + subPane] = $('#chat-room-' + self.Chat.rooms[roomJid].id).find(subPane);
+ return self.Chat.rooms[roomJid]['pane-' + subPane];
+ }
+ } else {
+ return $('#chat-room-' + self.Chat.rooms[roomJid].id);
+ }
+ }
+ },
+
+ /** Function: changeDataUserJidIfUserIsMe
+ * Changes the room's data-userjid attribute if the specified user is the current user.
+ *
+ * Parameters:
+ * (String) roomId - Id of the room
+ * (Candy.Core.ChatUser) user - User
+ */
+ changeDataUserJidIfUserIsMe: function(roomId, user) {
+ if (user.getNick() === Candy.Core.getUser().getNick()) {
+ var roomElement = $('#chat-room-' + roomId);
+ roomElement.attr('data-userjid', Strophe.getBareJidFromJid(roomElement.attr('data-userjid')) + '/' + user.getNick());
+ }
+ }
+ };
+
+ return self;
+}(Candy.View.Pane || {}, jQuery));
diff --git a/src/view/pane/roster.js b/src/view/pane/roster.js
new file mode 100644
index 0000000..077ae50
--- /dev/null
+++ b/src/view/pane/roster.js
@@ -0,0 +1,295 @@
+/** File: roster.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, Mustache, Strophe, jQuery */
+
+/** Class: Candy.View.Pane
+ * Candy view pane handles everything regarding DOM updates etc.
+ *
+ * Parameters:
+ * (Candy.View.Pane) self - itself
+ * (jQuery) $ - jQuery
+ */
+Candy.View.Pane = (function(self, $) {
+
+ /** Class Candy.View.Pane.Roster
+ * Handles everyhing regarding roster updates.
+ */
+ self.Roster = {
+ /** Function: update
+ * Called by <Candy.View.Observer.Presence.update> to update the roster if needed.
+ * Adds/removes users from the roster list or updates informations on their items (roles, affiliations etc.)
+ *
+ * TODO: Refactoring, this method has too much LOC.
+ *
+ * Parameters:
+ * (String) roomJid - Room JID in which the update happens
+ * (Candy.Core.ChatUser) user - User on which the update happens
+ * (String) action - one of "join", "leave", "kick" and "ban"
+ * (Candy.Core.ChatUser) currentUser - Current user
+ *
+ * Triggers:
+ * candy:view.roster.before-update using {roomJid, user, action, element}
+ * candy:view.roster.after-update using {roomJid, user, action, element}
+ */
+ update: function(roomJid, user, action, currentUser) {
+ Candy.Core.log('[View:Pane:Roster] ' + action);
+ var roomId = self.Chat.rooms[roomJid].id,
+ userId = Candy.Util.jidToId(user.getJid()),
+ usercountDiff = -1,
+ userElem = $('#user-' + roomId + '-' + userId),
+ evtData = {
+ 'roomJid' : roomJid,
+ 'user' : user,
+ 'action': action,
+ 'element': userElem
+ };
+
+ /** Event: candy:view.roster.before-update
+ * Before updating the roster of a room
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ * (Candy.Core.ChatUser) user - User
+ * (String) action - [join, leave, kick, ban]
+ * (jQuery.Element) element - User element
+ */
+ $(Candy).triggerHandler('candy:view.roster.before-update', evtData);
+
+ // a user joined the room
+ if(action === 'join') {
+ usercountDiff = 1;
+
+ if(userElem.length < 1) {
+ self.Roster._insertUser(roomJid, roomId, user, userId, currentUser);
+ self.Roster.showJoinAnimation(user, userId, roomId, roomJid, currentUser);
+ // user is in room but maybe the affiliation/role has changed
+ } else {
+ usercountDiff = 0;
+ userElem.remove();
+ self.Roster._insertUser(roomJid, roomId, user, userId, currentUser);
+ // it's me, update the toolbar
+ if(currentUser !== undefined && user.getNick() === currentUser.getNick() && self.Room.getUser(roomJid)) {
+ self.Chat.Toolbar.update(roomJid);
+ }
+ }
+
+ // Presence of client
+ if (currentUser !== undefined && currentUser.getNick() === user.getNick()) {
+ self.Room.setUser(roomJid, user);
+ // add click handler for private chat
+ } else {
+ $('#user-' + roomId + '-' + userId).click(self.Roster.userClick);
+ }
+
+ $('#user-' + roomId + '-' + userId + ' .context').click(function(e) {
+ self.Chat.Context.show(e.currentTarget, roomJid, user);
+ e.stopPropagation();
+ });
+
+ // check if current user is ignoring the user who has joined.
+ if (currentUser !== undefined && currentUser.isInPrivacyList('ignore', user.getJid())) {
+ Candy.View.Pane.Room.addIgnoreIcon(roomJid, user.getJid());
+ }
+ // a user left the room
+ } else if(action === 'leave') {
+ self.Roster.leaveAnimation('user-' + roomId + '-' + userId);
+ // always show leave message in private room, even if status messages have been disabled
+ if (self.Chat.rooms[roomJid].type === 'chat') {
+ self.Chat.onInfoMessage(roomJid, null, $.i18n._('userLeftRoom', [user.getNick()]));
+ } else {
+ self.Chat.infoMessage(roomJid, null, $.i18n._('userLeftRoom', [user.getNick()]), '');
+ }
+
+ } else if(action === 'nickchange') {
+ usercountDiff = 0;
+ self.Roster.changeNick(roomId, user);
+ self.Room.changeDataUserJidIfUserIsMe(roomId, user);
+ self.PrivateRoom.changeNick(roomJid, user);
+ var infoMessage = $.i18n._('userChangedNick', [user.getPreviousNick(), user.getNick()]);
+ self.Chat.infoMessage(roomJid, null, infoMessage);
+ // user has been kicked
+ } else if(action === 'kick') {
+ self.Roster.leaveAnimation('user-' + roomId + '-' + userId);
+ self.Chat.onInfoMessage(roomJid, null, $.i18n._('userHasBeenKickedFromRoom', [user.getNick()]));
+ // user has been banned
+ } else if(action === 'ban') {
+ self.Roster.leaveAnimation('user-' + roomId + '-' + userId);
+ self.Chat.onInfoMessage(roomJid, null, $.i18n._('userHasBeenBannedFromRoom', [user.getNick()]));
+ }
+
+ // Update user count
+ Candy.View.Pane.Chat.rooms[roomJid].usercount += usercountDiff;
+
+ if(roomJid === Candy.View.getCurrent().roomJid) {
+ Candy.View.Pane.Chat.Toolbar.updateUsercount(Candy.View.Pane.Chat.rooms[roomJid].usercount);
+ }
+
+
+ // in case there's been a join, the element is now there (previously not)
+ evtData.element = $('#user-' + roomId + '-' + userId);
+ /** Event: candy:view.roster.after-update
+ * After updating a room's roster
+ *
+ * Parameters:
+ * (String) roomJid - Room JID
+ * (Candy.Core.ChatUser) user - User
+ * (String) action - [join, leave, kick, ban]
+ * (jQuery.Element) element - User element
+ */
+ $(Candy).triggerHandler('candy:view.roster.after-update', evtData);
+ },
+
+ _insertUser: function(roomJid, roomId, user, userId, currentUser) {
+ var contact = user.getContact();
+ var html = Mustache.to_html(Candy.View.Template.Roster.user, {
+ roomId: roomId,
+ userId : userId,
+ userJid: user.getJid(),
+ realJid: user.getRealJid(),
+ status: user.getStatus(),
+ contact_status: contact ? contact.getStatus() : 'unavailable',
+ nick: user.getNick(),
+ displayNick: Candy.Util.crop(user.getNick(), Candy.View.getOptions().crop.roster.nickname),
+ role: user.getRole(),
+ affiliation: user.getAffiliation(),
+ me: currentUser !== undefined && user.getNick() === currentUser.getNick(),
+ tooltipRole: $.i18n._('tooltipRole'),
+ tooltipIgnored: $.i18n._('tooltipIgnored')
+ });
+
+ var userInserted = false,
+ rosterPane = self.Room.getPane(roomJid, '.roster-pane');
+
+ // there are already users in the roster
+ if(rosterPane.children().length > 0) {
+ // insert alphabetically, sorted by status
+ var userSortCompare = self.Roster._userSortCompare(user.getNick(), user.getStatus());
+ rosterPane.children().each(function() {
+ var elem = $(this);
+ if(self.Roster._userSortCompare(elem.attr('data-nick'), elem.attr('data-status')) > userSortCompare) {
+ elem.before(html);
+ userInserted = true;
+ return false;
+ }
+ return true;
+ });
+ }
+ // first user in roster
+ if(!userInserted) {
+ rosterPane.append(html);
+ }
+ },
+
+ _userSortCompare: function(nick, status) {
+ var statusWeight;
+ switch (status) {
+ case 'available':
+ statusWeight = 1;
+ break;
+ case 'unavailable':
+ statusWeight = 9;
+ break;
+ default:
+ statusWeight = 8;
+ }
+ return statusWeight + nick.toUpperCase();
+ },
+
+ /** Function: userClick
+ * Click handler for opening a private room
+ */
+ userClick: function() {
+ var elem = $(this),
+ realJid = elem.attr('data-real-jid'),
+ useRealJid = Candy.Core.getOptions().useParticipantRealJid && (realJid !== undefined && realJid !== null && realJid !== ''),
+ targetJid = useRealJid && realJid ? Strophe.getBareJidFromJid(realJid) : elem.attr('data-jid');
+ self.PrivateRoom.open(targetJid, elem.attr('data-nick'), true, useRealJid);
+ },
+
+ /** Function: showJoinAnimation
+ * Shows join animation if needed
+ *
+ * FIXME: Refactor. Part of this will be done by the big room improvements
+ */
+ showJoinAnimation: function(user, userId, roomId, roomJid, currentUser) {
+ // don't show if the user has recently changed the nickname.
+ var rosterUserId = 'user-' + roomId + '-' + userId,
+ $rosterUserElem = $('#' + rosterUserId);
+ if (!user.getPreviousNick() || !$rosterUserElem || $rosterUserElem.is(':visible') === false) {
+ self.Roster.joinAnimation(rosterUserId);
+ // only show other users joining & don't show if there's no message in the room.
+ if(currentUser !== undefined && user.getNick() !== currentUser.getNick() && self.Room.getUser(roomJid)) {
+ // always show join message in private room, even if status messages have been disabled
+ if (self.Chat.rooms[roomJid].type === 'chat') {
+ self.Chat.onInfoMessage(roomJid, null, $.i18n._('userJoinedRoom', [user.getNick()]));
+ } else {
+ self.Chat.infoMessage(roomJid, null, $.i18n._('userJoinedRoom', [user.getNick()]));
+ }
+ }
+ }
+ },
+
+ /** Function: joinAnimation
+ * Animates specified elementId on join
+ *
+ * Parameters:
+ * (String) elementId - Specific element to do the animation on
+ */
+ joinAnimation: function(elementId) {
+ $('#' + elementId).stop(true).slideDown('normal', function() {
+ $(this).animate({opacity: 1});
+ });
+ },
+
+ /** Function: leaveAnimation
+ * Leave animation for specified element id and removes the DOM element on completion.
+ *
+ * Parameters:
+ * (String) elementId - Specific element to do the animation on
+ */
+ leaveAnimation: function(elementId) {
+ $('#' + elementId).stop(true).attr('id', '#' + elementId + '-leaving').animate({opacity: 0}, {
+ complete: function() {
+ $(this).slideUp('normal', function() {
+ $(this).remove();
+ });
+ }
+ });
+ },
+
+ /** Function: changeNick
+ * Change nick of an existing user in the roster
+ *
+ * UserId has to be recalculated from the user because at the time of this call,
+ * the user is already set with the new jid & nick.
+ *
+ * Parameters:
+ * (String) roomId - Id of the room
+ * (Candy.Core.ChatUser) user - User object
+ */
+ changeNick: function(roomId, user) {
+ Candy.Core.log('[View:Pane:Roster] changeNick');
+ var previousUserJid = Strophe.getBareJidFromJid(user.getJid()) + '/' + user.getPreviousNick(),
+ elementId = 'user-' + roomId + '-' + Candy.Util.jidToId(previousUserJid),
+ el = $('#' + elementId);
+
+ el.attr('data-nick', user.getNick());
+ el.attr('data-jid', user.getJid());
+ el.children('div.label').text(user.getNick());
+ el.attr('id', 'user-' + roomId + '-' + Candy.Util.jidToId(user.getJid()));
+ }
+ };
+
+ return self;
+}(Candy.View.Pane || {}, jQuery));
diff --git a/src/view/pane/window.js b/src/view/pane/window.js
new file mode 100644
index 0000000..b6bb489
--- /dev/null
+++ b/src/view/pane/window.js
@@ -0,0 +1,117 @@
+/** File: window.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, jQuery, window */
+
+/** Class: Candy.View.Pane
+ * Candy view pane handles everything regarding DOM updates etc.
+ *
+ * Parameters:
+ * (Candy.View.Pane) self - itself
+ * (jQuery) $ - jQuery
+ */
+Candy.View.Pane = (function(self) {
+
+ /** Class: Candy.View.Pane.Window
+ * Window related view updates
+ */
+ self.Window = {
+ /** PrivateVariable: _hasFocus
+ * Window has focus
+ */
+ _hasFocus: true,
+ /** PrivateVariable: _plainTitle
+ * Document title
+ */
+ _plainTitle: window.top.document.title,
+ /** PrivateVariable: _unreadMessagesCount
+ * Unread messages count
+ */
+ _unreadMessagesCount: 0,
+
+ /** Variable: autoscroll
+ * Boolean whether autoscroll is enabled
+ */
+ autoscroll: true,
+
+ /** Function: hasFocus
+ * Checks if window has focus
+ *
+ * Returns:
+ * (Boolean)
+ */
+ hasFocus: function() {
+ return self.Window._hasFocus;
+ },
+
+ /** Function: increaseUnreadMessages
+ * Increases unread message count in window title by one.
+ */
+ increaseUnreadMessages: function() {
+ self.Window.renderUnreadMessages(++self.Window._unreadMessagesCount);
+ },
+
+ /** Function: reduceUnreadMessages
+ * Reduce unread message count in window title by `num`.
+ *
+ * Parameters:
+ * (Integer) num - Unread message count will be reduced by this value
+ */
+ reduceUnreadMessages: function(num) {
+ self.Window._unreadMessagesCount -= num;
+ if(self.Window._unreadMessagesCount <= 0) {
+ self.Window.clearUnreadMessages();
+ } else {
+ self.Window.renderUnreadMessages(self.Window._unreadMessagesCount);
+ }
+ },
+
+ /** Function: clearUnreadMessages
+ * Clear unread message count in window title.
+ */
+ clearUnreadMessages: function() {
+ self.Window._unreadMessagesCount = 0;
+ window.top.document.title = self.Window._plainTitle;
+ },
+
+ /** Function: renderUnreadMessages
+ * Update window title to show message count.
+ *
+ * Parameters:
+ * (Integer) count - Number of unread messages to show in window title
+ */
+ renderUnreadMessages: function(count) {
+ window.top.document.title = Candy.View.Template.Window.unreadmessages.replace('{{count}}', count).replace('{{title}}', self.Window._plainTitle);
+ },
+
+ /** Function: onFocus
+ * Window focus event handler.
+ */
+ onFocus: function() {
+ self.Window._hasFocus = true;
+ if (Candy.View.getCurrent().roomJid) {
+ self.Room.setFocusToForm(Candy.View.getCurrent().roomJid);
+ self.Chat.clearUnreadMessages(Candy.View.getCurrent().roomJid);
+ }
+ },
+
+ /** Function: onBlur
+ * Window blur event handler.
+ */
+ onBlur: function() {
+ self.Window._hasFocus = false;
+ }
+ };
+
+ return self;
+}(Candy.View.Pane || {}, jQuery));
diff --git a/src/view/template.js b/src/view/template.js
index 450386d..d116709 100644
--- a/src/view/template.js
+++ b/src/view/template.js
@@ -36,25 +36,19 @@ Candy.View.Template = (function(self){
'<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">' +
+ adminMessage: '<li><small data-timestamp="{{timestamp}}">{{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>',
+ '<span class="spacer">▸</span>{{subject}} {{{message}}}</div></li>',
+ infoMessage: '<li><small data-timestamp="{{timestamp}}">{{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-sound-control" class="checked" data-tooltip="{{tooltipSound}}"></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>',
@@ -73,7 +67,7 @@ Candy.View.Template = (function(self){
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">' +
+ subject: '<li><small data-timestamp="{{timestamp}}">{{time}}</small><div class="subject">' +
'<span class="label">{{roomName}}</span>' +
'<span class="spacer">▸</span>{{_roomSubject}} {{{subject}}}</div></li>',
form: '<div class="message-form-wrapper">' +
@@ -85,8 +79,8 @@ Candy.View.Template = (function(self){
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}}">' +
+ ' id="user-{{roomId}}-{{userId}}" data-jid="{{userJid}}" data-real-jid="{{realJid}}"' +
+ ' data-nick="{{nick}}" data-role="{{role}}" data-affiliation="{{affiliation}}" data-status="{{status}}">' +
'<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>' +
@@ -95,7 +89,7 @@ Candy.View.Template = (function(self){
self.Message = {
pane: '<div class="message-pane-wrapper"><ul class="message-pane"></ul></div>',
- item: '<li><small>{{time}}</small><div>' +
+ item: '<li><small data-timestamp="{{timestamp}}">{{time}}</small><div>' +
'<a class="label" href="#" class="name">{{displayName}}</a>' +
'<span class="spacer">▸</span>{{{message}}}</div></li>'
};
@@ -104,7 +98,11 @@ Candy.View.Template = (function(self){
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}}' +
+ '<input type="text" id="username" name="username"/>' +
+ '{{#displayDomain}} <span class="at-symbol">@</span> ' +
+ '<select id="domain" name="domain">{{#domains}}<option value="{{domain}}">{{domain}}</option>{{/domains}}</select>' +
+ '{{/displayDomain}}' +
+ '{{/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}}' +
diff --git a/src/view/translation.js b/src/view/translation.js
index a4ed8f5..c346234 100644
--- a/src/view/translation.js
+++ b/src/view/translation.js
@@ -56,10 +56,7 @@ Candy.View.Translation = {
'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.',
+ 'userChangedNick': '%1$s is now known as %2$s.',
'dateFormat': 'dd.mm.yyyy',
'timeFormat': 'HH:MM:ss',
@@ -126,9 +123,6 @@ Candy.View.Translation = {
'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',
@@ -154,25 +148,25 @@ Candy.View.Translation = {
'antiSpamMessage' : 'Bitte nicht spammen. Du wurdest für eine kurze Zeit blockiert.'
},
'fr' : {
- 'status': 'Status : %s',
+ 'status': 'Status&thinsp;: %s',
'statusConnecting': 'Connexion…',
- 'statusConnected' : 'Connecté.',
+ 'statusConnected' : 'Connecté',
'statusDisconnecting': 'Déconnexion…',
- 'statusDisconnected' : 'Déconnecté.',
- 'statusAuthfail': 'L\'authentification a échoué',
+ 'statusDisconnected' : 'Déconnecté',
+ 'statusAuthfail': 'L’identification a échoué',
- 'roomSubject' : 'Sujet :',
+ 'roomSubject' : 'Sujet&thinsp;:',
'messageSubmit': 'Envoyer',
- 'labelUsername': 'Nom d\'utilisateur :',
- 'labelNickname': 'Pseudo :',
- 'labelPassword': 'Mot de passe :',
+ 'labelUsername': 'Nom d’utilisateur&thinsp;:',
+ 'labelNickname': 'Pseudo&thinsp;:',
+ 'labelPassword': 'Mot de passe&thinsp;:',
'loginSubmit' : 'Connexion',
- 'loginInvalid' : 'JID invalide',
+ 'loginInvalid' : 'JID invalide',
- 'reason' : 'Motif :',
- 'subject' : 'Titre :',
- 'reasonWas' : 'Motif : %s.',
+ 'reason' : 'Motif&thinsp;:',
+ 'subject' : 'Titre&thinsp;:',
+ 'reasonWas' : 'Motif&thinsp;: %s.',
'kickActionLabel' : 'Kick',
'youHaveBeenKickedBy' : 'Vous avez été expulsé du salon %1$s (%2$s)',
'youHaveBeenKicked' : 'Vous avez été expulsé du salon %s',
@@ -182,42 +176,39 @@ Candy.View.Translation = {
'privateActionLabel' : 'Chat privé',
'ignoreActionLabel' : 'Ignorer',
- 'unignoreActionLabel' : 'Ne plus ignorer',
+ 'unignoreActionLabel': 'Ne plus ignorer',
'setSubjectActionLabel': 'Changer le sujet',
'administratorMessageSubject' : 'Administrateur',
- 'userJoinedRoom' : '%s vient d\'entrer dans le salon.',
+ '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',
+ 'tooltipSound' : 'Jouer un son lors de la réception de messages privés',
'tooltipAutoscroll' : 'Défilement automatique',
- 'tooltipStatusmessage' : 'Messages d\'état',
+ 'tooltipStatusmessage' : 'Afficher les changements d’état',
'tooltipAdministration' : 'Administration du salon',
- 'tooltipUsercount' : 'Nombre d\'utilisateurs dans le 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.',
+ '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.',
+ 'nicknameConflict': 'Ce 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.',
+ 'errorMembersOnly': 'Vous ne pouvez pas entrer dans le salon %s&thinsp;: droits insuffisants.',
+ 'errorMaxOccupantsReached': 'Vous ne pouvez pas entrer dans le salon %s&thinsp;: limite d’utilisateurs atteinte.',
- 'antiSpamMessage' : 'Merci de ne pas envoyer de spam. Vous avez été bloqué pendant une courte période..'
+ 'antiSpamMessage' : 'Merci de ne pas spammer. Vous avez été bloqué pendant une courte période.'
},
'nl' : {
'status': 'Status: %s',
@@ -258,9 +249,6 @@ Candy.View.Translation = {
'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',
@@ -323,9 +311,6 @@ Candy.View.Translation = {
'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',
@@ -386,9 +371,6 @@ Candy.View.Translation = {
'userHasBeenKickedFromRoom': '%s 被请出这个房间',
'userHasBeenBannedFromRoom': '%s 被管理者禁言',
- 'presenceUnknownWarningSubject': '注意:',
- 'presenceUnknownWarning': '这个会员可能已经下线,不能追踪到他的连接信息',
-
'dateFormat': 'dd.mm.yyyy',
'timeFormat': 'HH:MM:ss',
@@ -451,9 +433,6 @@ Candy.View.Translation = {
'userHasBeenKickedFromRoom' : '%sは部屋からキックされました。',
'userHasBeenBannedFromRoom' : '%sは部屋からアカウントバンされました。',
- 'presenceUnknownWarningSubject' : '忠告:',
- 'presenceUnknownWarning' : 'このユーザーのステータスは不明です。',
-
'dateFormat' : 'dd.mm.yyyy',
'timeFormat' : 'HH:MM:ss',
@@ -516,9 +495,6 @@ Candy.View.Translation = {
'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',
@@ -581,9 +557,6 @@ Candy.View.Translation = {
'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',
@@ -607,6 +580,74 @@ Candy.View.Translation = {
'antiSpamMessage' : 'Per favore non scrivere messaggi pubblicitari. Sei stato bloccato per un po\' di tempo.'
},
+ 'pl' : {
+ 'status': 'Status: %s',
+ 'statusConnecting': 'Łączę...',
+ 'statusConnected' : 'Połączone',
+ 'statusDisconnecting': 'Rozłączam...',
+ 'statusDisconnected' : 'Rozłączone',
+ 'statusAuthfail': 'Nieprawidłowa autoryzacja',
+
+ 'roomSubject' : 'Temat:',
+ 'messageSubmit': 'Wyślij',
+
+ 'labelUsername': 'Nazwa użytkownika:',
+ 'labelNickname': 'Ksywka:',
+ 'labelPassword': 'Hasło:',
+ 'loginSubmit' : 'Zaloguj',
+ 'loginInvalid' : 'Nieprawidłowy JID',
+
+ 'reason' : 'Przyczyna:',
+ 'subject' : 'Temat:',
+ 'reasonWas' : 'Z powodu: %s.',
+ 'kickActionLabel' : 'Wykop',
+ 'youHaveBeenKickedBy' : 'Zostałeś wykopany z %2$s przez %1$s',
+ 'youHaveBeenKicked' : 'Zostałeś wykopany z %s',
+ 'banActionLabel' : 'Ban',
+ 'youHaveBeenBannedBy' : 'Zostałeś zbanowany na %1$s przez %2$s',
+ 'youHaveBeenBanned' : 'Zostałeś zbanowany na %s',
+
+ 'privateActionLabel' : 'Rozmowa prywatna',
+ 'ignoreActionLabel' : 'Zignoruj',
+ 'unignoreActionLabel' : 'Przestań ignorować',
+
+ 'setSubjectActionLabel': 'Zmień temat',
+
+ 'administratorMessageSubject' : 'Administrator',
+
+ 'userJoinedRoom' : '%s wszedł do pokoju.',
+ 'userLeftRoom' : '%s opuścił pokój.',
+ 'userHasBeenKickedFromRoom': '%s został wykopany z pokoju.',
+ 'userHasBeenBannedFromRoom': '%s został zbanowany w pokoju.',
+ 'userChangedNick': '%1$s zmienił ksywkę na %2$s.',
+
+ 'presenceUnknownWarningSubject': 'Uwaga:',
+ 'presenceUnknownWarning' : 'Rozmówca może nie być połączony. Nie możemy ustalić jego obecności.',
+
+ 'dateFormat': 'dd.mm.yyyy',
+ 'timeFormat': 'HH:MM:ss',
+
+ 'tooltipRole' : 'Moderator',
+ 'tooltipIgnored' : 'Ignorujesz tego rozmówcę',
+ 'tooltipEmoticons' : 'Emoty',
+ 'tooltipSound' : 'Sygnał dźwiękowy przy otrzymaniu wiadomości',
+ 'tooltipAutoscroll' : 'Autoprzewijanie',
+ 'tooltipStatusmessage' : 'Wyświetl statusy',
+ 'tooltipAdministration' : 'Administrator pokoju',
+ 'tooltipUsercount' : 'Obecni rozmówcy',
+
+ 'enterRoomPassword' : 'Pokój "%s" wymaga hasła.',
+ 'enterRoomPasswordSubmit' : 'Wejdź do pokoju',
+ 'passwordEnteredInvalid' : 'Niewłaściwie hasło do pokoju "%s".',
+
+ 'nicknameConflict': 'Nazwa w użyciu. Wybierz inną.',
+
+ 'errorMembersOnly': 'Nie możesz wejść do pokoju "%s": Niepełne uprawnienia.',
+ 'errorMaxOccupantsReached': 'Nie możesz wejść do pokoju "%s": Siedzi w nim zbyt wielu ludzi.',
+ 'errorAutojoinMissing': 'Konfiguracja nie zawiera parametru automatycznego wejścia do pokoju. Wskaż pokój do którego chcesz wejść.',
+
+ 'antiSpamMessage' : 'Please do not spam. You have been blocked for a short-time.'
+ },
'pt': {
'status': 'Status: %s',
'statusConnecting': 'Conectando...',
@@ -646,8 +687,6 @@ Candy.View.Translation = {
'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',
@@ -710,9 +749,6 @@ Candy.View.Translation = {
'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',
@@ -748,6 +784,7 @@ Candy.View.Translation = {
'messageSubmit': 'Послать',
'labelUsername': 'Имя:',
+ 'labelNickname': 'Ник:',
'labelPassword': 'Пароль:',
'loginSubmit' : 'Логин',
'loginInvalid' : 'Неверный JID',
@@ -774,11 +811,12 @@ Candy.View.Translation = {
'userLeftRoom' : '%s вышел из чата.',
'userHasBeenKickedFromRoom': '%s выброшен из чата.',
'userHasBeenBannedFromRoom': '%s запрещён доступ в чат.',
+ 'userChangedNick': '%1$s сменил имя на %2$s.',
'presenceUnknownWarningSubject': 'Уведомление:',
'presenceUnknownWarning' : 'Этот пользователь вероятнее всего оффлайн.',
- 'dateFormat': 'mm.dd.yyyy',
+ 'dateFormat': 'dd.mm.yyyy',
'timeFormat': 'HH:MM:ss',
'tooltipRole' : 'Модератор',
@@ -798,6 +836,7 @@ Candy.View.Translation = {
'errorMembersOnly': 'Вы не можете войти в чат "%s": Недостаточно прав доступа.',
'errorMaxOccupantsReached': 'Вы не можете войти в чат "%s": Слишком много участников.',
+ 'errorAutojoinMissing': 'Параметры автовхода не устновлены. Настройте их для продолжения.',
'antiSpamMessage' : 'Пожалуйста не рассылайте спам. Вас заблокировали на короткое время.'
},
@@ -840,9 +879,6 @@ Candy.View.Translation = {
'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',
@@ -865,5 +901,73 @@ Candy.View.Translation = {
'errorMaxOccupantsReached': 'No pots unir-te a la sala "%s": hi ha masses participants.',
'antiSpamMessage' : 'Si us plau, no facis spam. Has estat bloquejat temporalment.'
- }
+ },
+ 'cs' : {
+ 'status': 'Stav: %s',
+ 'statusConnecting': 'Připojování...',
+ 'statusConnected': 'Připojeno',
+ 'statusDisconnecting': 'Odpojování...',
+ 'statusDisconnected': 'Odpojeno',
+ 'statusAuthfail': 'Přihlášení selhalo',
+
+ 'roomSubject': 'Předmět:',
+ 'messageSubmit': 'Odeslat',
+
+ 'labelUsername': 'Už. jméno:',
+ 'labelNickname': 'Přezdívka:',
+ 'labelPassword': 'Heslo:',
+ 'loginSubmit': 'Přihlásit se',
+ 'loginInvalid': 'Neplatné JID',
+
+ 'reason': 'Důvod:',
+ 'subject': 'Předmět:',
+ 'reasonWas': 'Důvod byl: %s.',
+ 'kickActionLabel': 'Vykopnout',
+ 'youHaveBeenKickedBy': 'Byl jsi vyloučen z %2$s uživatelem %1$s',
+ 'youHaveBeenKicked': 'Byl jsi vyloučen z %s',
+ 'banActionLabel': 'Ban',
+ 'youHaveBeenBannedBy': 'Byl jsi trvale vyloučen z %1$s uživatelem %2$s',
+ 'youHaveBeenBanned': 'Byl jsi trvale vyloučen z %s',
+
+ 'privateActionLabel': 'Soukromý chat',
+ 'ignoreActionLabel': 'Ignorovat',
+ 'unignoreActionLabel': 'Neignorovat',
+
+ 'setSubjectActionLabel': 'Změnit předmět',
+
+ 'administratorMessageSubject': 'Adminitrátor',
+
+ 'userJoinedRoom': '%s vešel do místnosti.',
+ 'userLeftRoom': '%s opustil místnost.',
+ 'userHasBeenKickedFromRoom': '%s byl vyloučen z místnosti.',
+ 'userHasBeenBannedFromRoom': '%s byl trvale vyloučen z místnosti.',
+ 'userChangedNick': '%1$s si změnil přezdívku na %2$s.',
+
+ 'presenceUnknownWarningSubject': 'Poznámka:',
+ 'presenceUnknownWarning': 'Tento uživatel může být offiline. Nemůžeme sledovat jeho přítmonost..',
+
+ 'dateFormat': 'dd.mm.yyyy',
+ 'timeFormat': 'HH:MM:ss',
+
+ 'tooltipRole': 'Moderátor',
+ 'tooltipIgnored': 'Tento uživatel je ignorován',
+ 'tooltipEmoticons': 'Emotikony',
+ 'tooltipSound': 'Přehrát zvuk při nové soukromé zprávě',
+ 'tooltipAutoscroll': 'Automaticky rolovat',
+ 'tooltipStatusmessage': 'Zobrazovat stavové zprávy',
+ 'tooltipAdministration': 'Správa místnosti',
+ 'tooltipUsercount': 'Uživatelé',
+
+ 'enterRoomPassword': 'Místnost "%s" je chráněna heslem.',
+ 'enterRoomPasswordSubmit': 'Připojit se do místnosti',
+ 'passwordEnteredInvalid': 'Neplatné heslo pro místnost "%s".',
+
+ 'nicknameConflict': 'Takové přihlašovací jméno je již použito. Vyberte si prosím jiné.',
+
+ 'errorMembersOnly': 'Nemůžete se připojit do místnosti "%s": Nedostatečné oprávnění.',
+ 'errorMaxOccupantsReached': 'Nemůžete se připojit do místnosti "%s": Příliš mnoho uživatelů.',
+ 'errorAutojoinMissing': 'Není nastaven parametr autojoin. Nastavte jej prosím.',
+
+ 'antiSpamMessage': 'Nespamujte prosím. Váš účet byl na chvilku zablokován.'
+ }
};