message-actions.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import log from '@converse/headless/log';
  2. import { CustomElement } from 'shared/components/element.js';
  3. import { __ } from 'i18n';
  4. import { _converse, api, converse } from "@converse/headless/core";
  5. import { html } from 'lit';
  6. import { until } from 'lit/directives/until.js';
  7. const { Strophe, u } = converse.env;
  8. class MessageActions extends CustomElement {
  9. static get properties () {
  10. return {
  11. correcting: { type: Boolean },
  12. editable: { type: Boolean },
  13. hide_url_previews: { type: Boolean },
  14. is_retracted: { type: Boolean },
  15. message_type: { type: String },
  16. model: { type: Object },
  17. unfurls: { type: Number }
  18. }
  19. }
  20. render () {
  21. return html`${ until(this.renderActions(), '') }`;
  22. }
  23. async renderActions () {
  24. const buttons = await this.getActionButtons();
  25. const items = buttons.map(b => MessageActions.getActionsDropdownItem(b));
  26. if (items.length) {
  27. return html`<converse-dropdown class="chat-msg__actions" .items=${ items }></converse-dropdown>`;
  28. } else {
  29. return '';
  30. }
  31. }
  32. static getActionsDropdownItem (o) {
  33. return html`
  34. <button class="chat-msg__action ${o.button_class}" @click=${o.handler}>
  35. <converse-icon class="${o.icon_class}"
  36. path-prefix="${api.settings.get("assets_path")}"
  37. color="var(--text-color-lighten-15-percent)"
  38. size="1em"></converse-icon>
  39. ${o.i18n_text}
  40. </button>
  41. `;
  42. }
  43. onMessageEditButtonClicked (ev) {
  44. ev.preventDefault();
  45. const currently_correcting = this.model.collection.findWhere('correcting');
  46. // TODO: Use state intead of DOM querying
  47. // Then this code can also be put on the model
  48. const unsent_text = u.ancestor(this, '.chatbox')?.querySelector('.chat-textarea')?.value;
  49. if (unsent_text && (!currently_correcting || currently_correcting.get('message') !== unsent_text)) {
  50. if (!confirm(__('You have an unsent message which will be lost if you continue. Are you sure?'))) {
  51. return;
  52. }
  53. }
  54. if (currently_correcting !== this.model) {
  55. currently_correcting?.save('correcting', false);
  56. this.model.save('correcting', true);
  57. } else {
  58. this.model.save('correcting', false);
  59. }
  60. }
  61. async onDirectMessageRetractButtonClicked () {
  62. if (this.model.get('sender') !== 'me') {
  63. return log.error("onMessageRetractButtonClicked called for someone else's message!");
  64. }
  65. const retraction_warning = __(
  66. 'Be aware that other XMPP/Jabber clients (and servers) may ' +
  67. 'not yet support retractions and that this message may not ' +
  68. 'be removed everywhere.'
  69. );
  70. const messages = [__('Are you sure you want to retract this message?')];
  71. if (api.settings.get('show_retraction_warning')) {
  72. messages[1] = retraction_warning;
  73. }
  74. const result = await api.confirm(__('Confirm'), messages);
  75. if (result) {
  76. const chatbox = this.model.collection.chatbox;
  77. chatbox.retractOwnMessage(this.model);
  78. }
  79. }
  80. /**
  81. * Retract someone else's message in this groupchat.
  82. * @private
  83. * @param { _converse.Message } message - The message which we're retracting.
  84. * @param { string } [reason] - The reason for retracting the message.
  85. */
  86. async retractOtherMessage (reason) {
  87. const chatbox = this.model.collection.chatbox;
  88. const result = await chatbox.retractOtherMessage(this.model, reason);
  89. if (result === null) {
  90. const err_msg = __(`A timeout occurred while trying to retract the message`);
  91. api.alert('error', __('Error'), err_msg);
  92. log(err_msg, Strophe.LogLevel.WARN);
  93. } else if (u.isErrorStanza(result)) {
  94. const err_msg = __(`Sorry, you're not allowed to retract this message.`);
  95. api.alert('error', __('Error'), err_msg);
  96. log(err_msg, Strophe.LogLevel.WARN);
  97. log(result, Strophe.LogLevel.WARN);
  98. }
  99. }
  100. async onMUCMessageRetractButtonClicked () {
  101. const retraction_warning = __(
  102. 'Be aware that other XMPP/Jabber clients (and servers) may ' +
  103. 'not yet support retractions and that this message may not ' +
  104. 'be removed everywhere.'
  105. );
  106. if (this.model.mayBeRetracted()) {
  107. const messages = [__('Are you sure you want to retract this message?')];
  108. if (api.settings.get('show_retraction_warning')) {
  109. messages[1] = retraction_warning;
  110. }
  111. if (await api.confirm(__('Confirm'), messages)) {
  112. const chatbox = this.model.collection.chatbox;
  113. chatbox.retractOwnMessage(this.model);
  114. }
  115. } else if (await this.model.mayBeModerated()) {
  116. if (this.model.get('sender') === 'me') {
  117. let messages = [__('Are you sure you want to retract this message?')];
  118. if (api.settings.get('show_retraction_warning')) {
  119. messages = [messages[0], retraction_warning, messages[1]];
  120. }
  121. !!(await api.confirm(__('Confirm'), messages)) && this.retractOtherMessage();
  122. } else {
  123. let messages = [
  124. __('You are about to retract this message.'),
  125. __('You may optionally include a message, explaining the reason for the retraction.')
  126. ];
  127. if (api.settings.get('show_retraction_warning')) {
  128. messages = [messages[0], retraction_warning, messages[1]];
  129. }
  130. const reason = await api.prompt(__('Message Retraction'), messages, __('Optional reason'));
  131. reason !== false && this.retractOtherMessage(reason);
  132. }
  133. } else {
  134. const err_msg = __(`Sorry, you're not allowed to retract this message`);
  135. api.alert('error', __('Error'), err_msg);
  136. }
  137. }
  138. onMessageRetractButtonClicked (ev) {
  139. ev?.preventDefault?.();
  140. const chatbox = this.model.collection.chatbox;
  141. if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
  142. this.onMUCMessageRetractButtonClicked();
  143. } else {
  144. this.onDirectMessageRetractButtonClicked();
  145. }
  146. }
  147. onHidePreviewsButtonClicked (ev) {
  148. ev?.preventDefault?.();
  149. if (this.hide_url_previews) {
  150. this.model.save({
  151. 'hide_url_previews': false,
  152. 'url_preview_transition': 'fade-in'
  153. });
  154. } else {
  155. this.model.set('url_preview_transition', 'fade-out');
  156. }
  157. }
  158. async getActionButtons () {
  159. const buttons = [];
  160. if (this.editable) {
  161. buttons.push({
  162. 'i18n_text': this.correcting ? __('Cancel Editing') : __('Edit'),
  163. 'handler': ev => this.onMessageEditButtonClicked(ev),
  164. 'button_class': 'chat-msg__action-edit',
  165. 'icon_class': 'fa fa-pencil-alt',
  166. 'name': 'edit'
  167. });
  168. }
  169. const may_be_moderated = this.model.get('type') === 'groupchat' && await this.model.mayBeModerated();
  170. const retractable = !this.is_retracted && (this.model.mayBeRetracted() || may_be_moderated);
  171. if (retractable) {
  172. buttons.push({
  173. 'i18n_text': __('Retract'),
  174. 'handler': ev => this.onMessageRetractButtonClicked(ev),
  175. 'button_class': 'chat-msg__action-retract',
  176. 'icon_class': 'fas fa-trash-alt',
  177. 'name': 'retract'
  178. });
  179. }
  180. if (!this.model.collection) {
  181. // While we were awaiting, this model got removed from the
  182. // collection (happens during tests)
  183. return [];
  184. }
  185. const ogp_metadata = this.model.get('ogp_metadata') || [];
  186. const chatbox = this.model.collection.chatbox;
  187. if (chatbox.get('type') === _converse.CHATROOMS_TYPE &&
  188. api.settings.get('muc_show_ogp_unfurls') &&
  189. ogp_metadata.length) {
  190. let title;
  191. const hidden_preview = this.hide_url_previews;
  192. if (ogp_metadata.length > 1) {
  193. title = hidden_preview ? __('Show URL previews') : __('Hide URL previews');
  194. } else {
  195. title = hidden_preview ? __('Show URL preview') : __('Hide URL preview');
  196. }
  197. buttons.push({
  198. 'i18n_text': title,
  199. 'handler': ev => this.onHidePreviewsButtonClicked(ev),
  200. 'button_class': 'chat-msg__action-hide-previews',
  201. 'icon_class': this.hide_url_previews ? 'fas fa-eye' : 'fas fa-eye-slash',
  202. 'name': 'hide'
  203. });
  204. }
  205. /**
  206. * *Hook* which allows plugins to add more message action buttons
  207. * @event _converse#getMessageActionButtons
  208. * @example
  209. * api.listen.on('getMessageActionButtons', (el, buttons) => {
  210. * buttons.push({
  211. * 'i18n_text': 'Foo',
  212. * 'handler': ev => alert('Foo!'),
  213. * 'button_class': 'chat-msg__action-foo',
  214. * 'icon_class': 'fa fa-check',
  215. * 'name': 'foo'
  216. * });
  217. * return buttons;
  218. * });
  219. */
  220. return api.hook('getMessageActionButtons', this, buttons);
  221. }
  222. }
  223. api.elements.define('converse-message-actions', MessageActions);