1
0

uiMessage.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. /* jshint sub: true */
  2. /**
  3. * @constructor
  4. * @extends {RoomHistory}
  5. * @param {Room|string} room or roomId
  6. * @param {number} keepMessages number of messages to keep in memory
  7. * @param {Array|undefined} evts
  8. * @param {number|undefined} now
  9. **/
  10. function UiRoomHistory(room, keepMessages, evts, now) {
  11. RoomHistory.call(this, room, keepMessages, 0, evts, now);
  12. }
  13. UiRoomHistory.prototype = Object.create(RoomHistory.prototype);
  14. UiRoomHistory.prototype.constructor = UiRoomHistory;
  15. UiRoomHistory.prototype.messageFactory = function(ev, ts) {
  16. if (ev["isMeMessage"] === true)
  17. return new UiMeMessage(this.id, ev, ts);
  18. if (ev["isNotice"] === true)
  19. return new UiNoticeMessage(this.id, ev, ts);
  20. return new UiMessage(this.id, ev, ts);
  21. };
  22. UiRoomHistory.prototype.invalidateAllMessages = function() {
  23. this.messages.forEach(function(m) {
  24. m.invalidate();
  25. });
  26. };
  27. /** @interface */
  28. function IUiMessage() {}
  29. /**
  30. * @return {Element}
  31. **/
  32. IUiMessage.prototype.getDom = function() {};
  33. /**
  34. * @return {IUiMessage}
  35. **/
  36. IUiMessage.prototype.removeDom = function() {};
  37. /**
  38. * @return {IUiMessage}
  39. **/
  40. IUiMessage.prototype.invalidate = function() {};
  41. /**
  42. * @return {IUiMessage}
  43. **/
  44. IUiMessage.prototype.createDom = function() {};
  45. /**
  46. * @return {IUiMessage}
  47. **/
  48. IUiMessage.prototype.updateDom = function() {};
  49. /**
  50. * @return {Element}
  51. **/
  52. IUiMessage.prototype.duplicateDom = function() {};
  53. /** @const */
  54. var AbstractUiMessage = (function() {
  55. var updateReactions = function(_this, channelId) {
  56. var reactionFrag = document.createDocumentFragment();
  57. if (_this.reactions) {
  58. for (var reaction in _this.reactions) {
  59. var reac = createReactionDom(channelId, _this.id, reaction, _this.reactions[reaction]);
  60. if (reac)
  61. reactionFrag.appendChild(reac);
  62. }
  63. }
  64. _this.dom.reactions.textContent = '';
  65. _this.dom.reactions.appendChild(reactionFrag);
  66. },
  67. _linkFilter = function(msgContext, str) {
  68. var sep = str.indexOf('|'),
  69. link,
  70. text,
  71. style,
  72. classes,
  73. isInternal = false;
  74. if (sep === -1) {
  75. link = str;
  76. } else {
  77. link = str.substr(0, sep);
  78. text = str.substr(sep +1);
  79. }
  80. var newLink;
  81. if (link[0] === '@') {
  82. newLink = msgContext.context.getId() +'|' +link.substr(1);
  83. var user = DATA.context.getUser(newLink);
  84. if (user) {
  85. isInternal = true;
  86. link = '#' +user.privateRoom.id;
  87. text = '@' +user.getName();
  88. style = "background-color:" +makeUserColor(user.getName());
  89. classes = [ R.klass.msg.linkuser, R.klass.msg.link ];
  90. } else {
  91. return null;
  92. }
  93. } else if (link[0] === '#') {
  94. newLink = msgContext.context.getId() +'|' +link.substr(1);
  95. var chan = DATA.context.getChannel(newLink);
  96. if (chan) {
  97. isInternal = true;
  98. link = '#' +newLink;
  99. text = '#' +chan.name;
  100. classes = [ R.klass.msg.linkchan, R.klass.msg.link ];
  101. } else {
  102. return null;
  103. }
  104. } else {
  105. if (!link.match(/^(https?|mailto):\/\//i))
  106. return null;
  107. classes = [ R.klass.msg.link ];
  108. isInternal = false;
  109. }
  110. return {
  111. link: link,
  112. text: text || link,
  113. style: style,
  114. classes: classes,
  115. isInternal: isInternal
  116. };
  117. },
  118. _formatText = function(_this, text) {
  119. return formatText(text, {
  120. highlights: _this.context.self.prefs.highlights,
  121. emojiFormatFunction: function(emoji) {
  122. if (emoji[0] === ':' && emoji[emoji.length -1] === ':')
  123. emoji = emoji.substr(1, emoji.length -2);
  124. var emojiDom = makeEmojiDom(emoji);
  125. if (emojiDom) {
  126. var domParent = document.createElement("span");
  127. domParent.className = R.klass.emoji.small;
  128. domParent.appendChild(emojiDom);
  129. return domParent.outerHTML;
  130. }
  131. return null;
  132. },
  133. linkFilter: function(link) {
  134. return _linkFilter(_this, link);
  135. }
  136. });
  137. },
  138. updateAttachments = function(_this, channelId) {
  139. var attachmentFrag = document.createDocumentFragment(),
  140. containImages = false,
  141. hasAttachment = false;
  142. for (var i =0, nbAttachments = _this.attachments.length; i < nbAttachments; i++) {
  143. var attachment = _this.attachments[i];
  144. if (attachment) {
  145. var domAttachment = createAttachmentDom(channelId, _this, attachment, i);
  146. if (domAttachment) {
  147. attachmentFrag.appendChild(domAttachment);
  148. containImages |= !!attachment["image_url"];
  149. hasAttachment = true;
  150. }
  151. }
  152. }
  153. _this.dom.attachments.textContent = '';
  154. _this.dom.attachments.appendChild(attachmentFrag);
  155. if (hasAttachment) {
  156. _this.dom.attachmentWrapper.classList.remove(R.klass.hidden);
  157. if (!_this.dom.attachmentWrapper.toggled) {
  158. if (CONFIG.shouldExpandAttachment(containImages))
  159. _this.dom.attachmentWrapper["open"] = true;
  160. else
  161. _this.dom.attachmentWrapper["open"] = false;
  162. }
  163. } else {
  164. _this.dom.attachmentWrapper.classList.add(R.klass.hidden);
  165. }
  166. },
  167. updateHover = function(_this, channelId) {
  168. if (_this.dom.hover.hoverStar)
  169. _this.dom.hover.hoverStar.style.backgroundImage = _this.starred ? 'url("star_full.png")' : 'url("star_empty.png")';
  170. },
  171. updateCommon = function(_this, sender) {
  172. _this.dom.ts.innerHTML = locale.formatDate(_this.ts);
  173. _this.dom.textDom.innerHTML = _formatText(_this, _this.text);
  174. _this.dom.authorName.textContent = sender ? sender.getName() : (_this.username || "?");
  175. };
  176. return {
  177. /** @type {Element|null} *
  178. dom: null,
  179. /** @type {boolean} *
  180. uiNeedRefresh: true,
  181. */
  182. /** @param {IUiMessage} _this */
  183. invalidate: function(_this) {
  184. _this.uiNeedRefresh = true;
  185. return _this;
  186. },
  187. /** @param {UiMessage|UiMeMessage|UiNoticeMessage} _this */
  188. removeDom: function(_this) {
  189. if (_this.dom && _this.dom.parentElement) {
  190. _this.dom.remove();
  191. delete(_this.dom);
  192. }
  193. return _this;
  194. },
  195. /** @param {UiMessage|UiMeMessage|UiNoticeMessage} _this */
  196. getDom: function(_this) {
  197. if (!_this.dom) {
  198. _this.createDom().updateDom();
  199. } else if (_this.uiNeedRefresh) {
  200. _this.uiNeedRefresh = false;
  201. _this.updateDom();
  202. }
  203. return _this.dom;
  204. },
  205. /** @param {UiMessage|UiMeMessage|UiNoticeMessage} _this */
  206. updateDom: function(_this) {
  207. var sender = DATA.context.getUser(_this.userId);
  208. updateCommon(_this, sender);
  209. updateAttachments(_this, _this.channelId);
  210. updateReactions(_this, _this.channelId);
  211. updateHover(_this, _this.channelId);
  212. if (_this.edited) {
  213. _this.dom.edited.innerHTML = locale.edited(_this.edited);
  214. _this.dom.classList.add(R.klass.msg.editedStatus);
  215. }
  216. return _this;
  217. },
  218. duplicateDom: function(_this) {
  219. return _this.getDom().cloneNode(true);
  220. },
  221. formatText: function(_this, text) {
  222. return _formatText(_this, text);
  223. }
  224. };
  225. })();
  226. /**
  227. * @constructor
  228. * @implements {IUiMessage}
  229. * @extends {MeMessage}
  230. * @param {*} ev
  231. * @param {number} ts
  232. **/
  233. function UiMeMessage(channelId, ev, ts) {
  234. // Extends AbstractUiMessage and MeMessage
  235. MeMessage.call(this, ev, ts);
  236. /** @const @type {ChatContext} */
  237. this.context = DATA.context.getChannelContext(channelId).getChatContext();
  238. /** @type {string} @const */
  239. this.channelId = channelId;
  240. this.dom = AbstractUiMessage.dom;
  241. this.uiNeedRefresh = AbstractUiMessage.uiNeedRefresh;
  242. }
  243. UiMeMessage.prototype = Object.create(MeMessage.prototype);
  244. UiMeMessage.prototype.constructor = UiMeMessage;
  245. UiMeMessage.prototype.invalidate = function() {
  246. return AbstractUiMessage.invalidate(this);
  247. };
  248. UiMeMessage.prototype.formatText = function(text) {
  249. return AbstractUiMessage.formatText(this, text);
  250. };
  251. UiMeMessage.prototype.removeDom = function() {
  252. return AbstractUiMessage.removeDom(this);
  253. };
  254. UiMeMessage.prototype.getDom = function() {
  255. return AbstractUiMessage.getDom(this);
  256. };
  257. UiMeMessage.prototype.createDom = function() {
  258. this.dom = doCreateMessageDom(this);
  259. this.dom.classList.add(R.klass.msg.meMessage);
  260. return this;
  261. };
  262. UiMeMessage.prototype.duplicateDom = function() {
  263. return AbstractUiMessage.duplicateDom(this);
  264. };
  265. UiMeMessage.prototype.updateDom = function() {
  266. AbstractUiMessage.updateDom(this);
  267. return this;
  268. };
  269. UiMeMessage.prototype.update = function(ev, ts) {
  270. MeMessage.prototype.update.call(this, ev, ts);
  271. this.invalidate();
  272. };
  273. /**
  274. * @constructor
  275. * @implements {IUiMessage}
  276. * @extends {Message}
  277. * @param {*} ev
  278. * @param {number} ts
  279. **/
  280. function UiMessage(channelId, ev, ts) {
  281. // Extends AbstractUiMessage and Message
  282. Message.call(this, ev, ts);
  283. /** @const @type {ChatContext} */
  284. this.context = DATA.context.getChannelContext(channelId).getChatContext();
  285. /** @type {string} @const */
  286. this.channelId = channelId;
  287. /** @type {Element} */
  288. this.dom = AbstractUiMessage.dom;
  289. /** @type {boolean} */
  290. this.uiNeedRefresh = AbstractUiMessage.uiNeedRefresh;
  291. }
  292. UiMessage.prototype = Object.create(Message.prototype);
  293. UiMessage.prototype.constructor = UiMessage;
  294. UiMessage.prototype.invalidate = function() {
  295. return AbstractUiMessage.invalidate(this);
  296. };
  297. UiMessage.prototype.formatText = function(text) {
  298. return AbstractUiMessage.formatText(this, text);
  299. };
  300. UiMessage.prototype.removeDom = function() {
  301. return AbstractUiMessage.removeDom(this);
  302. };
  303. UiMessage.prototype.getDom = function() {
  304. return AbstractUiMessage.getDom(this);
  305. };
  306. UiMessage.prototype.createDom = function() {
  307. this.dom = doCreateMessageDom(this);
  308. return this;
  309. };
  310. UiMessage.prototype.duplicateDom = function() {
  311. return AbstractUiMessage.duplicateDom(this);
  312. };
  313. UiMessage.prototype.updateDom = function() {
  314. AbstractUiMessage.updateDom(this);
  315. return this;
  316. };
  317. UiMessage.prototype.update = function(ev, ts) {
  318. Message.prototype.update.call(this, ev, ts);
  319. this.invalidate();
  320. var match = this.text.match(/^<?https:\/\/www\.openstreetmap\.org\/\?mlat=(-?[0-9\.]+)(&amp;|&)mlon=(-?[0-9\.]+)(&amp;|&)macc=([0-9\.]+)[^\s]*/);
  321. if (match) {
  322. var lat = match[1],
  323. lon = match[3],
  324. acc = match[5];
  325. this.text = this.text.substr(0, match.index) +this.text.substr(match.index +match[0].length).trim();
  326. this.attachments.unshift({
  327. "color": "#008000",
  328. "text": match[0],
  329. "footer": "Open Street Map",
  330. "footer_icon": "https://www.openstreetmap.org/assets/favicon-32x32-36d06d8a01933075bc7093c9631cffd02d49b03b659f767340f256bb6839d990.png",
  331. "geo": {
  332. "latitude": match[1],
  333. "longitude": match[3],
  334. "accuracy": match[5]
  335. }
  336. });
  337. }
  338. };
  339. /**
  340. * @constructor
  341. * @implements {IUiMessage}
  342. * @extends {NoticeMessage}
  343. * @param {*} ev
  344. * @param {number} ts
  345. **/
  346. function UiNoticeMessage(channelId, ev, ts) {
  347. // Extends AbstractUiMessage and NoticeMessage
  348. NoticeMessage.call(this, ev, ts);
  349. /** @const @type {ChatContext} */
  350. this.context = DATA.context.getChannelContext(channelId).getChatContext();
  351. /** @type {string} @const */
  352. this.channelId = channelId;
  353. /** @type {Element} */
  354. this.dom; // jshint ignore:line
  355. /** @type {Element} */
  356. this.domWrapper = null;
  357. /** @type {boolean} */
  358. this.uiNeedRefresh = true;
  359. }
  360. UiNoticeMessage.prototype = Object.create(NoticeMessage.prototype);
  361. UiNoticeMessage.prototype.constructor = UiNoticeMessage;
  362. UiNoticeMessage.prototype.invalidate = function() {
  363. return AbstractUiMessage.invalidate(this);
  364. };
  365. UiNoticeMessage.prototype.formatText = function(text) {
  366. return AbstractUiMessage.formatText(this, text);
  367. };
  368. UiNoticeMessage.prototype.removeDom = function() {
  369. if (this.domWrapper && this.domWrapper.parentElement) {
  370. this.domWrapper.remove();
  371. delete(this.domWrapper);
  372. }
  373. if (this.dom)
  374. delete(this.dom);
  375. return this;
  376. };
  377. UiNoticeMessage.prototype.getDom = function() {
  378. AbstractUiMessage.getDom(this);
  379. return this.domWrapper;
  380. };
  381. UiNoticeMessage.prototype.duplicateDom = function() {
  382. return this.domWrapper.cloneNode(true);
  383. };
  384. UiNoticeMessage.prototype.createDom = function() {
  385. this.dom = doCreateMessageDom(this);
  386. this.domWrapper = document.createElement("span");
  387. this.dom.classList.add(R.klass.msg.notice);
  388. this.domWrapper.className = R.klass.msg.notice;
  389. this.domWrapper.textContent = locale.onlyVisible;
  390. this.domWrapper.appendChild(this.dom);
  391. return this;
  392. };
  393. UiNoticeMessage.prototype.updateDom = function() {
  394. AbstractUiMessage.updateDom(this);
  395. return this;
  396. };
  397. UiNoticeMessage.prototype.update = function(ev, ts) {
  398. NoticeMessage.prototype.update.call(this, ev, ts);
  399. this.invalidate();
  400. };
  401. /**
  402. * @param {string} input
  403. * @param {boolean} isMe
  404. * @return {Element}
  405. **/
  406. function createTmpMsgDom(input, isMe) {
  407. var dom = doCreateMessageDom(null),
  408. sender = SELECTED_CONTEXT.self;
  409. dom.classList.add(R.klass.msg.pending);
  410. if (isMe)
  411. dom.classList.add(R.klass.msg.meMessage);
  412. dom.textDom.innerHTML = formatText(input, {
  413. emojiFormatFunction: function(emoji) {
  414. if (emoji[0] === ':' && emoji[emoji.length -1] === ':')
  415. emoji = emoji.substr(1, emoji.length -2);
  416. var emojiDom = makeEmojiDom(emoji);
  417. if (emojiDom) {
  418. var domParent = document.createElement("span");
  419. domParent.className = R.klass.emoji.small;
  420. domParent.appendChild(emojiDom);
  421. return domParent.outerHTML;
  422. }
  423. return null;
  424. }
  425. });
  426. dom.authorName.textContent = SELECTED_CONTEXT.self ? SELECTED_CONTEXT.self.getName() : "";
  427. var dot = document.createElement("span");
  428. dot.className = R.klass.typing.dot1;
  429. dot.textContent = '.';
  430. dom.ts.appendChild(dot);
  431. dot = document.createElement("span");
  432. dot.className = R.klass.typing.dot2;
  433. dot.textContent = '.';
  434. dom.ts.appendChild(dot);
  435. dot = document.createElement("span");
  436. dot.className = R.klass.typing.dot3;
  437. dot.textContent = '.';
  438. dom.ts.appendChild(dot);
  439. dom.ts.classList.add(R.klass.typing.container);
  440. dom.attachmentWrapper.remove();
  441. return dom;
  442. }