PageRenderTime 100ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/chat/public/app.js

https://gitlab.com/ad-si-2016-2/p3-g3
JavaScript | 586 lines | 571 code | 10 blank | 5 comment | 3 complexity | e9811b402d06f5b3571eabd53b89b9c3 MD5 | raw file
  1. $(function() {
  2. // Objeto global
  3. window.irc = window.irc || {};
  4. // socket.io
  5. var socket = io.connect();
  6. // modelos
  7. var Message = Backbone.Model.extend({
  8. defaults: {
  9. 'type': 'message'
  10. },
  11. initialize: function() {
  12. if (this.get('raw')) {
  13. this.set({text: this.parse( irc.util.escapeHTML(this.get('raw')) )});
  14. }
  15. },
  16. parse: function(text) {
  17. return this._linkify(text);
  18. },
  19. // Definir texto de saída para mensagens de status
  20. setText: function() {
  21. var text = '';
  22. switch (this.get('type')) {
  23. case 'join':
  24. text = this.get('nick') + ' entrou no canal';
  25. break;
  26. case 'part':
  27. text = this.get('nick') + ' saiu do canal';
  28. break;
  29. case 'nick':
  30. text = this.get('oldNick') + ' agora é ' + this.get('newNick');
  31. break;
  32. }
  33. this.set({text: text});
  34. },
  35. // Encontrar e linkar URLs
  36. _linkify: function(text) {
  37. var re = /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/gi;
  38. var parsed = text.replace(re, function(url) {
  39. // transformar em link
  40. var href = url;
  41. if (url.indexOf('http') !== 0) {
  42. href = 'http://' + url;
  43. }
  44. return '<a href="' + href + '" target="_blank">' + url + '</a>';
  45. });
  46. return parsed;
  47. }
  48. });
  49. var Stream = Backbone.Collection.extend({
  50. model: Message
  51. });
  52. var Person = Backbone.Model.extend({
  53. defaults: {
  54. opStatus: ''
  55. }
  56. });
  57. var Participants = Backbone.Collection.extend({
  58. model: Person,
  59. getByNick: function(nick) {
  60. return this.detect(function(person) {
  61. return person.get('nick') == nick;
  62. });
  63. }
  64. });
  65. var Frame = Backbone.Model.extend({
  66. defaults: {
  67. 'type': 'channel',
  68. 'active': true
  69. },
  70. initialize: function() {
  71. this.stream = new Stream;
  72. this.participants = new Participants;
  73. },
  74. part: function() {
  75. console.log('Leaving ' + this.get('name'));
  76. this.destroy();
  77. }
  78. });
  79. var FrameList = Backbone.Collection.extend({
  80. model: Frame,
  81. getByName: function(name) {
  82. return this.detect(function(frame) {
  83. return frame.get('name') == name;
  84. });
  85. },
  86. getActive: function() {
  87. return this.detect(function(frame) {
  88. return frame.get('active') == true;
  89. });
  90. },
  91. setActive: function(frame) {
  92. this.each(function(frm) {
  93. frm.set({active: false});
  94. });
  95. frame.set({active: true});
  96. },
  97. getChannels: function() {
  98. return this.filter(function(frame) {
  99. return frame.get('type') == 'channel';
  100. });
  101. }
  102. });
  103. window.frames = new FrameList;
  104. // VIEWS
  105. // =====
  106. var MessageView = Backbone.View.extend({
  107. tmpl: $('#message-tmpl').html(),
  108. initialize: function() {
  109. this.render();
  110. },
  111. render: function() {
  112. var context = {
  113. sender: this.model.get('sender'),
  114. text: this.model.get('text')
  115. };
  116. var html = Mustache.to_html(this.tmpl, context);
  117. $(this.el).addClass(this.model.get('type'))
  118. .html(html);
  119. return this;
  120. }
  121. });
  122. // Nick na sidebar
  123. var NickListView = Backbone.View.extend({
  124. el: $('.nicks'),
  125. initialize: function() {
  126. _.bindAll(this);
  127. },
  128. tmpl: function(opStatus, nick) {
  129. return '<div>' + opStatus + ' ' + nick + '</div>'
  130. },
  131. switchChannel: function(ch) {
  132. ch.participants.bind('add', this.addOne, this);
  133. ch.participants.bind('change', this.changeNick, this);
  134. },
  135. addOne: function(p) {
  136. var text = this.tmpl(p.get('opStatus'), p.get('nick'));
  137. $(this.el).append(text);
  138. },
  139. addAll: function(participants) {
  140. var nicks = [];
  141. participants.each(function(p) {
  142. var text = this.tmpl(p.get('opStatus'), p.get('nick'));
  143. nicks.push(text);
  144. }, this);
  145. $(this.el).html(nicks.join('\n'));
  146. },
  147. changeNick: function() {
  148. console.log('Change of nick seen');
  149. console.log(arguments);
  150. }
  151. });
  152. var nickList = new NickListView;
  153. var FrameView = Backbone.View.extend({
  154. el: $('#frame'),
  155. // Para controlar a posição de rolagem
  156. position: {},
  157. initialize: function() {
  158. _.bindAll(this);
  159. },
  160. addMessage: function(message, single) {
  161. // Faça isso apenas em adições de mensagem única
  162. if (single) {
  163. var position = $('#messages').scrollTop();
  164. var atBottom = $('#messages')[0].scrollHeight - position
  165. == $('#messages').innerHeight();
  166. }
  167. var view = new MessageView({model: message});
  168. $('#messages').append(view.el);
  169. // Desloque-se para baixo na nova mensagem se já estiver no fundo
  170. if (atBottom) {
  171. $('#messages').scrollTop(position + 100);
  172. }
  173. },
  174. updateTopic: function(channel) {
  175. this.$('#topic').text(channel.get('topic')).show();
  176. $('#messages').css('top', $('#topic').outerHeight(true));
  177. },
  178. // Mudar o foco para um quadro diferente
  179. focus: function(frame) {
  180. // Salvar a posição de rolagem para o frame
  181. if (this.focused) {
  182. this.position[this.focused.get('name')] = this.$('#output').scrollTop();
  183. }
  184. this.focused = frame;
  185. frames.setActive(this.focused);
  186. $('#messages').empty();
  187. frame.stream.each(function(message) {
  188. this.addMessage(message, false);
  189. }, this);
  190. nickList.addAll(frame.participants);
  191. if (frame.get('type') == 'channel') {
  192. this.$('#sidebar').show();
  193. frame.get('topic') && this.updateTopic(frame);
  194. $('.wrapper').css('margin-right', 205);
  195. $('#messages').css('top', $('#topic').outerHeight(true));
  196. } else {
  197. this.$('#sidebar').hide();
  198. this.$('#topic').hide();
  199. $('.wrapper').css('margin-right', 0);
  200. $('#messages').css('top', 0);
  201. }
  202. $(this.el).removeClass().addClass(frame.get('type'));
  203. this.$('#output #messsages').scrollTop(this.position[frame.get('name')] || 0);
  204. // Somente o frame selecionado deve enviar mensagens
  205. frames.each(function(frm) {
  206. frm.stream.unbind('add');
  207. frm.participants.unbind();
  208. frm.unbind();
  209. });
  210. frame.bind('change:topic', this.updateTopic, this);
  211. frame.stream.bind('add', this.addMessage, this);
  212. nickList.switchChannel(frame);
  213. },
  214. updateNicks: function(model, nicks) {
  215. console.log('Nicks rendered');
  216. }
  217. });
  218. var FrameTabView = Backbone.View.extend({
  219. tagName: 'li',
  220. tmpl: $('#tab-tmpl').html(),
  221. initialize: function() {
  222. this.model.bind('destroy', this.close, this);
  223. this.render();
  224. },
  225. events: {
  226. 'click': 'setActive',
  227. 'click .close-frame': 'close'
  228. },
  229. // Comando PART
  230. part: function() {
  231. if (this.model.get('type') === 'channel') {
  232. socket.emit('part', this.model.get('name'));
  233. } else {
  234. this.model.destroy();
  235. }
  236. },
  237. // Fechar o frame
  238. close: function() {
  239. // Concentre-se no próximo quadro se este tiver o foco
  240. if ($(this.el).hasClass('active')) {
  241. // Ir para o quadro anterior, a menos que seja o status
  242. if ($(this.el).prev().text().trim() !== 'status') {
  243. $(this.el).prev().click();
  244. } else {
  245. $(this.el).next().click();
  246. }
  247. }
  248. $(this.el).remove();
  249. },
  250. // Definir como guia ativa; Foco janela no frame
  251. setActive: function() {
  252. console.log('View setting active status');
  253. $(this.el).addClass('active')
  254. .siblings().removeClass('active');
  255. irc.frameWindow.focus(this.model);
  256. },
  257. render: function() {
  258. console.log(this.model);
  259. var self = this;
  260. var context = {
  261. text: this.model.get('name'),
  262. type: this.model.get('type'),
  263. isStatus: function() {
  264. return self.model.get('type') == 'status';
  265. }
  266. };
  267. var html = Mustache.to_html(this.tmpl, context);
  268. $(this.el).html(html);
  269. return this;
  270. }
  271. });
  272. var AppView = Backbone.View.extend({
  273. el: $('#content'),
  274. testFrames: $('#sidebar .frames'),
  275. frameList: $('header .frames'),
  276. initialize: function() {
  277. frames.bind('add', this.addTab, this);
  278. this.input = this.$('#prime-input');
  279. this.render();
  280. },
  281. events: {
  282. 'keypress #prime-input': 'sendInput',
  283. },
  284. addTab: function(frame) {
  285. var tab = new FrameTabView({model: frame});
  286. this.frameList.append(tab.el);
  287. tab.setActive();
  288. },
  289. joinChannel: function(name) {
  290. socket.emit('join', name);
  291. },
  292. // Mapeia os comandos IRC comuns para o padrão (RFC 1459)
  293. parse: function(text) {
  294. var command = text.split(' ')[0];
  295. console.log(command);
  296. var revised = '';
  297. switch (command) {
  298. case 'msg':
  299. revised = 'privmsg';
  300. break;
  301. default:
  302. revised = command;
  303. break;
  304. }
  305. return irc.util.swapCommand(command, revised, text);
  306. },
  307. sendInput: function(e) {
  308. if (e.keyCode != 13) return;
  309. var frame = irc.frameWindow.focused,
  310. input = this.input.val();
  311. if (input.indexOf('/') === 0) {
  312. var parsed = this.parse(input.substr(1));
  313. socket.emit('command', parsed);
  314. var msgParts = parsed.split(' ');
  315. if (msgParts[0].toLowerCase() === 'privmsg') {
  316. pm = frames.getByName(msgParts[1]) || new Frame({type: 'pm', name: msgParts[1]});
  317. pm.stream.add({sender: irc.me.get('nick'), raw: msgParts[2]})
  318. frames.add(pm);
  319. }
  320. } else {
  321. socket.emit('say', {
  322. target: frame.get('name'),
  323. message: input
  324. });
  325. frame.stream.add({sender: irc.me.get('nick'), raw: input});
  326. }
  327. this.input.val('');
  328. },
  329. render: function() {
  330. // Alocação dinâmica de altura
  331. this.el.show();
  332. $(window).resize(function() {
  333. sizeContent($('#frame #output'));
  334. sizeContent($('#frame #sidebar'));
  335. sizeContent($('#sidebar .nicks', '.stats'));
  336. });
  337. }
  338. });
  339. var ConnectView = Backbone.View.extend({
  340. el: $('#connect'),
  341. events: {
  342. 'click .btn': 'connect',
  343. 'keypress': 'connectOnEnter'
  344. },
  345. initialize: function() {
  346. _.bindAll(this);
  347. this.render();
  348. },
  349. render: function() {
  350. this.el.modal({backdrop: true, show: true});
  351. $('#connect-nick').focus();
  352. },
  353. connectOnEnter: function(e) {
  354. if (e.keyCode != 13) return;
  355. this.connect();
  356. },
  357. connect: function(e) {
  358. e && e.preventDefault();
  359. var channelInput = $('#connect-channels').val(),
  360. channels = channelInput ? channelInput.split(' ') : [];
  361. var connectInfo = {
  362. nick: $('#connect-nick').val(),
  363. server: $('#connect-server').val(),
  364. channels: channels
  365. };
  366. socket.emit('connect', connectInfo);
  367. $('#connect').modal('hide');
  368. irc.me = new Person({nick: connectInfo.nick});
  369. irc.frameWindow = new FrameView;
  370. irc.app = new AppView;
  371. // Cria status "frame"
  372. frames.add({name: 'status', type: 'status'});
  373. sizeContent($('#frame #output'));
  374. sizeContent($('#frame #sidebar'));
  375. sizeContent($('#sidebar .nicks', '.stats'));
  376. }
  377. });
  378. var connect = new ConnectView;
  379. // UTILS
  380. // =====
  381. function humanizeError(message) {
  382. var text = '';
  383. switch (message.command) {
  384. case 'err_unknowncommand':
  385. text = 'Isto não é um comando IRC conhecido.';
  386. break;
  387. }
  388. return text;
  389. }
  390. // Definir janela de saída para altura total, menos outros elementos
  391. function sizeContent(sel, additional) {
  392. var newHeight = $('html').height() - $('header').outerHeight(true)
  393. - $('#prime-input').outerHeight(true)
  394. - (sel.outerHeight(true) - sel.height()) - 10;
  395. // 10 = #content padding
  396. if (additional) {
  397. newHeight -= $(additional).outerHeight(true);
  398. }
  399. sel.height(newHeight);
  400. }
  401. // SOCKET EVENTS
  402. // =============
  403. socket.on('message', function(msg) {
  404. if (msg.to.indexOf('#') !== 0 &&
  405. msg.to.indexOf('&') !== 0 &&
  406. msg.to !== 'status') return;
  407. frame = frames.getByName(msg.to);
  408. if (frame) {
  409. frame.stream.add({sender: msg.from, raw: msg.text});
  410. }
  411. });
  412. socket.on('pm', function(msg) {
  413. pm = frames.getByName(msg.nick) || new Frame({type: 'pm', name: msg.nick});
  414. pm.stream.add({sender: msg.nick, raw: msg.text})
  415. frames.add(pm);
  416. })
  417. // Mensagem do dia
  418. socket.on('motd', function(data) {
  419. data.motd.split('\n').forEach(function(line) {
  420. frames.getByName('status').stream.add({sender: '', raw: line});
  421. });
  422. });
  423. // Entrando em um canal
  424. socket.on('join', function(data) {
  425. console.log('Join event received for ' + data.channel + ' - ' + data.nick);
  426. if (data.nick == irc.me.get('nick')) {
  427. frames.add({name: data.channel});
  428. } else {
  429. channel = frames.getByName(data.channel);
  430. channel.participants.add({nick: data.nick});
  431. var joinMessage = new Message({type: 'join', nick: data.nick});
  432. joinMessage.setText();
  433. channel.stream.add(joinMessage);
  434. }
  435. });
  436. // Evento Part channel
  437. socket.on('part', function(data) {
  438. console.log('Part event received for ' + data.channel + ' - ' + data.nick);
  439. if (data.nick == irc.me.get('nick')) {
  440. frames.getByName(data.channel).part();
  441. } else {
  442. channel = frames.getByName(data.channel);
  443. channel.participants.getByNick(data.nick).destroy();
  444. var partMessage = new Message({type: 'part', nick: data.nick});
  445. partMessage.setText();
  446. channel.stream.add(partMessage);
  447. }
  448. });
  449. // Definir evento topic
  450. socket.on('topic', function(data) {
  451. var channel = frames.getByName(data.channel);
  452. channel.set({topic: data.topic});
  453. });
  454. // Evento troca de nick
  455. socket.on('nick', function(data) {
  456. console.log('Nick change', data);
  457. if (data.oldNick == irc.me.get('nick')) {
  458. irc.me.set({nick: data.newNick});
  459. }
  460. // Definir novo nome em todos os canais
  461. data.channels.forEach(function(ch) {
  462. var channel = frames.getByName(ch);
  463. // Alterar nick na lista de usuários
  464. channel.participants.getByNick(data.oldNick).set({nick: data.newNick});
  465. // Enviar mensagem de alteração de nick para o fluxo de canal
  466. var nickMessage = new Message({
  467. type: 'nick',
  468. oldNick: data.oldNick,
  469. newNick: data.newNick
  470. });
  471. nickMessage.setText();
  472. channel.stream.add(nickMessage);
  473. });
  474. });
  475. socket.on('names', function(data) {
  476. var frame = frames.getByName(data.channel);
  477. console.log(data);
  478. for (var nick in data.nicks) {
  479. frame.participants.add({nick: nick, opStatus: data.nicks[nick]});
  480. }
  481. });
  482. socket.on('error', function(data) {
  483. console.log(data.message);
  484. frame = frames.getActive();
  485. error = humanizeError(data.message);
  486. frame.stream.add({type: 'error', raw: error});
  487. });
  488. });