2
0

message.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. import "./message-body.js";
  2. import MessageVersionsModal from '../modals/message-versions.js';
  3. import dayjs from 'dayjs';
  4. import filesize from "filesize";
  5. import tpl_spinner from '../templates/spinner.js';
  6. import { CustomElement } from './element.js';
  7. import { __ } from '@converse/headless/i18n';
  8. import { _converse, api, converse } from "@converse/headless/converse-core";
  9. import { html } from 'lit-element';
  10. import { renderAvatar } from './../templates/directives/avatar';
  11. import { renderRetractionLink } from './../templates/directives/retraction';
  12. const { Strophe } = converse.env;
  13. const u = converse.env.utils;
  14. const i18n_edit_message = __('Edit this message');
  15. const i18n_edited = __('This message has been edited');
  16. const i18n_show = __('Show more');
  17. const i18n_show_less = __('Show less');
  18. const i18n_uploading = __('Uploading file:');
  19. const i18n_new_messages = __('New messages');
  20. class Message extends CustomElement {
  21. static get properties () {
  22. return {
  23. allow_retry: { type: Boolean },
  24. chatview: { type: Object},
  25. correcting: { type: Boolean },
  26. editable: { type: Boolean },
  27. error: { type: String },
  28. error_text: { type: String },
  29. from: { type: String },
  30. has_mentions: { type: Boolean },
  31. hats: { type: Array },
  32. edited: { type: String },
  33. is_delayed: { type: Boolean },
  34. is_encrypted: { type: Boolean },
  35. is_first_unread: { type: Boolean },
  36. is_me_message: { type: Boolean },
  37. is_only_emojis: { type: Boolean },
  38. is_retracted: { type: Boolean },
  39. is_spoiler: { type: Boolean },
  40. is_spoiler_visible: { type: Boolean },
  41. message_type: { type: String },
  42. model: { type: Object },
  43. moderated_by: { type: String },
  44. moderation_reason: { type: String },
  45. msgid: { type: String },
  46. occupant_affiliation: { type: String },
  47. occupant_role: { type: String },
  48. oob_url: { type: String },
  49. progress: { type: Number },
  50. reason: { type: String },
  51. received: { type: String },
  52. retractable: { type: Boolean },
  53. sender: { type: String },
  54. show_spinner: { type: Boolean },
  55. spoiler_hint: { type: String },
  56. subject: { type: String },
  57. time: { type: String },
  58. username: { type: String }
  59. }
  60. }
  61. render () {
  62. const format = api.settings.get('time_format');
  63. this.pretty_time = dayjs(this.time).format(format);
  64. if (this.show_spinner) {
  65. return tpl_spinner();
  66. } else if (this.model.get('file') && !this.model.get('oob_url')) {
  67. return this.renderFileProgress();
  68. } else if (['error', 'info'].includes(this.message_type)) {
  69. return this.renderInfoMessage();
  70. } else {
  71. return this.renderChatMessage();
  72. }
  73. }
  74. updated () {
  75. // XXX: This is ugly but tests rely on this event.
  76. // For "normal" chat messages the event is fired in
  77. // src/templates/directives/body.js
  78. if (
  79. this.show_spinner ||
  80. (this.model.get('file') && !this.model.get('oob_url')) ||
  81. (['error', 'info'].includes(this.message_type))
  82. ) {
  83. this.model.collection?.trigger('rendered', this.model);
  84. }
  85. }
  86. renderInfoMessage () {
  87. const isodate = dayjs(this.model.get('time')).toISOString();
  88. const i18n_retry = __('Retry');
  89. return html`
  90. <div class="message chat-info chat-${this.message_type}"
  91. data-isodate="${isodate}"
  92. data-type="${this.data_name}"
  93. data-value="${this.data_value}">
  94. <div class="chat-info__message">
  95. ${ this.model.getMessageText() }
  96. </div>
  97. ${ this.reason ? html`<q class="reason">${this.reason}</q>` : `` }
  98. ${ this.error_text ? html`<q class="reason">${this.error_text}</q>` : `` }
  99. ${ this.allow_retry ? html`<a class="retry" @click=${this.onRetryClicked}>${i18n_retry}</a>` : '' }
  100. </div>
  101. `;
  102. }
  103. renderFileProgress () {
  104. const filename = this.model.file.name;
  105. const size = filesize(this.model.file.size);
  106. return html`
  107. <div class="message chat-msg">
  108. ${ renderAvatar(this.getAvatarData()) }
  109. <div class="chat-msg__content">
  110. <span class="chat-msg__text">${i18n_uploading} <strong>${filename}</strong>, ${size}</span>
  111. <progress value="${this.progress}"/>
  112. </div>
  113. </div>`;
  114. }
  115. renderChatMessage () {
  116. const is_groupchat_message = (this.message_type === 'groupchat');
  117. return html`
  118. ${ this.is_first_unread ? html`<div class="message date-separator"><hr class="separator"><span class="separator-text">${ i18n_new_messages }</span></div>` : '' }
  119. <div class="message chat-msg ${this.message_type} ${this.getExtraMessageClasses()}
  120. ${ this.is_me_message ? 'chat-msg--action' : '' }
  121. ${this.isFollowup() ? 'chat-msg--followup' : ''}"
  122. data-isodate="${this.time}" data-msgid="${this.msgid}" data-from="${this.from}" data-encrypted="${this.is_encrypted}">
  123. ${ (this.is_me_message || this.type === 'headline') ? '' : renderAvatar(this.getAvatarData()) }
  124. <div class="chat-msg__content chat-msg__content--${this.sender} ${this.is_me_message ? 'chat-msg__content--action' : ''}">
  125. <span class="chat-msg__heading">
  126. ${ (this.is_me_message) ? html`
  127. <time timestamp="${this.time}" class="chat-msg__time">${this.pretty_time}</time>
  128. ${this.hats.map(hat => html`<span class="badge badge-secondary">${hat}</span>`)}
  129. ` : '' }
  130. <span class="chat-msg__author">${ this.is_me_message ? '**' : ''}${this.username}</span>
  131. ${ !this.is_me_message ? this.renderAvatarByline() : '' }
  132. ${ this.is_encrypted ? html`<span class="fa fa-lock"></span>` : '' }
  133. </span>
  134. <div class="chat-msg__body chat-msg__body--${this.message_type} ${this.received ? 'chat-msg__body--received' : '' } ${this.is_delayed ? 'chat-msg__body--delayed' : '' }">
  135. <div class="chat-msg__message">
  136. ${ this.is_retracted ? this.renderRetraction() : this.renderMessageText() }
  137. </div>
  138. ${ (this.received && !this.is_me_message && !is_groupchat_message) ? html`<span class="fa fa-check chat-msg__receipt"></span>` : '' }
  139. ${ (this.edited) ? html`<i title="${ i18n_edited }" class="fa fa-edit chat-msg__edit-modal" @click=${this.showMessageVersionsModal}></i>` : '' }
  140. <div class="chat-msg__actions">
  141. ${ this.editable ?
  142. html`<button
  143. class="chat-msg__action chat-msg__action-edit"
  144. title="${i18n_edit_message}"
  145. @click=${this.onMessageEditButtonClicked}
  146. >
  147. <fa-icon class="fas fa-pencil-alt" path-prefix="dist" color="var(--text-color-lighten-15-percent)" size="1em"></fa-icon>
  148. </button>` : '' }
  149. ${ renderRetractionLink(this) }
  150. </div>
  151. </div>
  152. </div>
  153. </div>`;
  154. }
  155. getAvatarData () {
  156. const image_type = this.model.vcard.get('image_type');
  157. const image_data = this.model.vcard.get('image');
  158. const image = "data:" + image_type + ";base64," + image_data;
  159. return {
  160. 'classes': 'chat-msg__avatar',
  161. 'height': 36,
  162. 'width': 36,
  163. image,
  164. };
  165. }
  166. async onRetryClicked () {
  167. this.show_spinner = true;
  168. await this.model.error.retry();
  169. this.model.destroy();
  170. this.parentElement.removeChild(this);
  171. }
  172. onMessageRetractButtonClicked (ev) {
  173. ev.preventDefault();
  174. this.chatview.onMessageRetractButtonClicked(this.model);
  175. }
  176. onMessageEditButtonClicked (ev) {
  177. ev.preventDefault();
  178. this.chatview.onMessageEditButtonClicked(this.model);
  179. }
  180. isFollowup () {
  181. const messages = this.model.collection.models;
  182. const idx = messages.indexOf(this.model);
  183. const prev_model = idx ? messages[idx-1] : null;
  184. if (prev_model === null) {
  185. return false;
  186. }
  187. const date = dayjs(this.time);
  188. return this.from === prev_model.get('from') &&
  189. !this.is_me_message &&
  190. !prev_model.isMeCommand() &&
  191. this.message_type !== 'info' &&
  192. prev_model.get('type') !== 'info' &&
  193. date.isBefore(dayjs(prev_model.get('time')).add(10, 'minutes')) &&
  194. this.is_encrypted === prev_model.get('is_encrypted');
  195. }
  196. getExtraMessageClasses () {
  197. const extra_classes = [
  198. ...(this.is_delayed ? ['delayed'] : []),
  199. ...(this.is_retracted ? ['chat-msg--retracted'] : [])
  200. ];
  201. if (this.message_type === 'groupchat') {
  202. this.occupant_role && extra_classes.push(this.occupant_role);
  203. this.occupant_affiliation && extra_classes.push(this.occupant_affiliation);
  204. if (this.sender === 'them' && this.has_mentions) {
  205. extra_classes.push('mentioned');
  206. }
  207. }
  208. this.correcting && extra_classes.push('correcting');
  209. return extra_classes.filter(c => c).join(" ");
  210. }
  211. getRetractionText () {
  212. if (this.message_type === 'groupchat' && this.moderated_by) {
  213. const retracted_by_mod = this.moderated_by;
  214. const chatbox = this.model.collection.chatbox;
  215. if (!this.model.mod) {
  216. this.model.mod =
  217. chatbox.occupants.findOccupant({'jid': retracted_by_mod}) ||
  218. chatbox.occupants.findOccupant({'nick': Strophe.getResourceFromJid(retracted_by_mod)});
  219. }
  220. const modname = this.model.mod ? this.model.mod.getDisplayName() : 'A moderator';
  221. return __('%1$s has removed this message', modname);
  222. } else {
  223. return __('%1$s has removed this message', this.model.getDisplayName());
  224. }
  225. }
  226. renderRetraction () {
  227. const retraction_text = this.is_retracted ? this.getRetractionText() : null;
  228. return html`
  229. <div>${retraction_text}</div>
  230. ${ this.moderation_reason ? html`<q class="chat-msg--retracted__reason">${this.moderation_reason}</q>` : '' }
  231. `;
  232. }
  233. renderMessageText () {
  234. const tpl_spoiler_hint = html`
  235. <div class="chat-msg__spoiler-hint">
  236. <span class="spoiler-hint">${this.spoiler_hint}</span>
  237. <a class="badge badge-info spoiler-toggle" href="#" @click=${this.toggleSpoilerMessage}>
  238. <i class="fa ${this.is_spoiler_visible ? 'fa-eye-slash' : 'fa-eye'}"></i>
  239. ${ this.is_spoiler_visible ? i18n_show_less : i18n_show }
  240. </a>
  241. </div>
  242. `;
  243. return html`
  244. ${ this.is_spoiler ? tpl_spoiler_hint : '' }
  245. ${ this.subject ? html`<div class="chat-msg__subject">${this.subject}</div>` : '' }
  246. <converse-chat-message-body
  247. .model="${this.model}"
  248. ?is_me_message="${this.is_me_message}"
  249. ?is_only_emojis="${this.is_only_emojis}"
  250. ?is_spoiler="${this.is_spoiler}"
  251. ?is_spoiler_visible="${this.is_spoiler_visible}"
  252. text="${this.model.getMessageText()}"></converse-chat-message-body>
  253. ${ this.oob_url ? html`<div class="chat-msg__media">${u.getOOBURLMarkup(_converse, this.oob_url)}</div>` : '' }
  254. <div class="chat-msg__error">${ this.error_text || this.error }</div>
  255. `;
  256. }
  257. renderAvatarByline () {
  258. return html`
  259. ${ this.hats.map(h => html`<span class="badge badge-secondary">${h.title}</span>`) }
  260. <time timestamp="${this.time}" class="chat-msg__time">${this.pretty_time}</time>
  261. `;
  262. }
  263. showMessageVersionsModal (ev) {
  264. ev.preventDefault();
  265. if (this.message_versions_modal === undefined) {
  266. this.message_versions_modal = new MessageVersionsModal({'model': this.model});
  267. }
  268. this.message_versions_modal.show(ev);
  269. }
  270. toggleSpoilerMessage (ev) {
  271. ev?.preventDefault();
  272. this.model.save({'is_spoiler_visible': !this.model.get('is_spoiler_visible')});
  273. }
  274. }
  275. customElements.define('converse-chat-message', Message);