converse.js 104 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516
  1. /*!
  2. * Converse.js (Web-based XMPP instant messaging client)
  3. * http://conversejs.org
  4. *
  5. * Copyright (c) 2012, Jan-Carel Brand <jc@opkode.com>
  6. * Dual licensed under the MIT and GPL Licenses
  7. */
  8. // AMD/global registrations
  9. (function (root, factory) {
  10. if (console===undefined || console.log===undefined) {
  11. console = { log: function () {}, error: function () {} };
  12. }
  13. if (typeof define === 'function' && define.amd) {
  14. require.config({
  15. paths: {
  16. "sjcl": "Libraries/sjcl",
  17. "tinysort": "Libraries/jquery.tinysort",
  18. "underscore": "Libraries/underscore",
  19. "backbone": "Libraries/backbone",
  20. "localstorage": "Libraries/backbone.localStorage",
  21. "strophe": "Libraries/strophe",
  22. "strophe.muc": "Libraries/strophe.muc",
  23. "strophe.roster": "Libraries/strophe.roster",
  24. "strophe.vcard": "Libraries/strophe.vcard",
  25. "strophe.disco": "Libraries/strophe.disco"
  26. },
  27. // define module dependencies for modules not using define
  28. shim: {
  29. 'backbone': {
  30. //These script dependencies should be loaded before loading
  31. //backbone.js
  32. deps: [
  33. 'underscore',
  34. 'jquery'
  35. ],
  36. //Once loaded, use the global 'Backbone' as the
  37. //module value.
  38. exports: 'Backbone'
  39. },
  40. 'underscore': { exports: '_' },
  41. 'strophe.muc': { deps: ['strophe', 'jquery'] },
  42. 'strophe.roster': { deps: ['strophe', 'jquery'] },
  43. 'strophe.vcard': { deps: ['strophe', 'jquery'] },
  44. 'strophe.disco': { deps: ['strophe', 'jquery'] }
  45. }
  46. });
  47. define("converse", [
  48. "localstorage",
  49. "tinysort",
  50. "sjcl",
  51. "strophe.muc",
  52. "strophe.roster",
  53. "strophe.vcard",
  54. "strophe.disco"
  55. ], function() {
  56. // Use Mustache style syntax for variable interpolation
  57. _.templateSettings = {
  58. evaluate : /\{\[([\s\S]+?)\]\}/g,
  59. interpolate : /\{\{([\s\S]+?)\}\}/g
  60. };
  61. return factory(jQuery, _, console);
  62. }
  63. );
  64. } else {
  65. // Browser globals
  66. _.templateSettings = {
  67. evaluate : /\{\[([\s\S]+?)\]\}/g,
  68. interpolate : /\{\{([\s\S]+?)\}\}/g
  69. };
  70. root.converse = factory(jQuery, _, console || {log: function(){}});
  71. }
  72. }(this, function ($, _, console) {
  73. var converse = {};
  74. converse.msg_counter = 0;
  75. var strinclude = function(str, needle){
  76. if (needle === '') { return true; }
  77. if (str === null) { return false; }
  78. return String(str).indexOf(needle) !== -1;
  79. };
  80. converse.autoLink = function (text) {
  81. // Convert URLs into hyperlinks
  82. var re = /((http|https|ftp):\/\/[\w?=&.\/\-;#~%\-]+(?![\w\s?&.\/;#~%"=\-]*>))/g;
  83. return text.replace(re, '<a target="_blank" href="$1">$1</a>');
  84. };
  85. converse.toISOString = function (date) {
  86. var pad;
  87. if (typeof date.toISOString !== 'undefined') {
  88. return date.toISOString();
  89. } else {
  90. // IE <= 8 Doesn't have toISOStringMethod
  91. pad = function (num) {
  92. return (num < 10) ? '0' + num : '' + num;
  93. };
  94. return date.getUTCFullYear() + '-' +
  95. pad(date.getUTCMonth() + 1) + '-' +
  96. pad(date.getUTCDate()) + 'T' +
  97. pad(date.getUTCHours()) + ':' +
  98. pad(date.getUTCMinutes()) + ':' +
  99. pad(date.getUTCSeconds()) + '.000Z';
  100. }
  101. };
  102. converse.parseISO8601 = function (datestr) {
  103. /* Parses string formatted as 2013-02-14T11:27:08.268Z to a Date obj.
  104. */
  105.     var numericKeys = [1, 4, 5, 6, 7, 10, 11],
  106. struct = /^\s*(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}\.?\d*)Z\s*$/.exec(datestr),
  107. minutesOffset = 0,
  108. i, k;
  109. for (i = 0; (k = numericKeys[i]); ++i) {
  110. struct[k] = +struct[k] || 0;
  111. }
  112. // allow undefined days and months
  113. struct[2] = (+struct[2] || 1) - 1;
  114. struct[3] = +struct[3] || 1;
  115. if (struct[8] !== 'Z' && struct[9] !== undefined) {
  116. minutesOffset = struct[10] * 60 + struct[11];
  117. if (struct[9] === '+') {
  118. minutesOffset = 0 - minutesOffset;
  119. }
  120. }
  121. return new Date(Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]));
  122. };
  123. converse.updateMsgCounter = function () {
  124. if (this.msg_counter > 0) {
  125. if (document.title.search(/^Messages \(\d+\) /) == -1) {
  126. document.title = "Messages (" + this.msg_counter + ") " + document.title;
  127. } else {
  128. document.title = document.title.replace(/^Messages \(\d+\) /, "Messages (" + this.msg_counter + ") ");
  129. }
  130. window.blur();
  131. window.focus();
  132. } else if (document.title.search(/^Messages \(\d+\) /) != -1) {
  133. document.title = document.title.replace(/^Messages \(\d+\) /, "");
  134. }
  135. };
  136. converse.incrementMsgCounter = function () {
  137. this.msg_counter += 1;
  138. this.updateMsgCounter();
  139. };
  140. converse.clearMsgCounter = function () {
  141. this.msg_counter = 0;
  142. this.updateMsgCounter();
  143. };
  144. converse.collections = {
  145. /* FIXME: XEP-0136 specifies 'urn:xmpp:archive' but the mod_archive_odbc
  146. * add-on for ejabberd wants the URL below. This might break for other
  147. * Jabber servers.
  148. */
  149. 'URI': 'http://www.xmpp.org/extensions/xep-0136.html#ns'
  150. };
  151. converse.collections.getLastCollection = function (jid, callback) {
  152. var bare_jid = Strophe.getBareJidFromJid(jid),
  153. iq = $iq({'type':'get'})
  154. .c('list', {'xmlns': this.URI,
  155. 'with': bare_jid
  156. })
  157. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  158. .c('before').up()
  159. .c('max')
  160. .t('1');
  161. converse.connection.sendIQ(iq,
  162. callback,
  163. function () {
  164. console.log('Error while retrieving collections');
  165. });
  166. };
  167. converse.collections.getLastMessages = function (jid, callback) {
  168. var that = this;
  169. this.getLastCollection(jid, function (result) {
  170. // Retrieve the last page of a collection (max 30 elements).
  171. var $collection = $(result).find('chat'),
  172. jid = $collection.attr('with'),
  173. start = $collection.attr('start'),
  174. iq = $iq({'type':'get'})
  175. .c('retrieve', {'start': start,
  176. 'xmlns': that.URI,
  177. 'with': jid
  178. })
  179. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  180. .c('max')
  181. .t('30');
  182. converse.connection.sendIQ(iq, callback);
  183. });
  184. };
  185. converse.Message = Backbone.Model.extend();
  186. converse.Messages = Backbone.Collection.extend({
  187. model: converse.Message
  188. });
  189. converse.ChatBox = Backbone.Model.extend({
  190. initialize: function () {
  191. if (this.get('box_id') !== 'controlbox') {
  192. this.messages = new converse.Messages();
  193. this.messages.localStorage = new Backbone.LocalStorage(
  194. hex_sha1('converse.messages'+this.get('jid')));
  195. this.set({
  196. 'user_id' : Strophe.getNodeFromJid(this.get('jid')),
  197. 'box_id' : hex_sha1(this.get('jid')),
  198. 'fullname' : this.get('fullname'),
  199. 'url': this.get('url'),
  200. 'image_type': this.get('image_type'),
  201. 'image_src': this.get('image_src')
  202. });
  203. }
  204. },
  205. messageReceived: function (message) {
  206. var $message = $(message),
  207. body = converse.autoLink($message.children('body').text()),
  208. from = Strophe.getBareJidFromJid($message.attr('from')),
  209. composing = $message.find('composing'),
  210. delayed = $message.find('delay').length > 0,
  211. fullname = (this.get('fullname')||'').split(' ')[0],
  212. stamp, time, sender;
  213. if (!body) {
  214. if (composing.length) {
  215. this.messages.add({
  216. fullname: fullname,
  217. sender: 'them',
  218. delayed: delayed,
  219. time: converse.toISOString(new Date()),
  220. composing: composing.length
  221. });
  222. }
  223. } else {
  224. if (delayed) {
  225. stamp = $message.find('delay').attr('stamp');
  226. time = stamp;
  227. } else {
  228. time = converse.toISOString(new Date());
  229. }
  230. if (from == converse.bare_jid) {
  231. fullname = 'me';
  232. sender = 'me';
  233. } else {
  234. sender = 'them';
  235. }
  236. this.messages.create({
  237. fullname: fullname,
  238. sender: sender,
  239. delayed: delayed,
  240. time: time,
  241. message: body
  242. });
  243. }
  244. }
  245. });
  246. converse.ChatBoxView = Backbone.View.extend({
  247. length: 200,
  248. tagName: 'div',
  249. className: 'chatbox',
  250. events: {
  251. 'click .close-chatbox-button': 'closeChat',
  252. 'keypress textarea.chat-textarea': 'keyPressed'
  253. },
  254. message_template: _.template(
  255. '<div class="chat-message {{extra_classes}}">' +
  256. '<span class="chat-message-{{sender}}">{{time}} {{username}}:&nbsp;</span>' +
  257. '<span class="chat-message-content">{{message}}</span>' +
  258. '</div>'),
  259. action_template: _.template(
  260. '<div class="chat-message {{extra_classes}}">' +
  261. '<span class="chat-message-{{sender}}">{{time}}:&nbsp;</span>' +
  262. '<span class="chat-message-content">{{message}}</span>' +
  263. '</div>'),
  264. new_day_template: _.template(
  265. '<time class="chat-date" datetime="{{isodate}}">{{datestring}}</time>'
  266. ),
  267. insertStatusNotification: function (message, replace) {
  268. var $chat_content = this.$el.find('.chat-content');
  269. $chat_content.find('div.chat-event').remove().end()
  270. .append($('<div class="chat-event"></div>').text(message));
  271. this.scrollDown();
  272. },
  273. showMessage: function (message) {
  274. var time = message.get('time'),
  275. times = this.model.messages.pluck('time'),
  276. this_date = converse.parseISO8601(time),
  277. $chat_content = this.$el.find('.chat-content'),
  278. previous_message, idx, prev_date, isodate;
  279. // If this message is on a different day than the one received
  280. // prior, then indicate it on the chatbox.
  281. idx = _.indexOf(times, time)-1;
  282. if (idx >= 0) {
  283. previous_message = this.model.messages.at(idx);
  284. prev_date = converse.parseISO8601(previous_message.get('time'));
  285. isodate = new Date(this_date.getTime());
  286. isodate.setUTCHours(0,0,0,0);
  287. isodate = converse.toISOString(isodate);
  288. if (this.isDifferentDay(prev_date, this_date)) {
  289. $chat_content.append(this.new_day_template({
  290. isodate: isodate,
  291. datestring: this_date.toString().substring(0,15)
  292. }));
  293. }
  294. }
  295. if (message.get('composing')) {
  296. this.insertStatusNotification(message.get('fullname')+' '+'is typing');
  297. return;
  298. } else {
  299. $chat_content.find('div.chat-event').remove();
  300. $chat_content.append(
  301. this.message_template({
  302. 'sender': message.get('sender'),
  303. 'time': this_date.toLocaleTimeString().substring(0,5),
  304. 'message': message.get('message'),
  305. 'username': message.get('fullname'),
  306. 'extra_classes': message.get('delayed') && 'delayed' || ''
  307. }));
  308. }
  309. if ((message.get('sender') != 'me') && (converse.windowState == 'blur')) {
  310. converse.incrementMsgCounter();
  311. }
  312. this.scrollDown();
  313. },
  314. isDifferentDay: function (prev_date, next_date) {
  315. return (
  316. (next_date.getDate() != prev_date.getDate()) ||
  317. (next_date.getFullYear() != prev_date.getFullYear()) ||
  318. (next_date.getMonth() != prev_date.getMonth()));
  319. },
  320. addHelpMessages: function (msgs) {
  321. var $chat_content = this.$el.find('.chat-content'), i,
  322. msgs_length = msgs.length;
  323. for (i=0; i<msgs_length; i++) {
  324. $chat_content.append($('<div class="chat-help">'+msgs[i]+'</div>'));
  325. }
  326. this.scrollDown();
  327. },
  328. sendMessage: function (text) {
  329. // TODO: Look in ChatPartners to see what resources we have for the recipient.
  330. // if we have one resource, we sent to only that resources, if we have multiple
  331. // we send to the bare jid.
  332. var timestamp = (new Date()).getTime(),
  333. bare_jid = this.model.get('jid'),
  334. match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/),
  335. msgs;
  336. if (match) {
  337. if (match[1] === "clear") {
  338. this.$el.find('.chat-content').empty();
  339. this.model.messages.reset();
  340. return;
  341. }
  342. else if (match[1] === "help") {
  343. msgs = [
  344. '<strong>/help</strong>: Show this menu',
  345. '<strong>/clear</strong>: Remove messages'
  346. ];
  347. this.addHelpMessages(msgs);
  348. return;
  349. }
  350. }
  351. var message = $msg({from: converse.bare_jid, to: bare_jid, type: 'chat', id: timestamp})
  352. .c('body').t(text).up()
  353. .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'});
  354. // Forward the message, so that other connected resources are also aware of it.
  355. // TODO: Forward the message only to other connected resources (inside the browser)
  356. var forwarded = $msg({to:converse.bare_jid, type:'chat', id:timestamp})
  357. .c('forwarded', {xmlns:'urn:xmpp:forward:0'})
  358. .c('delay', {xmns:'urn:xmpp:delay',stamp:timestamp}).up()
  359. .cnode(message.tree());
  360. converse.connection.send(message);
  361. converse.connection.send(forwarded);
  362. // Add the new message
  363. this.model.messages.create({
  364. fullname: 'me',
  365. sender: 'me',
  366. time: converse.toISOString(new Date()),
  367. message: text
  368. });
  369. },
  370. keyPressed: function (ev) {
  371. var $textarea = $(ev.target),
  372. message,
  373. notify,
  374. composing;
  375. if(ev.keyCode == 13) {
  376. ev.preventDefault();
  377. message = $textarea.val();
  378. $textarea.val('').focus();
  379. if (message !== '') {
  380. if (this.model.get('chatroom')) {
  381. this.sendChatRoomMessage(message);
  382. } else {
  383. this.sendMessage(message);
  384. }
  385. }
  386. this.$el.data('composing', false);
  387. } else if (!this.model.get('chatroom')) {
  388. // composing data is only for single user chat
  389. composing = this.$el.data('composing');
  390. if (!composing) {
  391. if (ev.keyCode != 47) {
  392. // We don't send composing messages if the message
  393. // starts with forward-slash.
  394. notify = $msg({'to':this.model.get('jid'), 'type': 'chat'})
  395. .c('composing', {'xmlns':'http://jabber.org/protocol/chatstates'});
  396. converse.connection.send(notify);
  397. }
  398. this.$el.data('composing', true);
  399. }
  400. }
  401. },
  402. onChange: function (item, changed) {
  403. if (_.has(item.changed, 'chat_status')) {
  404. var chat_status = item.get('chat_status'),
  405. fullname = item.get('fullname');
  406. if (this.$el.is(':visible')) {
  407. if (chat_status === 'offline') {
  408. this.insertStatusNotification(fullname+' '+'has gone offline');
  409. } else if (chat_status === 'away') {
  410. this.insertStatusNotification(fullname+' '+'has gone away');
  411. } else if ((chat_status === 'dnd')) {
  412. this.insertStatusNotification(fullname+' '+'is busy');
  413. } else if (chat_status === 'online') {
  414. this.$el.find('div.chat-event').remove();
  415. }
  416. }
  417. } if (_.has(item.changed, 'status')) {
  418. this.showStatusMessage(item.get('status'));
  419. }
  420. },
  421. showStatusMessage: function (msg) {
  422. this.$el.find('p.user-custom-message').text(msg).attr('title', msg);
  423. },
  424. closeChat: function () {
  425. if (converse.connection) {
  426. this.model.destroy();
  427. } else {
  428. this.model.trigger('hide');
  429. }
  430. },
  431. initialize: function (){
  432. this.model.messages.on('add', this.showMessage, this);
  433. this.model.on('show', this.show, this);
  434. this.model.on('destroy', this.hide, this);
  435. this.model.on('change', this.onChange, this);
  436. this.$el.appendTo(converse.chatboxesview.$el);
  437. this.render().show().model.messages.fetch({add: true});
  438. if (this.model.get('status')) {
  439. this.showStatusMessage(this.model.get('status'));
  440. }
  441. },
  442. template: _.template(
  443. '<div class="chat-head chat-head-chatbox">' +
  444. '<a class="close-chatbox-button">X</a>' +
  445. '<a href="{{url}}" target="_blank" class="user">' +
  446. '<div class="chat-title"> {{ fullname }} </div>' +
  447. '</a>' +
  448. '<p class="user-custom-message"><p/>' +
  449. '</div>' +
  450. '<div class="chat-content"></div>' +
  451. '<form class="sendXMPPMessage" action="" method="post">' +
  452. '<textarea ' +
  453. 'type="text" ' +
  454. 'class="chat-textarea" ' +
  455. 'placeholder="Personal message"/>'+
  456. '</form>'),
  457. render: function () {
  458. this.$el.attr('id', this.model.get('box_id'))
  459. .html(this.template(this.model.toJSON()));
  460. if (this.model.get('image')) {
  461. var img_src = 'data:'+this.model.get('image_type')+';base64,'+this.model.get('image'),
  462. canvas = $('<canvas height="35px" width="35px" class="avatar"></canvas>'),
  463. ctx = canvas.get(0).getContext('2d'),
  464. img = new Image(); // Create new Image object
  465. img.onload = function() {
  466. var ratio = img.width/img.height;
  467. ctx.drawImage(img,0,0, 35*ratio, 35);
  468. };
  469. img.src = img_src;
  470. this.$el.find('.chat-title').before(canvas);
  471. }
  472. return this;
  473. },
  474. focus: function () {
  475. this.$el.find('.chat-textarea').focus();
  476. return this;
  477. },
  478. hide: function () {
  479. if (converse.animate) {
  480. this.$el.hide('fast');
  481. } else {
  482. this.$el.hide();
  483. }
  484. },
  485. show: function () {
  486. if (this.$el.is(':visible') && this.$el.css('opacity') == "1") {
  487. return this.focus();
  488. }
  489. if (converse.animate) {
  490. this.$el.css({'opacity': 0, 'display': 'inline'}).animate({opacity: '1'}, 200);
  491. } else {
  492. this.$el.css({'opacity': 1, 'display': 'inline'});
  493. }
  494. if (converse.connection) {
  495. // Without a connection, we haven't yet initialized
  496. // localstorage
  497. this.model.save();
  498. }
  499. return this;
  500. },
  501. scrollDown: function () {
  502. var $content = this.$el.find('.chat-content');
  503. $content.scrollTop($content[0].scrollHeight);
  504. return this;
  505. }
  506. });
  507. converse.ContactsPanel = Backbone.View.extend({
  508. tagName: 'div',
  509. className: 'oc-chat-content',
  510. id: 'users',
  511. events: {
  512. 'click a.toggle-xmpp-contact-form': 'toggleContactForm',
  513. 'submit form.add-xmpp-contact': 'addContactFromForm',
  514. 'submit form.search-xmpp-contact': 'searchContacts',
  515. 'click a.subscribe-to-user': 'addContactFromList'
  516. },
  517. tab_template: _.template('<li><a class="s current" href="#users">Contacts</a></li>'),
  518. template: _.template(
  519. '<form class="set-xmpp-status" action="" method="post">'+
  520. '<span id="xmpp-status-holder">'+
  521. '<select id="select-xmpp-status">'+
  522. '<option value="online">Online</option>'+
  523. '<option value="dnd">Busy</option>'+
  524. '<option value="away">Away</option>'+
  525. '<option value="offline">Offline</option>'+
  526. '</select>'+
  527. '</span>'+
  528. '</form>'+
  529. '<dl class="add-converse-contact dropdown">' +
  530. '<dt id="xmpp-contact-search" class="fancy-dropdown">' +
  531. '<a class="toggle-xmpp-contact-form" href="#" title="Click to add new chat contacts">Add a contact</a>' +
  532. '</dt>' +
  533. '<dd class="search-xmpp" style="display:none"><ul></ul></dd>' +
  534. '</dl>'
  535. ),
  536. add_contact_template: _.template(
  537. '<li>'+
  538. '<form class="add-xmpp-contact">' +
  539. '<input type="text" name="identifier" class="username" placeholder="Contact username"/>' +
  540. '<button type="submit">Add</button>' +
  541. '</form>'+
  542. '<li>'
  543. ),
  544. search_contact_template: _.template(
  545. '<li>'+
  546. '<form class="search-xmpp-contact">' +
  547. '<input type="text" name="identifier" class="username" placeholder="Contact name"/>' +
  548. '<button type="submit">Search</button>' +
  549. '</form>'+
  550. '<li>'
  551. ),
  552. render: function () {
  553. var markup;
  554. this.$parent.find('#controlbox-tabs').append(this.tab_template());
  555. this.$parent.find('#controlbox-panes').append(this.$el.html(this.template()));
  556. if (converse.xhr_user_search) {
  557. markup = this.search_contact_template();
  558. } else {
  559. markup = this.add_contact_template();
  560. }
  561. this.$el.find('.search-xmpp ul').append(markup);
  562. return this;
  563. },
  564. toggleContactForm: function (ev) {
  565. ev.preventDefault();
  566. this.$el.find('.search-xmpp').toggle('fast', function () {
  567. if ($(this).is(':visible')) {
  568. $(this).find('input.username').focus();
  569. }
  570. });
  571. },
  572. searchContacts: function (ev) {
  573. ev.preventDefault();
  574. $.getJSON(portal_url + "/search-users?q=" + $(ev.target).find('input.username').val(), function (data) {
  575. var $ul= $('.search-xmpp ul');
  576. $ul.find('li.found-user').remove();
  577. $ul.find('li.chat-help').remove();
  578. if (!data.length) {
  579. $ul.append('<li class="chat-help">No users found</li>');
  580. }
  581. $(data).each(function (idx, obj) {
  582. $ul.append(
  583. $('<li class="found-user"></li>')
  584. .append(
  585. $('<a class="subscribe-to-user" href="#" title="Click to add as a chat contact"></a>')
  586. .attr('data-recipient', Strophe.escapeNode(obj.id)+'@'+converse.domain)
  587. .text(obj.fullname)
  588. )
  589. );
  590. });
  591. });
  592. },
  593. addContactFromForm: function (ev) {
  594. ev.preventDefault();
  595. var $input = $(ev.target).find('input');
  596. var jid = $input.val();
  597. if (! jid) {
  598. // this is not a valid JID
  599. $input.addClass('error');
  600. return;
  601. }
  602. converse.getVCard(
  603. jid,
  604. $.proxy(function (jid, fullname, image, image_type, url) {
  605. this.addContact(jid, fullname);
  606. }, this),
  607. $.proxy(function (stanza) {
  608. console.log("An error occured while fetching vcard");
  609. if ($(stanza).find('error').attr('code') == '503') {
  610. // If we get service-unavailable, we continue to create
  611. // the user
  612. var jid = $(stanza).attr('from');
  613. this.addContact(jid, jid);
  614. }
  615. }, this));
  616. $('.search-xmpp').hide();
  617. },
  618. addContactFromList: function (ev) {
  619. ev.preventDefault();
  620. var $target = $(ev.target),
  621. jid = $target.attr('data-recipient'),
  622. name = $target.text();
  623. this.addContact(jid, name);
  624. $target.parent().remove();
  625. $('.search-xmpp').hide();
  626. },
  627. addContact: function (jid, name) {
  628. converse.connection.roster.add(jid, name, [], function (iq) {
  629. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  630. });
  631. }
  632. });
  633. converse.RoomsPanel = Backbone.View.extend({
  634. tagName: 'div',
  635. id: 'chatrooms',
  636. events: {
  637. 'submit form.add-chatroom': 'createChatRoom',
  638. 'click input#show-rooms': 'showRooms',
  639. 'click a.open-room': 'createChatRoom',
  640. 'click a.room-info': 'showRoomInfo'
  641. },
  642. room_template: _.template(
  643. '<dd class="available-chatroom">'+
  644. '<a class="open-room" data-room-jid="{{jid}}" title="Click to open this room" href="#">{{name}}</a>'+
  645. '<a class="room-info" data-room-jid="{{jid}}" title="Show more information on this room" href="#">&nbsp;</a>'+
  646. '</dd>'),
  647. room_description_template: _.template(
  648. '<div class="room-info">'+
  649. '<p class="room-info"><strong>Description:</strong> {{desc}}</p>' +
  650. '<p class="room-info"><strong>Occupants:</strong> {{occ}}</p>' +
  651. '<p class="room-info"><strong>Features:</strong> <ul>'+
  652. '{[ if (passwordprotected) { ]}' +
  653. '<li class="room-info locked">Requires authentication</li>' +
  654. '{[ } ]}' +
  655. '{[ if (hidden) { ]}' +
  656. '<li class="room-info">Hidden</li>' +
  657. '{[ } ]}' +
  658. '{[ if (membersonly) { ]}' +
  659. '<li class="room-info">Requires an invitation</li>' +
  660. '{[ } ]}' +
  661. '{[ if (moderated) { ]}' +
  662. '<li class="room-info">Moderated</li>' +
  663. '{[ } ]}' +
  664. '{[ if (nonanonymous) { ]}' +
  665. '<li class="room-info">Non-anonymous</li>' +
  666. '{[ } ]}' +
  667. '{[ if (open) { ]}' +
  668. '<li class="room-info">Open room</li>' +
  669. '{[ } ]}' +
  670. '{[ if (persistent) { ]}' +
  671. '<li class="room-info">Permanent room</li>' +
  672. '{[ } ]}' +
  673. '{[ if (publicroom) { ]}' +
  674. '<li class="room-info">Public</li>' +
  675. '{[ } ]}' +
  676. '{[ if (semianonymous) { ]}' +
  677. '<li class="room-info">Semi-anonymous</li>' +
  678. '{[ } ]}' +
  679. '{[ if (temporary) { ]}' +
  680. '<li class="room-info">Temporary room</li>' +
  681. '{[ } ]}' +
  682. '{[ if (unmoderated) { ]}' +
  683. '<li class="room-info">Unmoderated</li>' +
  684. '{[ } ]}' +
  685. '</p>' +
  686. '</div>'
  687. ),
  688. tab_template: _.template('<li><a class="s" href="#chatrooms">Rooms</a></li>'),
  689. template: _.template(
  690. '<form class="add-chatroom" action="" method="post">'+
  691. '<legend>'+
  692. '<input type="text" name="chatroom" class="new-chatroom-name" placeholder="Room name"/>'+
  693. '<input type="text" name="server" class="new-chatroom-server" placeholder="Server"/>'+
  694. '</legend>'+
  695. '<input type="submit" name="join" value="Join"/>'+
  696. '<input type="button" name="show" id="show-rooms" value="Show rooms"/>'+
  697. '</form>'+
  698. '<dl id="available-chatrooms"></dl>'),
  699. render: function () {
  700. this.$parent.find('#controlbox-tabs').append(this.tab_template());
  701. this.$parent.find('#controlbox-panes').append(this.$el.html(this.template()).hide());
  702. return this;
  703. },
  704. initialize: function () {
  705. this.on('update-rooms-list', function (ev) {
  706. this.updateRoomsList();
  707. });
  708. },
  709. updateRoomsList: function (domain) {
  710. converse.connection.muc.listRooms(
  711. this.muc_domain,
  712. $.proxy(function (iq) { // Success
  713. var name, jid, i, fragment,
  714. that = this,
  715. $available_chatrooms = this.$el.find('#available-chatrooms');
  716. this.rooms = $(iq).find('query').find('item');
  717. if (this.rooms.length) {
  718. $available_chatrooms.html('<dt>Rooms on '+this.muc_domain+'</dt>');
  719. fragment = document.createDocumentFragment();
  720. for (i=0; i<this.rooms.length; i++) {
  721. name = Strophe.unescapeNode($(this.rooms[i]).attr('name')||$(this.rooms[i]).attr('jid'));
  722. jid = $(this.rooms[i]).attr('jid');
  723. fragment.appendChild($(this.room_template({
  724. 'name':name,
  725. 'jid':jid
  726. }))[0]);
  727. }
  728. $available_chatrooms.append(fragment);
  729. $('input#show-rooms').show().siblings('img.spinner').remove();
  730. } else {
  731. $available_chatrooms.html('<dt>No rooms on '+this.muc_domain+'</dt>');
  732. $('input#show-rooms').show().siblings('img.spinner').remove();
  733. }
  734. return true;
  735. }, this),
  736. $.proxy(function (iq) { // Failure
  737. var $available_chatrooms = this.$el.find('#available-chatrooms');
  738. $available_chatrooms.html('<dt>No rooms on '+this.muc_domain+'</dt>');
  739. $('input#show-rooms').show().siblings('img.spinner').remove();
  740. }, this));
  741. },
  742. showRooms: function (ev) {
  743. var $available_chatrooms = this.$el.find('#available-chatrooms');
  744. var $server = this.$el.find('input.new-chatroom-server');
  745. var server = $server.val();
  746. if (!server) {
  747. $server.addClass('error');
  748. return;
  749. }
  750. this.$el.find('input.new-chatroom-name').removeClass('error');
  751. $server.removeClass('error');
  752. $available_chatrooms.empty();
  753. $('input#show-rooms').hide().after('<img class="spinner" style="width: auto" src="images/spinner.gif"/>');
  754. this.muc_domain = server;
  755. this.updateRoomsList();
  756. },
  757. showRoomInfo: function (ev) {
  758. var target = ev.target,
  759. $dd = $(target).parent('dd'),
  760. $div = $dd.find('div.room-info');
  761. if ($div.length) {
  762. $div.remove();
  763. } else {
  764. $dd.append('<img class="spinner" src="images/spinner.gif"/>');
  765. converse.connection.disco.info(
  766. $(target).attr('data-room-jid'),
  767. null,
  768. $.proxy(function (stanza) {
  769. var $stanza = $(stanza);
  770. // All MUC features shown here: http://xmpp.org/registrar/disco-features.html
  771. var desc = $stanza.find('field[var="muc#roominfo_description"] value').text();
  772. var occ = $stanza.find('field[var="muc#roominfo_occupants"] value').text();
  773. var hidden = $stanza.find('feature[var="muc_hidden"]').length;
  774. var membersonly = $stanza.find('feature[var="muc_membersonly"]').length;
  775. var moderated = $stanza.find('feature[var="muc_moderated"]').length;
  776. var nonanonymous = $stanza.find('feature[var="muc_nonanonymous"]').length;
  777. var open = $stanza.find('feature[var="muc_open"]').length;
  778. var passwordprotected = $stanza.find('feature[var="muc_passwordprotected"]').length;
  779. var persistent = $stanza.find('feature[var="muc_persistent"]').length;
  780. var publicroom = $stanza.find('feature[var="muc_public"]').length;
  781. var semianonymous = $stanza.find('feature[var="muc_semianonymous"]').length;
  782. var temporary = $stanza.find('feature[var="muc_temporary"]').length;
  783. var unmoderated = $stanza.find('feature[var="muc_unmoderated"]').length;
  784. $dd.find('img.spinner').replaceWith(
  785. this.room_description_template({
  786. 'desc':desc,
  787. 'occ':occ,
  788. 'hidden':hidden,
  789. 'membersonly':membersonly,
  790. 'moderated':moderated,
  791. 'nonanonymous':nonanonymous,
  792. 'open':open,
  793. 'passwordprotected':passwordprotected,
  794. 'persistent':persistent,
  795. 'publicroom': publicroom,
  796. 'semianonymous':semianonymous,
  797. 'temporary':temporary,
  798. 'unmoderated':unmoderated
  799. }));
  800. }, this));
  801. }
  802. },
  803. createChatRoom: function (ev) {
  804. ev.preventDefault();
  805. var name, server, jid, $name, $server, errors;
  806. if (ev.type === 'click') {
  807. jid = $(ev.target).attr('data-room-jid');
  808. } else {
  809. $name = this.$el.find('input.new-chatroom-name');
  810. $server= this.$el.find('input.new-chatroom-server');
  811. server = $server.val();
  812. name = $name.val().trim().toLowerCase();
  813. $name.val(''); // Clear the input
  814. if (name && server) {
  815. jid = Strophe.escapeNode(name) + '@' + server;
  816. $name.removeClass('error');
  817. $server.removeClass('error');
  818. this.muc_domain = server;
  819. } else {
  820. errors = true;
  821. if (!name) { $name.addClass('error'); }
  822. if (!server) { $server.addClass('error'); }
  823. return;
  824. }
  825. }
  826. converse.chatboxes.create({
  827. 'id': jid,
  828. 'jid': jid,
  829. 'name': Strophe.unescapeNode(Strophe.getNodeFromJid(jid)),
  830. 'nick': converse.xmppstatus.get('fullname')||converse.bare_jid,
  831. 'chatroom': true,
  832. 'box_id' : hex_sha1(jid)
  833. });
  834. }
  835. });
  836. converse.ControlBoxView = converse.ChatBoxView.extend({
  837. tagName: 'div',
  838. className: 'chatbox',
  839. id: 'controlbox',
  840. events: {
  841. 'click a.close-chatbox-button': 'closeChat',
  842. 'click ul#controlbox-tabs li a': 'switchTab'
  843. },
  844. initialize: function () {
  845. this.$el.appendTo(converse.chatboxesview.$el);
  846. this.model.on('change', $.proxy(function (item, changed) {
  847. var i;
  848. if (_.has(item.changed, 'connected')) {
  849. this.render();
  850. converse.features.on('add', $.proxy(this.featureAdded, this));
  851. // Features could have been added before the controlbox was
  852. // initialized. Currently we're only interested in MUC
  853. var feature = converse.features.findWhere({'var': 'http://jabber.org/protocol/muc'});
  854. if (feature) {
  855. this.featureAdded(feature);
  856. }
  857. }
  858. if (_.has(item.changed, 'visible')) {
  859. if (item.changed.visible === true) {
  860. this.show();
  861. }
  862. }
  863. }, this));
  864. this.model.on('show', this.show, this);
  865. this.model.on('destroy', this.hide, this);
  866. this.model.on('hide', this.hide, this);
  867. if (this.model.get('visible')) {
  868. this.show();
  869. }
  870. },
  871. featureAdded: function (feature) {
  872. if (feature.get('var') == 'http://jabber.org/protocol/muc') {
  873. this.roomspanel.muc_domain = feature.get('from');
  874. var $server= this.$el.find('input.new-chatroom-server');
  875. if (! $server.is(':focus')) {
  876. $server.val(this.roomspanel.muc_domain);
  877. }
  878. if (converse.auto_list_rooms) {
  879. this.roomspanel.trigger('update-rooms-list');
  880. }
  881. }
  882. },
  883. template: _.template(
  884. '<div class="chat-head oc-chat-head">'+
  885. '<ul id="controlbox-tabs"></ul>'+
  886. '<a class="close-chatbox-button">X</a>'+
  887. '</div>'+
  888. '<div id="controlbox-panes"></div>'
  889. ),
  890. switchTab: function (ev) {
  891. ev.preventDefault();
  892. var $tab = $(ev.target),
  893. $sibling = $tab.parent().siblings('li').children('a'),
  894. $tab_panel = $($tab.attr('href')),
  895. $sibling_panel = $($sibling.attr('href'));
  896. $sibling_panel.fadeOut('fast', function () {
  897. $sibling.removeClass('current');
  898. $tab.addClass('current');
  899. $tab_panel.fadeIn('fast', function () {
  900. });
  901. });
  902. },
  903. addHelpMessages: function (msgs) {
  904. // Override addHelpMessages in ChatBoxView, for now do nothing.
  905. return;
  906. },
  907. render: function () {
  908. this.$el.html(this.template(this.model.toJSON()));
  909. if ((!converse.prebind) && (!converse.connection)) {
  910. // Add login panel if the user still has to authenticate
  911. this.loginpanel = new converse.LoginPanel();
  912. this.loginpanel.$parent = this.$el;
  913. this.loginpanel.render();
  914. } else {
  915. this.contactspanel = new converse.ContactsPanel();
  916. this.contactspanel.$parent = this.$el;
  917. this.contactspanel.render();
  918. this.roomspanel = new converse.RoomsPanel();
  919. this.roomspanel.$parent = this.$el;
  920. this.roomspanel.render();
  921. }
  922. return this;
  923. }
  924. });
  925. converse.ChatRoomView = converse.ChatBoxView.extend({
  926. length: 300,
  927. tagName: 'div',
  928. className: 'chatroom',
  929. events: {
  930. 'click a.close-chatbox-button': 'closeChat',
  931. 'click a.configure-chatroom-button': 'configureChatRoom',
  932. 'keypress textarea.chat-textarea': 'keyPressed'
  933. },
  934. info_template: _.template('<div class="chat-event">{{message}}</div>'),
  935. sendChatRoomMessage: function (body) {
  936. var match = body.replace(/^\s*/, "").match(/^\/(.*?)(?: (.*))?$/) || [false],
  937. $chat_content;
  938. switch (match[1]) {
  939. case 'msg':
  940. // TODO: Private messages
  941. break;
  942. case 'topic':
  943. converse.connection.muc.setTopic(this.model.get('jid'), match[2]);
  944. break;
  945. case 'kick':
  946. converse.connection.muc.kick(this.model.get('jid'), match[2]);
  947. break;
  948. case 'ban':
  949. converse.connection.muc.ban(this.model.get('jid'), match[2]);
  950. break;
  951. case 'op':
  952. converse.connection.muc.op(this.model.get('jid'), match[2]);
  953. break;
  954. case 'deop':
  955. converse.connection.muc.deop(this.model.get('jid'), match[2]);
  956. break;
  957. case 'help':
  958. $chat_content = this.$el.find('.chat-content');
  959. $chat_content.append('<div class="chat-help"><strong>/help</strong>: Show this menu</div>' +
  960. '<div class="chat-help"><strong>/topic</strong>: Set chatroom topic</div>');
  961. /* TODO:
  962. $chat_content.append($('<div class="chat-help"><strong>/kick</strong>: Kick out user</div>'));
  963. $chat_content.append($('<div class="chat-help"><strong>/ban</strong>: Ban user</div>'));
  964. $chat_content.append($('<div class="chat-help"><strong>/op $user</strong>: Remove messages</div>'));
  965. $chat_content.append($('<div class="chat-help"><strong>/deop $user</strong>: Remove messages</div>'));
  966. */
  967. this.scrollDown();
  968. break;
  969. default:
  970. this.last_msgid = converse.connection.muc.groupchat(this.model.get('jid'), body);
  971. break;
  972. }
  973. },
  974. template: _.template(
  975. '<div class="chat-head chat-head-chatroom">' +
  976. '<a class="close-chatbox-button">X</a>' +
  977. '<a class="configure-chatroom-button" style="display:none">&nbsp;</a>' +
  978. '<div class="chat-title"> {{ name }} </div>' +
  979. '<p class="chatroom-topic"><p/>' +
  980. '</div>' +
  981. '<div class="chat-body">' +
  982. '<img class="spinner centered" src="images/spinner.gif"/>' +
  983. '</div>'),
  984. chatarea_template: _.template(
  985. '<div class="chat-area">' +
  986. '<div class="chat-content"></div>' +
  987. '<form class="sendXMPPMessage" action="" method="post">' +
  988. '<textarea type="text" class="chat-textarea" ' +
  989. 'placeholder="Message"/>' +
  990. '</form>' +
  991. '</div>' +
  992. '<div class="participants">' +
  993. '<ul class="participant-list"></ul>' +
  994. '</div>'),
  995. render: function () {
  996. this.$el.attr('id', this.model.get('box_id'))
  997. .html(this.template(this.model.toJSON()));
  998. return this;
  999. },
  1000. renderChatArea: function () {
  1001. this.$el.find('img.spinner.centered').remove();
  1002. this.$el.find('.chat-body').append(this.chatarea_template());
  1003. return this;
  1004. },
  1005. initialize: function () {
  1006. converse.connection.muc.join(
  1007. this.model.get('jid'),
  1008. this.model.get('nick'),
  1009. $.proxy(this.onChatRoomMessage, this),
  1010. $.proxy(this.onChatRoomPresence, this),
  1011. $.proxy(this.onChatRoomRoster, this),
  1012. null);
  1013. this.model.messages.on('add', this.showMessage, this);
  1014. this.model.on('destroy', function (model, response, options) {
  1015. this.$el.hide('fast');
  1016. converse.connection.muc.leave(
  1017. this.model.get('jid'),
  1018. this.model.get('nick'),
  1019. this.onLeave,
  1020. undefined);
  1021. },
  1022. this);
  1023. this.$el.appendTo(converse.chatboxesview.$el);
  1024. this.render().show().model.messages.fetch({add: true});
  1025. },
  1026. onLeave: function () {},
  1027. form_input_template: _.template('<label>{{label}}<input name="{{name}}" type="{{type}}" value="{{value}}"></label>'),
  1028. select_option_template: _.template('<option value="{{value}}">{{label}}</option>'),
  1029. form_select_template: _.template('<label>{{label}}<select name="{{name}}">{{options}}</select></label>'),
  1030. form_checkbox_template: _.template('<label>{{label}}<input name="{{name}}" type="{{type}}" {{checked}}"></label>'),
  1031. renderConfigurationForm: function (stanza) {
  1032. var $form= this.$el.find('form.chatroom-form'),
  1033. $stanza = $(stanza),
  1034. $fields = $stanza.find('field'),
  1035. title = $stanza.find('title').text(),
  1036. instructions = $stanza.find('instructions').text(),
  1037. i, j, options=[];
  1038. var input_types = {
  1039. 'text-private': 'password',
  1040. 'text-single': 'textline',
  1041. 'boolean': 'checkbox',
  1042. 'hidden': 'hidden',
  1043. 'list-single': 'dropdown'
  1044. };
  1045. $form.find('img.spinner').remove();
  1046. $form.append($('<legend>').text(title));
  1047. if (instructions != title) {
  1048. $form.append($('<p>').text(instructions));
  1049. }
  1050. for (i=0; i<$fields.length; i++) {
  1051. $field = $($fields[i]);
  1052. if ($field.attr('type') == 'list-single') {
  1053. $options = $field.find('option');
  1054. for (j=0; j<$options.length; j++) {
  1055. options.push(this.select_option_template({
  1056. value: $($options[j]).find('value').text(),
  1057. label: $($options[j]).attr('label')
  1058. }));
  1059. }
  1060. $form.append(this.form_select_template({
  1061. name: $field.attr('var'),
  1062. label: $field.attr('label'),
  1063. options: options.join('')
  1064. }));
  1065. } else if ($field.attr('type') == 'boolean') {
  1066. $form.append(this.form_checkbox_template({
  1067. name: $field.attr('var'),
  1068. type: input_types[$field.attr('type')],
  1069. label: $field.attr('label') || '',
  1070. checked: $field.find('value').text() && 'checked="1"' || ''
  1071. }));
  1072. } else {
  1073. $form.append(this.form_input_template({
  1074. name: $field.attr('var'),
  1075. type: input_types[$field.attr('type')],
  1076. label: $field.attr('label') || '',
  1077. value: $field.find('value').text()
  1078. }));
  1079. }
  1080. }
  1081. $form.append('<input type="submit" value="Save"/>');
  1082. $form.append('<input type="button" value="Cancel"/>');
  1083. $form.on('submit', $.proxy(this.saveConfiguration, this));
  1084. $form.find('input[type=button]').on('click', $.proxy(this.cancelConfiguration, this));
  1085. },
  1086. field_template: _.template('<field var="{{name}}"><value>{{value}}</value></field>'),
  1087. saveConfiguration: function (ev) {
  1088. ev.preventDefault();
  1089. var that = this;
  1090. var $inputs = $(ev.target).find(':input:not([type=button]):not([type=submit])'),
  1091. count = $inputs.length,
  1092. configArray = [];
  1093. $inputs.each(function () {
  1094. var $input = $(this), value;
  1095. if ($input.is('[type=checkbox]')) {
  1096. value = $input.is(':checked') && 1 || 0;
  1097. } else {
  1098. value = $input.val();
  1099. }
  1100. var cnode = $(that.field_template({
  1101. name: $input.attr('name'),
  1102. value: value
  1103. }))[0];
  1104. configArray.push(cnode);
  1105. if (!--count) {
  1106. converse.connection.muc.saveConfiguration(
  1107. that.model.get('jid'),
  1108. configArray,
  1109. that.onConfigSaved,
  1110. that.onErrorConfigSaved
  1111. );
  1112. }
  1113. });
  1114. this.$el.find('div.chatroom-form-container').hide(
  1115. function () {
  1116. $(this).remove();
  1117. that.$el.find('.chat-area').show();
  1118. that.$el.find('.participants').show();
  1119. });
  1120. },
  1121. onConfigSaved: function (stanza) {
  1122. // XXX
  1123. },
  1124. onErrorConfigSaved: function (stanza) {
  1125. // XXX
  1126. },
  1127. cancelConfiguration: function (ev) {
  1128. ev.preventDefault();
  1129. var that = this;
  1130. this.$el.find('div.chatroom-form-container').hide(
  1131. function () {
  1132. $(this).remove();
  1133. that.$el.find('.chat-area').show();
  1134. that.$el.find('.participants').show();
  1135. });
  1136. },
  1137. configureChatRoom: function (ev) {
  1138. ev.preventDefault();
  1139. if (this.$el.find('div.chatroom-form-container').length) {
  1140. return;
  1141. }
  1142. this.$el.find('.chat-area').hide();
  1143. this.$el.find('.participants').hide();
  1144. this.$el.find('.chat-body').append(
  1145. $('<div class="chatroom-form-container">'+
  1146. '<form class="chatroom-form">'+
  1147. '<img class="spinner centered" src="images/spinner.gif"/>'+
  1148. '</form>'+
  1149. '</div>'));
  1150. converse.connection.muc.configure(
  1151. this.model.get('jid'),
  1152. $.proxy(this.renderConfigurationForm, this)
  1153. );
  1154. },
  1155. renderPasswordForm: function () {
  1156. this.$el.find('img.centered.spinner').remove();
  1157. this.$el.find('.chat-body').append(
  1158. $('<div class="chatroom-form-container">'+
  1159. '<form class="chatroom-form">'+
  1160. '<legend>This chat room requires a password</legend>' +
  1161. '<label>Password: <input type="password" name="password"/></label>' +
  1162. '<input type="submit"/>' +
  1163. '</form>'+
  1164. '</div>'));
  1165. this.$el.find('.chatroom-form').on('submit', $.proxy(this.submitPassword, this));
  1166. },
  1167. renderErrorMessage: function (msg) {
  1168. this.$el.find('img.centered.spinner').remove();
  1169. this.$el.find('.chat-body').append($('<p>'+msg+'</p>'));
  1170. },
  1171. submitPassword: function (ev) {
  1172. ev.preventDefault();
  1173. var password = this.$el.find('.chatroom-form').find('input[type=password]').val();
  1174. this.$el.find('.chatroom-form-container').replaceWith(
  1175. '<img class="spinner centered" src="images/spinner.gif"/>');
  1176. converse.connection.muc.join(
  1177. this.model.get('jid'),
  1178. this.model.get('nick'),
  1179. $.proxy(this.onChatRoomMessage, this),
  1180. $.proxy(this.onChatRoomPresence, this),
  1181. $.proxy(this.onChatRoomRoster, this),
  1182. password);
  1183. },
  1184. onChatRoomPresence: function (presence, room) {
  1185. var nick = room.nick,
  1186. $presence = $(presence),
  1187. from = $presence.attr('from'), $item;
  1188. if ($presence.attr('type') !== 'error') {
  1189. if (!this.$el.find('.chat-area').length) { this.renderChatArea(); }
  1190. if ($presence.find("status[code='201']").length) {
  1191. // This is a new chatroom. We create an instant
  1192. // chatroom, and let the user manually set any
  1193. // configuration setting.
  1194. converse.connection.muc.createInstantRoom(room.name);
  1195. }
  1196. // check for status 110 to see if it's our own presence
  1197. if ($presence.find("status[code='110']").length) {
  1198. $item = $presence.find('item');
  1199. if ($item.length) {
  1200. if ($item.attr('affiliation') == 'owner') {
  1201. this.$el.find('a.configure-chatroom-button').show();
  1202. }
  1203. }
  1204. if ($presence.find("status[code='210']").length) {
  1205. // check if server changed our nick
  1206. this.model.set({'nick': Strophe.getResourceFromJid(from)});
  1207. }
  1208. }
  1209. } else {
  1210. var $error = $presence.find('error'),
  1211. $chat_content = this.$el.find('.chat-content');
  1212. // We didn't enter the room, so we must remove it from the MUC
  1213. // add-on
  1214. converse.connection.muc.removeRoom(room.name);
  1215. if ($error.attr('type') == 'auth') {
  1216. if ($error.find('not-authorized').length) {
  1217. this.renderPasswordForm();
  1218. } else if ($error.find('registration-required').length) {
  1219. this.renderErrorMessage('You are not on the member list of this room');
  1220. } else if ($error.find('forbidden').length) {
  1221. this.renderErrorMessage('You have been banned from this room');
  1222. }
  1223. } else if ($error.attr('type') == 'modify') {
  1224. if ($error.find('jid-malformed').length) {
  1225. this.renderErrorMessage('No nickname was specified');
  1226. }
  1227. } else if ($error.attr('type') == 'cancel') {
  1228. if ($error.find('not-allowed').length) {
  1229. this.renderErrorMessage('You are not allowed to create new rooms');
  1230. } else if ($error.find('not-acceptable').length) {
  1231. this.renderErrorMessage("Your nickname doesn't conform to this room's policies");
  1232. } else if ($error.find('conflict').length) {
  1233. this.renderErrorMessage("Your nickname is already taken");
  1234. } else if ($error.find('item-not-found').length) {
  1235. this.renderErrorMessage("This room does not (yet) exist");
  1236. } else if ($error.find('service-unavailable').length) {
  1237. this.renderErrorMessage("This room has reached it's maximum number of occupants");
  1238. }
  1239. }
  1240. }
  1241. return true;
  1242. },
  1243. communicateStatusCodes: function ($message, $chat_content) {
  1244. /* Parse the message for status codes and communicate their purpose
  1245. * to the user.
  1246. * See: http://xmpp.org/registrar/mucstatus.html
  1247. */
  1248. var status_messages = {
  1249. 100: 'This room is not anonymous',
  1250. 102: 'This room now shows unavailable members',
  1251. 103: 'This room does not show unavailable members',
  1252. 104: 'Non-privacy-related room configuration has changed',
  1253. 170: 'Room logging is now enabled',
  1254. 171: 'Room logging is now disabled',
  1255. 172: 'This room is now non-anonymous',
  1256. 173: 'This room is now semi-anonymous',
  1257. 174: 'This room is now fully-anonymous'
  1258. };
  1259. $message.find('x').find('status').each($.proxy(function (idx, item) {
  1260. var code = $(item).attr('code');
  1261. var message = code && status_messages[code] || null;
  1262. if (message) {
  1263. $chat_content.append(this.info_template({message: message}));
  1264. }
  1265. }, this));
  1266. },
  1267. onChatRoomMessage: function (message) {
  1268. var $message = $(message),
  1269. body = $message.children('body').text(),
  1270. jid = $message.attr('from'),
  1271. $chat_content = this.$el.find('.chat-content'),
  1272. resource = Strophe.getResourceFromJid(jid),
  1273. sender = resource && Strophe.unescapeNode(resource) || '',
  1274. delayed = $message.find('delay').length > 0,
  1275. subject = $message.children('subject').text(),
  1276. match, template, message_datetime, message_date, dates, isodate, stamp;
  1277. if (delayed) {
  1278. stamp = $message.find('delay').attr('stamp');
  1279. message_datetime = converse.parseISO8601(stamp);
  1280. } else {
  1281. message_datetime = new Date();
  1282. }
  1283. // If this message is on a different day than the one received
  1284. // prior, then indicate it on the chatbox.
  1285. dates = $chat_content.find("time").map(function(){return $(this).attr("datetime");}).get();
  1286. message_date = new Date(message_datetime.getTime());
  1287. message_date.setUTCHours(0,0,0,0);
  1288. isodate = converse.toISOString(message_date);
  1289. if (_.indexOf(dates, isodate) == -1) {
  1290. $chat_content.append(this.new_day_template({
  1291. isodate: isodate,
  1292. datestring: message_date.toString().substring(0,15)
  1293. }));
  1294. }
  1295. this.communicateStatusCodes($message, $chat_content);
  1296. if (subject) {
  1297. this.$el.find('.chatroom-topic').text(subject).attr('title', subject);
  1298. $chat_content.append(this.info_template({'message': 'Topic set by '+sender+' to: '+subject }));
  1299. }
  1300. if (!body) { return true; }
  1301. match = body.match(/^\/(.*?)(?: (.*))?$/);
  1302. if ((match) && (match[1] === 'me')) {
  1303. body = body.replace(/^\/me/, '*'+sender);
  1304. template = this.action_template;
  1305. } else {
  1306. template = this.message_template;
  1307. }
  1308. if (sender === this.model.get('nick')) {
  1309. sender = 'me';
  1310. }
  1311. $chat_content.append(
  1312. template({
  1313. 'sender': sender == 'me' && sender || 'room',
  1314. 'time': message_datetime.toLocaleTimeString().substring(0,5),
  1315. 'message': body,
  1316. 'username': sender,
  1317. 'extra_classes': delayed && 'delayed' || ''
  1318. })
  1319. );
  1320. this.scrollDown();
  1321. return true;
  1322. },
  1323. occupant_template: _.template(
  1324. '<li class="{{role}}" '+
  1325. '{[ if (role === "moderator") { ]}' +
  1326. 'title="This user is a moderator"' +
  1327. '{[ } ]}'+
  1328. '{[ if (role === "participant") { ]}' +
  1329. 'title="This user can send messages in this room"' +
  1330. '{[ } ]}'+
  1331. '{[ if (role === "visitor") { ]}' +
  1332. 'title="This user can NOT send messages in this room"' +
  1333. '{[ } ]}'+
  1334. '>{{nick}}</li>'
  1335. ),
  1336. onChatRoomRoster: function (roster, room) {
  1337. if (!this.$el.find('.chat-area').length) { this.renderChatArea(); }
  1338. var controlboxview = converse.chatboxesview.views.controlbox,
  1339. roster_size = _.size(roster),
  1340. $participant_list = this.$el.find('.participant-list'),
  1341. participants = [], keys = _.keys(roster), i;
  1342. this.$el.find('.participant-list').empty();
  1343. for (i=0; i<roster_size; i++) {
  1344. participants.push(
  1345. this.occupant_template({
  1346. role: roster[keys[i]].role,
  1347. nick: Strophe.unescapeNode(keys[i])
  1348. }));
  1349. }
  1350. $participant_list.append(participants.join(""));
  1351. return true;
  1352. }
  1353. });
  1354. converse.ChatBoxes = Backbone.Collection.extend({
  1355. model: converse.ChatBox,
  1356. onConnected: function () {
  1357. this.localStorage = new Backbone.LocalStorage(
  1358. hex_sha1('converse.chatboxes-'+converse.bare_jid));
  1359. if (!this.get('controlbox')) {
  1360. this.add({
  1361. id: 'controlbox',
  1362. box_id: 'controlbox'
  1363. });
  1364. } else {
  1365. this.get('controlbox').save();
  1366. }
  1367. // This will make sure the Roster is set up
  1368. this.get('controlbox').set({connected:true});
  1369. // Get cached chatboxes from localstorage
  1370. this.fetch({
  1371. add: true,
  1372. success: $.proxy(function (collection, resp) {
  1373. if (_.include(_.pluck(resp, 'id'), 'controlbox')) {
  1374. // If the controlbox was saved in localstorage, it must be visible
  1375. this.get('controlbox').set({visible:true}).save();
  1376. }
  1377. }, this)
  1378. });
  1379. },
  1380. messageReceived: function (message) {
  1381. var partner_jid, $message = $(message),
  1382. message_from = $message.attr('from');
  1383. if (message_from == converse.connection.jid) {
  1384. // FIXME: Forwarded messages should be sent to specific resources,
  1385. // not broadcasted
  1386. return true;
  1387. }
  1388. var $forwarded = $message.children('forwarded');
  1389. if ($forwarded.length) {
  1390. $message = $forwarded.children('message');
  1391. }
  1392. var from = Strophe.getBareJidFromJid(message_from),
  1393. to = Strophe.getBareJidFromJid($message.attr('to')),
  1394. resource, chatbox;
  1395. if (from == converse.bare_jid) {
  1396. // I am the sender, so this must be a forwarded message...
  1397. partner_jid = to;
  1398. resource = Strophe.getResourceFromJid($message.attr('to'));
  1399. } else {
  1400. partner_jid = from;
  1401. resource = Strophe.getResourceFromJid(message_from);
  1402. }
  1403. chatbox = this.get(partner_jid);
  1404. if (!chatbox) {
  1405. converse.getVCard(
  1406. partner_jid,
  1407. $.proxy(function (jid, fullname, image, image_type, url) {
  1408. var chatbox = this.create({
  1409. 'id': jid,
  1410. 'jid': jid,
  1411. 'fullname': fullname,
  1412. 'image_type': image_type,
  1413. 'image': image,
  1414. 'url': url
  1415. });
  1416. chatbox.messageReceived(message);
  1417. converse.roster.addResource(partner_jid, resource);
  1418. }, this),
  1419. $.proxy(function () {
  1420. // # FIXME
  1421. console.log("An error occured while fetching vcard");
  1422. }, this));
  1423. return true;
  1424. }
  1425. chatbox.messageReceived(message);
  1426. converse.roster.addResource(partner_jid, resource);
  1427. return true;
  1428. }
  1429. });
  1430. converse.ChatBoxesView = Backbone.View.extend({
  1431. el: '#collective-xmpp-chat-data',
  1432. initialize: function () {
  1433. // boxesviewinit
  1434. this.views = {};
  1435. this.model.on("add", function (item) {
  1436. var view = this.views[item.get('id')];
  1437. if (!view) {
  1438. if (item.get('chatroom')) {
  1439. view = new converse.ChatRoomView({'model': item});
  1440. } else if (item.get('box_id') === 'controlbox') {
  1441. view = new converse.ControlBoxView({model: item});
  1442. view.render();
  1443. } else {
  1444. view = new converse.ChatBoxView({model: item});
  1445. }
  1446. this.views[item.get('id')] = view;
  1447. } else {
  1448. view.model = item;
  1449. view.initialize();
  1450. if (item.get('id') !== 'controlbox') {
  1451. // FIXME: Why is it necessary to again append chatboxes?
  1452. view.$el.appendTo(this.$el);
  1453. }
  1454. }
  1455. }, this);
  1456. }
  1457. });
  1458. converse.RosterItem = Backbone.Model.extend({
  1459. initialize: function (attributes, options) {
  1460. var jid = attributes.jid;
  1461. if (!attributes.fullname) {
  1462. attributes.fullname = jid;
  1463. }
  1464. _.extend(attributes, {
  1465. 'id': jid,
  1466. 'user_id': Strophe.getNodeFromJid(jid),
  1467. 'resources': [],
  1468. 'chat_status': 'offline',
  1469. 'status': 'offline',
  1470. 'sorted': false
  1471. });
  1472. this.set(attributes);
  1473. }
  1474. });
  1475. converse.RosterItemView = Backbone.View.extend({
  1476. tagName: 'dd',
  1477. events: {
  1478. "click .accept-xmpp-request": "acceptRequest",
  1479. "click .decline-xmpp-request": "declineRequest",
  1480. "click .open-chat": "openChat",
  1481. "click .remove-xmpp-contact": "removeContact"
  1482. },
  1483. openChat: function (ev) {
  1484. ev.preventDefault();
  1485. var jid = Strophe.getBareJidFromJid(this.model.get('jid')),
  1486. chatbox = converse.chatboxes.get(jid);
  1487. if (chatbox) {
  1488. chatbox.trigger('show');
  1489. } else {
  1490. converse.chatboxes.create({
  1491. 'id': this.model.get('jid'),
  1492. 'jid': this.model.get('jid'),
  1493. 'fullname': this.model.get('fullname'),
  1494. 'image_type': this.model.get('image_type'),
  1495. 'image': this.model.get('image'),
  1496. 'url': this.model.get('url')
  1497. });
  1498. }
  1499. },
  1500. removeContact: function (ev) {
  1501. ev.preventDefault();
  1502. var result = confirm("Are you sure you want to remove this contact?");
  1503. if (result === true) {
  1504. var bare_jid = this.model.get('jid');
  1505. converse.connection.roster.remove(bare_jid, function (iq) {
  1506. converse.connection.roster.unauthorize(bare_jid);
  1507. converse.rosterview.model.remove(bare_jid);
  1508. });
  1509. }
  1510. },
  1511. acceptRequest: function (ev) {
  1512. var jid = this.model.get('jid');
  1513. converse.connection.roster.authorize(jid);
  1514. converse.connection.roster.add(jid, this.model.get('fullname'), [], function (iq) {
  1515. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  1516. });
  1517. ev.preventDefault();
  1518. },
  1519. declineRequest: function (ev) {
  1520. ev.preventDefault();
  1521. converse.connection.roster.unauthorize(this.model.get('jid'));
  1522. this.model.destroy();
  1523. },
  1524. template: _.template(
  1525. '<a class="open-chat" title="Click to chat with this contact" href="#">{{ fullname }}</a>' +
  1526. '<a class="remove-xmpp-contact" title="Click to remove this contact" href="#"></a>'),
  1527. pending_template: _.template(
  1528. '<span>{{ fullname }}</span>' +
  1529. '<a class="remove-xmpp-contact" title="Click to remove this contact" href="#"></a>'),
  1530. request_template: _.template('<div>{{ fullname }}</div>' +
  1531. '<button type="button" class="accept-xmpp-request">' +
  1532. 'Accept</button>' +
  1533. '<button type="button" class="decline-xmpp-request">' +
  1534. 'Decline</button>' +
  1535. ''),
  1536. render: function () {
  1537. var item = this.model,
  1538. ask = item.get('ask'),
  1539. subscription = item.get('subscription');
  1540. this.$el.addClass(item.get('chat_status'));
  1541. if (ask === 'subscribe') {
  1542. this.$el.addClass('pending-xmpp-contact');
  1543. this.$el.html(this.pending_template(item.toJSON()));
  1544. } else if (ask === 'request') {
  1545. this.$el.addClass('requesting-xmpp-contact');
  1546. this.$el.html(this.request_template(item.toJSON()));
  1547. converse.showControlBox();
  1548. } else if (subscription === 'both' || subscription === 'to') {
  1549. this.$el.addClass('current-xmpp-contact');
  1550. this.$el.html(this.template(item.toJSON()));
  1551. }
  1552. return this;
  1553. },
  1554. initialize: function () {
  1555. this.options.model.on('change', function (item, changed) {
  1556. if (_.has(item.changed, 'chat_status')) {
  1557. this.$el.attr('class', item.changed.chat_status);
  1558. }
  1559. }, this);
  1560. }
  1561. });
  1562. converse.getVCard = function (jid, callback, errback) {
  1563. converse.connection.vcard.get($.proxy(function (iq) {
  1564. $vcard = $(iq).find('vCard');
  1565. var fullname = $vcard.find('FN').text(),
  1566. img = $vcard.find('BINVAL').text(),
  1567. img_type = $vcard.find('TYPE').text(),
  1568. url = $vcard.find('URL').text();
  1569. callback(jid, fullname, img, img_type, url);
  1570. }, this), jid, errback);
  1571. }
  1572. converse.RosterItems = Backbone.Collection.extend({
  1573. model: converse.RosterItem,
  1574. comparator : function (rosteritem) {
  1575. var chat_status = rosteritem.get('chat_status'),
  1576. rank = 4;
  1577. switch(chat_status) {
  1578. case 'offline':
  1579. rank = 0;
  1580. break;
  1581. case 'unavailable':
  1582. rank = 1;
  1583. break;
  1584. case 'xa':
  1585. rank = 2;
  1586. break;
  1587. case 'away':
  1588. rank = 3;
  1589. break;
  1590. case 'dnd':
  1591. rank = 4;
  1592. break;
  1593. case 'online':
  1594. rank = 5;
  1595. break;
  1596. }
  1597. return rank;
  1598. },
  1599. subscribeToSuggestedItems: function (msg) {
  1600. $(msg).find('item').each(function () {
  1601. var $this = $(this),
  1602. jid = $this.attr('jid'),
  1603. action = $this.attr('action'),
  1604. fullname = $this.attr('name');
  1605. if (action === 'add') {
  1606. converse.connection.roster.add(jid, fullname, [], function (iq) {
  1607. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  1608. });
  1609. }
  1610. });
  1611. return true;
  1612. },
  1613. isSelf: function (jid) {
  1614. return (Strophe.getBareJidFromJid(jid) === Strophe.getBareJidFromJid(converse.connection.jid));
  1615. },
  1616. getItem: function (id) {
  1617. return Backbone.Collection.prototype.get.call(this, id);
  1618. },
  1619. addResource: function (bare_jid, resource) {
  1620. var item = this.getItem(bare_jid),
  1621. resources;
  1622. if (item) {
  1623. resources = item.get('resources');
  1624. if (resources) {
  1625. if (_.indexOf(resources, resource) == -1) {
  1626. resources.push(resource);
  1627. item.set({'resources': resources});
  1628. }
  1629. } else {
  1630. item.set({'resources': [resource]});
  1631. }
  1632. }
  1633. },
  1634. removeResource: function (bare_jid, resource) {
  1635. var item = this.getItem(bare_jid),
  1636. resources,
  1637. idx;
  1638. if (item) {
  1639. resources = item.get('resources');
  1640. idx = _.indexOf(resources, resource);
  1641. if (idx !== -1) {
  1642. resources.splice(idx, 1);
  1643. item.set({'resources': resources});
  1644. return resources.length;
  1645. }
  1646. }
  1647. return 0;
  1648. },
  1649. subscribeBack: function (jid) {
  1650. var bare_jid = Strophe.getBareJidFromJid(jid);
  1651. if (converse.connection.roster.findItem(bare_jid)) {
  1652. converse.connection.roster.authorize(bare_jid);
  1653. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  1654. } else {
  1655. converse.connection.roster.add(jid, '', [], function (iq) {
  1656. converse.connection.roster.authorize(bare_jid);
  1657. converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
  1658. });
  1659. }
  1660. },
  1661. unsubscribe: function (jid) {
  1662. /* Upon receiving the presence stanza of type "unsubscribed",
  1663. * the user SHOULD acknowledge receipt of that subscription state
  1664. * notification by sending a presence stanza of type "unsubscribe"
  1665. * this step lets the user's server know that it MUST no longer
  1666. * send notification of the subscription state change to the user.
  1667. */
  1668. converse.xmppstatus.sendPresence('unsubscribe');
  1669. if (converse.connection.roster.findItem(jid)) {
  1670. converse.connection.roster.remove(jid, function (iq) {
  1671. converse.rosterview.model.remove(jid);
  1672. });
  1673. }
  1674. },
  1675. getNumOnlineContacts: function () {
  1676. var count = 0,
  1677. models = this.models,
  1678. models_length = models.length,
  1679. i;
  1680. for (i=0; i<models_length; i++) {
  1681. if (_.indexOf(['offline', 'unavailable'], models[i].get('chat_status')) === -1) {
  1682. count++;
  1683. }
  1684. }
  1685. return count;
  1686. },
  1687. cleanCache: function (items) {
  1688. /* The localstorage cache containing roster contacts might contain
  1689. * some contacts that aren't actually in our roster anymore. We
  1690. * therefore need to remove them now.
  1691. */
  1692. var id, i,
  1693. roster_ids = [];
  1694. for (i=0; i < items.length; ++i) {
  1695. roster_ids.push(items[i].jid);
  1696. }
  1697. for (i=0; i < this.models.length; ++i) {
  1698. id = this.models[i].get('id');
  1699. if (_.indexOf(roster_ids, id) === -1) {
  1700. this.getItem(id).destroy();
  1701. }
  1702. }
  1703. },
  1704. rosterHandler: function (items) {
  1705. this.cleanCache(items);
  1706. _.each(items, function (item, index, items) {
  1707. if (this.isSelf(item.jid)) { return; }
  1708. var model = this.getItem(item.jid);
  1709. if (!model) {
  1710. is_last = false;
  1711. if (index === (items.length-1)) { is_last = true; }
  1712. this.create({
  1713. jid: item.jid,
  1714. subscription: item.subscription,
  1715. ask: item.ask,
  1716. fullname: item.name || item.jid,
  1717. is_last: is_last
  1718. });
  1719. } else {
  1720. if ((item.subscription === 'none') && (item.ask === null)) {
  1721. // This user is no longer in our roster
  1722. model.destroy();
  1723. } else if (model.get('subscription') !== item.subscription || model.get('ask') !== item.ask) {
  1724. // only modify model attributes if they are different from the
  1725. // ones that were already set when the rosterItem was added
  1726. model.set({'subscription': item.subscription, 'ask': item.ask});
  1727. model.save();
  1728. }
  1729. }
  1730. }, this);
  1731. },
  1732. presenceHandler: function (presence) {
  1733. var $presence = $(presence),
  1734. jid = $presence.attr('from'),
  1735. bare_jid = Strophe.getBareJidFromJid(jid),
  1736. resource = Strophe.getResourceFromJid(jid),
  1737. presence_type = $presence.attr('type'),
  1738. $show = $presence.find('show'),
  1739. chat_status = $show.text() || 'online',
  1740. status_message = $presence.find('status'),
  1741. item;
  1742. if (this.isSelf(bare_jid)) {
  1743. if ((converse.connection.jid !== jid)&&(presence_type !== 'unavailabe')) {
  1744. // Another resource has changed it's status, we'll update ours as well.
  1745. // FIXME: We should ideally differentiate between converse.js using
  1746. // resources and other resources (i.e Pidgin etc.)
  1747. converse.xmppstatus.set({'status': chat_status});
  1748. }
  1749. return true;
  1750. } else if (($presence.find('x').attr('xmlns') || '').indexOf(Strophe.NS.MUC) === 0) {
  1751. return true; // Ignore MUC
  1752. }
  1753. item = this.getItem(bare_jid);
  1754. if (item && (status_message.text() != item.get('status'))) {
  1755. item.set({'status': status_message.text()});
  1756. }
  1757. if ((presence_type === 'error') || (presence_type === 'subscribed') || (presence_type === 'unsubscribe')) {
  1758. return true;
  1759. } else if (presence_type === 'subscribe') {
  1760. if (converse.auto_subscribe) {
  1761. if ((!item) || (item.get('subscription') != 'to')) {
  1762. this.subscribeBack(jid);
  1763. } else {
  1764. converse.connection.roster.authorize(bare_jid);
  1765. }
  1766. } else {
  1767. if ((item) && (item.get('subscription') != 'none')) {
  1768. converse.connection.roster.authorize(bare_jid);
  1769. } else {
  1770. converse.getVCard(
  1771. bare_jid,
  1772. $.proxy(function (jid, fullname, img, img_type, url) {
  1773. this.add({
  1774. jid: bare_jid,
  1775. subscription: 'none',
  1776. ask: 'request',
  1777. fullname: fullname,
  1778. image: img,
  1779. image_type: img_type,
  1780. url: url,
  1781. is_last: true
  1782. });
  1783. }, this),
  1784. $.proxy(function (jid, fullname, img, img_type, url) {
  1785. console.log("Error while retrieving vcard");
  1786. this.add({jid: bare_jid, subscription: 'none', ask: 'request', fullname: jid, is_last: true});
  1787. }, this)
  1788. );
  1789. }
  1790. }
  1791. } else if (presence_type === 'unsubscribed') {
  1792. this.unsubscribe(bare_jid);
  1793. } else if (presence_type === 'unavailable') {
  1794. if (this.removeResource(bare_jid, resource) === 0) {
  1795. if (item) {
  1796. item.set({'chat_status': 'offline'});
  1797. }
  1798. }
  1799. } else if (item) {
  1800. // presence_type is undefined
  1801. this.addResource(bare_jid, resource);
  1802. item.set({'chat_status': chat_status});
  1803. }
  1804. return true;
  1805. }
  1806. });
  1807. converse.RosterView = Backbone.View.extend({
  1808. tagName: 'dl',
  1809. id: 'converse-roster',
  1810. rosteritemviews: {},
  1811. removeRosterItem: function (item) {
  1812. var view = this.rosteritemviews[item.id];
  1813. if (view) {
  1814. view.$el.remove();
  1815. delete this.rosteritemviews[item.id];
  1816. this.render();
  1817. }
  1818. },
  1819. initialize: function () {
  1820. this.model.on("add", function (item) {
  1821. var view = new converse.RosterItemView({model: item});
  1822. this.rosteritemviews[item.id] = view;
  1823. this.render(item);
  1824. }, this);
  1825. this.model.on('change', function (item, changed) {
  1826. if ((_.size(item.changed) === 1) && _.contains(_.keys(item.changed), 'sorted')) {
  1827. return;
  1828. }
  1829. this.updateChatBox(item, changed);
  1830. this.render(item);
  1831. }, this);
  1832. this.model.on("remove", function (item) { this.removeRosterItem(item); }, this);
  1833. this.model.on("destroy", function (item) { this.removeRosterItem(item); }, this);
  1834. this.$el.hide().html(this.template());
  1835. this.model.fetch({add: true}); // Get the cached roster items from localstorage
  1836. this.initialSort();
  1837. this.$el.appendTo(converse.chatboxesview.views.controlbox.contactspanel.$el);
  1838. },
  1839. updateChatBox: function (item, changed) {
  1840. var chatbox = converse.chatboxes.get(item.get('jid')),
  1841. changes = {};
  1842. if (!chatbox) { return; }
  1843. if (_.has(item.changed, 'chat_status')) {
  1844. changes.chat_status = item.get('chat_status');
  1845. }
  1846. if (_.has(item.changed, 'status')) {
  1847. changes.status = item.get('status');
  1848. }
  1849. chatbox.save(changes);
  1850. },
  1851. template: _.template('<dt id="xmpp-contact-requests">Contact requests</dt>' +
  1852. '<dt id="xmpp-contacts">My contacts</dt>' +
  1853. '<dt id="pending-xmpp-contacts">Pending contacts</dt>'),
  1854. render: function (item) {
  1855. var $my_contacts = this.$el.find('#xmpp-contacts'),
  1856. $contact_requests = this.$el.find('#xmpp-contact-requests'),
  1857. $pending_contacts = this.$el.find('#pending-xmpp-contacts'),
  1858. $count, presence_change;
  1859. if (item) {
  1860. var jid = item.id,
  1861. view = this.rosteritemviews[item.id],
  1862. ask = item.get('ask'),
  1863. subscription = item.get('subscription'),
  1864. crit = {order:'asc'};
  1865. if (ask === 'subscribe') {
  1866. $pending_contacts.after(view.render().el);
  1867. $pending_contacts.after($pending_contacts.siblings('dd.pending-xmpp-contact').tsort(crit));
  1868. } else if (ask === 'request') {
  1869. $contact_requests.after(view.render().el);
  1870. $contact_requests.after($contact_requests.siblings('dd.requesting-xmpp-contact').tsort(crit));
  1871. } else if (subscription === 'both' || subscription === 'to') {
  1872. if (!item.get('sorted')) {
  1873. // this attribute will be true only after all of the elements have been added on the page
  1874. // at this point all offline
  1875. $my_contacts.after(view.render().el);
  1876. }
  1877. else {
  1878. // just by calling render will be enough to change the icon of the existing item without
  1879. // having to reinsert it and the sort will come from the presence change
  1880. view.render();
  1881. }
  1882. }
  1883. presence_change = view.model.changed.chat_status;
  1884. if (presence_change) {
  1885. // resort all items only if the model has changed it's chat_status as this render
  1886. // is also triggered when the resource is changed which always comes before the presence change
  1887. // therefore we avoid resorting when the change doesn't affect the position of the item
  1888. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.offline').tsort('a', crit));
  1889. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.unavailable').tsort('a', crit));
  1890. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.away').tsort('a', crit));
  1891. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.dnd').tsort('a', crit));
  1892. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.online').tsort('a', crit));
  1893. }
  1894. if (item.get('is_last') && !item.get('sorted')) {
  1895. // this will be true after all of the roster items have been added with the default
  1896. // options where all of the items are offline and now we can show the rosterView
  1897. item.set('sorted', true);
  1898. this.initialSort();
  1899. this.$el.show();
  1900. }
  1901. }
  1902. // Hide the headings if there are no contacts under them
  1903. _.each([$my_contacts, $contact_requests, $pending_contacts], function (h) {
  1904. if (h.nextUntil('dt').length) {
  1905. h.show();
  1906. }
  1907. else {
  1908. h.hide();
  1909. }
  1910. });
  1911. $count = $('#online-count');
  1912. $count.text('('+this.model.getNumOnlineContacts()+')');
  1913. if (!$count.is(':visible')) {
  1914. $count.show();
  1915. }
  1916. return this;
  1917. },
  1918. initialSort: function () {
  1919. var $my_contacts = this.$el.find('#xmpp-contacts'),
  1920. crit = {order:'asc'};
  1921. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.offline').tsort('a', crit));
  1922. $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.unavailable').tsort('a', crit));
  1923. }
  1924. });
  1925. converse.XMPPStatus = Backbone.Model.extend({
  1926. initialize: function () {
  1927. this.set({
  1928. 'status' : this.get('status'),
  1929. 'status_message' : this.get('status_message'),
  1930. 'fullname' : this.get('fullname')
  1931. });
  1932. },
  1933. initStatus: function () {
  1934. var stat = this.get('status');
  1935. if (stat === undefined) {
  1936. this.save({status: 'online'});
  1937. } else {
  1938. this.sendPresence(stat);
  1939. }
  1940. },
  1941. sendPresence: function (type) {
  1942. var status_message = this.get('status_message'),
  1943. presence;
  1944. // Most of these presence types are actually not explicitly sent,
  1945. // but I add all of them here fore reference and future proofing.
  1946. if ((type === 'unavailable') ||
  1947. (type === 'probe') ||
  1948. (type === 'error') ||
  1949. (type === 'unsubscribe') ||
  1950. (type === 'unsubscribed') ||
  1951. (type === 'subscribe') ||
  1952. (type === 'subscribed')) {
  1953. presence = $pres({'type':type});
  1954. } else {
  1955. if (type === 'online') {
  1956. presence = $pres();
  1957. } else {
  1958. presence = $pres().c('show').t(type).up();
  1959. }
  1960. if (status_message) {
  1961. presence.c('status').t(status_message);
  1962. }
  1963. }
  1964. converse.connection.send(presence);
  1965. },
  1966. setStatus: function (value) {
  1967. this.sendPresence(value);
  1968. this.save({'status': value});
  1969. },
  1970. setStatusMessage: function (status_message) {
  1971. converse.connection.send($pres().c('show').t(this.get('status')).up().c('status').t(status_message));
  1972. this.save({'status_message': status_message});
  1973. }
  1974. });
  1975. converse.XMPPStatusView = Backbone.View.extend({
  1976. el: "span#xmpp-status-holder",
  1977. events: {
  1978. "click a.choose-xmpp-status": "toggleOptions",
  1979. "click #fancy-xmpp-status-select a.change-xmpp-status-message": "renderStatusChangeForm",
  1980. "submit #set-custom-xmpp-status": "setStatusMessage",
  1981. "click .dropdown dd ul li a": "setStatus"
  1982. },
  1983. toggleOptions: function (ev) {
  1984. ev.preventDefault();
  1985. $(ev.target).parent().parent().siblings('dd').find('ul').toggle('fast');
  1986. },
  1987. change_status_message_template: _.template(
  1988. '<form id="set-custom-xmpp-status">' +
  1989. '<input type="text" class="custom-xmpp-status" {{ status_message }}" placeholder="Custom status"/>' +
  1990. '<button type="submit">Save</button>' +
  1991. '</form>'),
  1992. status_template: _.template(
  1993. '<div class="xmpp-status">' +
  1994. '<a class="choose-xmpp-status {{ chat_status }}" data-value="{{status_message}}" href="#" title="Click to change your chat status">' +
  1995. '{{ status_message }}' +
  1996. '</a>' +
  1997. '<a class="change-xmpp-status-message" href="#" Title="Click here to write a custom status message"></a>' +
  1998. '</div>'),
  1999. renderStatusChangeForm: function (ev) {
  2000. ev.preventDefault();
  2001. var status_message = this.model.get('status') || 'offline';
  2002. var input = this.change_status_message_template({'status_message': status_message});
  2003. this.$el.find('.xmpp-status').replaceWith(input);
  2004. this.$el.find('.custom-xmpp-status').focus().focus();
  2005. },
  2006. setStatusMessage: function (ev) {
  2007. ev.preventDefault();
  2008. var status_message = $(ev.target).find('input').attr('value');
  2009. if (status_message === "") {
  2010. }
  2011. this.model.setStatusMessage(status_message);
  2012. },
  2013. setStatus: function (ev) {
  2014. ev.preventDefault();
  2015. var $el = $(ev.target),
  2016. value = $el.attr('data-value');
  2017. this.model.setStatus(value);
  2018. this.$el.find(".dropdown dd ul").hide();
  2019. },
  2020. getPrettyStatus: function (stat) {
  2021. if (stat === 'chat') {
  2022. pretty_status = 'online';
  2023. } else if (stat === 'dnd') {
  2024. pretty_status = 'busy';
  2025. } else if (stat === 'xa') {
  2026. pretty_status = 'away for long';
  2027. } else {
  2028. pretty_status = stat || 'online';
  2029. }
  2030. return pretty_status;
  2031. },
  2032. updateStatusUI: function (model) {
  2033. if (!(_.has(model.changed, 'status')) && !(_.has(model.changed, 'status_message'))) {
  2034. return;
  2035. }
  2036. var stat = model.get('status'),
  2037. status_message = model.get('status_message') || "I am " + this.getPrettyStatus(stat);
  2038. this.$el.find('#fancy-xmpp-status-select').html(
  2039. this.status_template({
  2040. 'chat_status': stat,
  2041. 'status_message': status_message
  2042. }));
  2043. },
  2044. choose_template: _.template(
  2045. '<dl id="target" class="dropdown">' +
  2046. '<dt id="fancy-xmpp-status-select" class="fancy-dropdown"></dt>' +
  2047. '<dd><ul></ul></dd>' +
  2048. '</dl>'),
  2049. option_template: _.template(
  2050. '<li>' +
  2051. '<a href="#" class="{{ value }}" data-value="{{ value }}">{{ text }}</a>' +
  2052. '</li>'),
  2053. initialize: function () {
  2054. this.model.on("change", this.updateStatusUI, this);
  2055. },
  2056. render: function () {
  2057. // Replace the default dropdown with something nicer
  2058. var $select = this.$el.find('select#select-xmpp-status'),
  2059. chat_status = this.model.get('status') || 'offline',
  2060. options = $('option', $select),
  2061. $options_target,
  2062. options_list = [],
  2063. that = this;
  2064. this.$el.html(this.choose_template());
  2065. this.$el.find('#fancy-xmpp-status-select')
  2066. .html(this.status_template({
  2067. 'status_message': "I am " + this.getPrettyStatus(chat_status),
  2068. 'chat_status': chat_status
  2069. }));
  2070. // iterate through all the <option> elements and add option values
  2071. options.each(function(){
  2072. options_list.push(that.option_template({'value': $(this).val(),
  2073. 'text': this.text
  2074. }));
  2075. });
  2076. $options_target = this.$el.find("#target dd ul").hide();
  2077. $options_target.append(options_list.join(''));
  2078. $select.remove();
  2079. return this;
  2080. }
  2081. });
  2082. converse.Feature = Backbone.Model.extend();
  2083. converse.Features = Backbone.Collection.extend({
  2084. /* Service Discovery
  2085. * -----------------
  2086. * This collection stores Feature Models, representing features
  2087. * provided by available XMPP entities (e.g. servers)
  2088. * See XEP-0030 for more details: http://xmpp.org/extensions/xep-0030.html
  2089. * All features are shown here: http://xmpp.org/registrar/disco-features.html
  2090. */
  2091. model: converse.Feature,
  2092. initialize: function () {
  2093. this.localStorage = new Backbone.LocalStorage(
  2094. hex_sha1('converse.features'+converse.bare_jid));
  2095. if (this.localStorage.records.length === 0) {
  2096. // localStorage is empty, so we've likely never queried this
  2097. // domain for features yet
  2098. converse.connection.disco.info(converse.domain, null, $.proxy(this.onInfo, this));
  2099. converse.connection.disco.items(converse.domain, null, $.proxy(this.onItems, this));
  2100. } else {
  2101. this.fetch({add:true});
  2102. }
  2103. },
  2104. onItems: function (stanza) {
  2105. $(stanza).find('query item').each($.proxy(function (idx, item) {
  2106. converse.connection.disco.info(
  2107. $(item).attr('jid'),
  2108. null,
  2109. $.proxy(this.onInfo, this));
  2110. }, this));
  2111. },
  2112. onInfo: function (stanza) {
  2113. var $stanza = $(stanza);
  2114. if (($stanza.find('identity[category=server][type=im]').length === 0) &&
  2115. ($stanza.find('identity[category=conference][type=text]').length === 0)) {
  2116. // This isn't an IM server component
  2117. return;
  2118. }
  2119. $stanza.find('feature').each($.proxy(function (idx, feature) {
  2120. this.create({
  2121. 'var': $(feature).attr('var'),
  2122. 'from': $stanza.attr('from')
  2123. });
  2124. }, this));
  2125. }
  2126. });
  2127. converse.LoginPanel = Backbone.View.extend({
  2128. tagName: 'div',
  2129. id: "login-dialog",
  2130. events: {
  2131. 'submit form#converse-login': 'authenticate'
  2132. },
  2133. tab_template: _.template(
  2134. '<li><a class="current" href="#login">Sign in</a></li>'),
  2135. template: _.template(
  2136. '<form id="converse-login">' +
  2137. '<label>XMPP/Jabber Username:</label>' +
  2138. '<input type="text" id="jid">' +
  2139. '<label>Password:</label>' +
  2140. '<input type="password" id="password">' +
  2141. '<input class="login-submit" type="submit" value="Log In">' +
  2142. '</form">'),
  2143. bosh_url_input: _.template(
  2144. '<label>BOSH Service URL:</label>' +
  2145. '<input type="text" id="bosh_service_url">'),
  2146. connect: function (jid, password) {
  2147. var connection = new Strophe.Connection(converse.bosh_service_url);
  2148. connection.connect(jid, password, $.proxy(function (status, message) {
  2149. if (status === Strophe.Status.CONNECTED) {
  2150. console.log('Connected');
  2151. converse.onConnected(connection);
  2152. } else if (status === Strophe.Status.DISCONNECTED) {
  2153. $button.show().siblings('img').remove();
  2154. converse.giveFeedback('Disconnected', 'error');
  2155. } else if (status === Strophe.Status.Error) {
  2156. $button.show().siblings('img').remove();
  2157. converse.giveFeedback('Error', 'error');
  2158. } else if (status === Strophe.Status.CONNECTING) {
  2159. converse.giveFeedback('Connecting');
  2160. } else if (status === Strophe.Status.CONNFAIL) {
  2161. $button.show().siblings('img').remove();
  2162. converse.giveFeedback('Connection Failed', 'error');
  2163. } else if (status === Strophe.Status.AUTHENTICATING) {
  2164. converse.giveFeedback('Authenticating');
  2165. } else if (status === Strophe.Status.AUTHFAIL) {
  2166. $button.show().siblings('img').remove();
  2167. converse.giveFeedback('Authentication Failed', 'error');
  2168. } else if (status === Strophe.Status.DISCONNECTING) {
  2169. converse.giveFeedback('Disconnecting', 'error');
  2170. } else if (status === Strophe.Status.ATTACHED) {
  2171. console.log('Attached');
  2172. }
  2173. }, this));
  2174. },
  2175. authenticate: function (ev) {
  2176. ev.preventDefault();
  2177. var $form = $(ev.target),
  2178. $jid_input = $form.find('input#jid'),
  2179. jid = $jid_input.val(),
  2180. $pw_input = $form.find('input#password'),
  2181. password = $pw_input.val(),
  2182. $bsu_input = null,
  2183. errors = false;
  2184. if (! converse.bosh_service_url) {
  2185. $bsu_input = $form.find('input#bosh_service_url');
  2186. converse.bosh_service_url = $bsu_input.val();
  2187. if (! converse.bosh_service_url) {
  2188. errors = true;
  2189. $bsu_input.addClass('error');
  2190. }
  2191. }
  2192. if (! jid) {
  2193. errors = true;
  2194. $jid_input.addClass('error');
  2195. }
  2196. if (! password) {
  2197. errors = true;
  2198. $pw_input.addClass('error');
  2199. }
  2200. if (errors) { return; }
  2201. var $button = $form.find('input[type=submit]');
  2202. $button.hide().after('<img class="spinner login-submit" src="images/spinner.gif"/>');
  2203. this.connect(jid, password);
  2204. },
  2205. remove: function () {
  2206. this.$parent.find('#controlbox-tabs').empty();
  2207. this.$parent.find('#controlbox-panes').empty();
  2208. },
  2209. render: function () {
  2210. this.$parent.find('#controlbox-tabs').append(this.tab_template());
  2211. var template = this.template();
  2212. if (! this.bosh_url_input) {
  2213. template.find('form').append(this.bosh_url_input);
  2214. }
  2215. this.$parent.find('#controlbox-panes').append(this.$el.html(template));
  2216. this.$el.find('input#jid').focus();
  2217. return this;
  2218. }
  2219. });
  2220. converse.showControlBox = function () {
  2221. var controlbox = this.chatboxes.get('controlbox');
  2222. if (!controlbox) {
  2223. this.chatboxes.add({
  2224. id: 'controlbox',
  2225. box_id: 'controlbox',
  2226. visible: true
  2227. });
  2228. if (this.connection) {
  2229. this.chatboxes.get('controlbox').save();
  2230. }
  2231. } else {
  2232. controlbox.trigger('show');
  2233. }
  2234. };
  2235. converse.toggleControlBox = function () {
  2236. if ($("div#controlbox").is(':visible')) {
  2237. var controlbox = this.chatboxes.get('controlbox');
  2238. if (this.connection) {
  2239. controlbox.destroy();
  2240. } else {
  2241. controlbox.trigger('hide');
  2242. }
  2243. } else {
  2244. this.showControlBox();
  2245. }
  2246. };
  2247. converse.giveFeedback = function (message, klass) {
  2248. $('.conn-feedback').text(message);
  2249. $('.conn-feedback').attr('class', 'conn-feedback');
  2250. if (klass) {
  2251. $('.conn-feedback').addClass(klass);
  2252. }
  2253. };
  2254. converse.onConnected = function (connection) {
  2255. this.connection = connection;
  2256. this.connection.xmlInput = function (body) { console.log(body); };
  2257. this.connection.xmlOutput = function (body) { console.log(body); };
  2258. this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid);
  2259. this.domain = Strophe.getDomainFromJid(this.connection.jid);
  2260. this.features = new this.Features();
  2261. // Set up the roster
  2262. this.roster = new this.RosterItems();
  2263. this.roster.localStorage = new Backbone.LocalStorage(
  2264. hex_sha1('converse.rosteritems-'+this.bare_jid));
  2265. this.xmppstatus = new this.XMPPStatus({id:1});
  2266. this.xmppstatus.localStorage = new Backbone.LocalStorage(
  2267. hex_sha1('converse.xmppstatus-'+this.bare_jid));
  2268. this.chatboxes.onConnected();
  2269. this.rosterview = new this.RosterView({'model':this.roster});
  2270. this.xmppstatusview = new this.XMPPStatusView({'model': this.xmppstatus}).render();
  2271. this.xmppstatus.fetch({
  2272. success: $.proxy(function (xmppstatus, resp) {
  2273. if (!xmppstatus.get('fullname')) {
  2274. this.getVCard(
  2275. null, // No 'to' attr when getting one's own vCard
  2276. $.proxy(function (jid, fullname, image, image_type, url) {
  2277. this.xmppstatus.save({'fullname': fullname});
  2278. }, this));
  2279. }
  2280. }, this)
  2281. });
  2282. this.connection.addHandler(
  2283. $.proxy(this.roster.subscribeToSuggestedItems, this.roster),
  2284. 'http://jabber.org/protocol/rosterx', 'message', null);
  2285. this.connection.roster.registerCallback(
  2286. $.proxy(this.roster.rosterHandler, this.roster),
  2287. null, 'presence', null);
  2288. this.connection.roster.get($.proxy(function () {
  2289. this.connection.addHandler(
  2290. $.proxy(function (presence) {
  2291. this.presenceHandler(presence);
  2292. return true;
  2293. }, this.roster), null, 'presence', null);
  2294. this.connection.addHandler(
  2295. $.proxy(function (message) {
  2296. this.chatboxes.messageReceived(message);
  2297. return true;
  2298. }, this), null, 'message', 'chat');
  2299. this.xmppstatus.initStatus();
  2300. }, this));
  2301. $(window).on("blur focus", $.proxy(function(e) {
  2302. if ((this.windowState != e.type) && (e.type == 'focus')) {
  2303. converse.clearMsgCounter();
  2304. }
  2305. this.windowState = e.type;
  2306. },this));
  2307. this.giveFeedback('Online Contacts');
  2308. };
  2309. converse.initialize = function (settings) {
  2310. _.extend(this, settings);
  2311. this.chatboxes = new this.ChatBoxes();
  2312. this.chatboxesview = new this.ChatBoxesView({model: this.chatboxes});
  2313. $('a.toggle-online-users').bind(
  2314. 'click',
  2315. $.proxy(function (e) {
  2316. e.preventDefault(); this.toggleControlBox();
  2317. }, this)
  2318. );
  2319. };
  2320. return converse;
  2321. }));