message.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. /**
  2. * This file is part of Threema Web.
  3. *
  4. * Threema Web is free software: you can redistribute it and/or modify it
  5. * under the terms of the GNU Affero General Public License as published by
  6. * the Free Software Foundation, either version 3 of the License, or (at
  7. * your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful, but
  10. * WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
  12. * General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU Affero General Public License
  15. * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17. // tslint:disable:max-line-length
  18. import {saveAs} from 'file-saver';
  19. import {BrowserInfo} from '../helpers/browser_info';
  20. import {getSenderIdentity} from '../helpers/messages';
  21. import {BrowserService} from '../services/browser';
  22. import {MessageService} from '../services/message';
  23. import {ReceiverService} from '../services/receiver';
  24. import {WebClientService} from '../services/webclient';
  25. export default [
  26. 'BrowserService',
  27. 'MessageService',
  28. 'ReceiverService',
  29. 'WebClientService',
  30. '$mdDialog',
  31. '$mdToast',
  32. '$translate',
  33. '$rootScope',
  34. '$log',
  35. function(browserService: BrowserService,
  36. messageService: MessageService,
  37. receiverService: ReceiverService,
  38. webClientService: WebClientService,
  39. $mdDialog: ng.material.IDialogService,
  40. $mdToast: ng.material.IToastService,
  41. $translate: ng.translate.ITranslateService,
  42. $rootScope: ng.IRootScopeService,
  43. $log: ng.ILogService) {
  44. return {
  45. restrict: 'E',
  46. scope: {},
  47. bindToController: {
  48. type: '=eeeType',
  49. receiver: '=eeeReceiver',
  50. message: '=eeeMessage',
  51. resolution: '=?eeeResolution',
  52. },
  53. controllerAs: 'ctrl',
  54. controller: [function() {
  55. this.logTag = '[MessageDirective]';
  56. // Determine browser
  57. this.browserInfo = browserService.getBrowser();
  58. this.$onInit = function() {
  59. // Defaults and variables
  60. if (this.resolution == null) {
  61. this.resolution = 'low';
  62. }
  63. // Find contact
  64. this.contact = webClientService.contacts.get(
  65. getSenderIdentity(this.message, webClientService.me.id),
  66. );
  67. // Show...
  68. this.isStatusMessage = this.message.isStatus;
  69. this.isContactMessage = !this.message.isStatus
  70. && webClientService.contacts.has(getSenderIdentity(this.message, webClientService.me.id));
  71. this.isGroup = this.type as threema.ReceiverType === 'group';
  72. this.isContact = this.type as threema.ReceiverType === 'contact';
  73. this.isBusinessReceiver = receiverService.isBusinessContact(this.receiver);
  74. this.showName = !this.message.isOutbox && this.isGroup;
  75. // show avatar only if a name is shown
  76. this.showAvatar = this.showName;
  77. this.showText = this.message.type === 'text' || this.message.caption;
  78. this.showMedia = this.message.type !== 'text';
  79. this.showState = messageService.showStatusIcon(this.message as threema.Message, this.receiver);
  80. this.showQuote = this.message.quote !== undefined;
  81. this.showVoipInfo = this.message.type === 'voipStatus';
  82. this.access = messageService.getAccess(this.message, this.receiver);
  83. this.ack = (ack: boolean) => {
  84. webClientService.ackMessage(this.receiver, this.message, ack);
  85. };
  86. this.quote = () => {
  87. // set message as quoted
  88. webClientService.setQuote(this.receiver, this.message);
  89. };
  90. this.delete = (ev) => {
  91. const confirm = $mdDialog.confirm()
  92. .title($translate.instant('messenger.CONFIRM_DELETE_TITLE'))
  93. .textContent($translate.instant('common.ARE_YOU_SURE'))
  94. .targetEvent(ev)
  95. .ok($translate.instant('common.YES'))
  96. .cancel($translate.instant('common.CANCEL'));
  97. $mdDialog.show(confirm).then((result) => {
  98. webClientService.deleteMessage(this.receiver, this.message);
  99. }, () => { /* do nothing */});
  100. };
  101. this.copyToClipboard = (ev: MouseEvent) => {
  102. // Get copyable text
  103. const text = messageService.getQuoteText(this.message);
  104. if (text === null) {
  105. return;
  106. }
  107. // In order to copy the text to the clipboard,
  108. // put it into a temporary textarea element.
  109. const textArea = document.createElement('textarea');
  110. textArea.value = text;
  111. document.body.appendChild(textArea);
  112. if ((this.browserInfo as BrowserInfo).isSafari()) {
  113. // Safari: Create a selection range.
  114. // Inspiration: https://stackoverflow.com/a/34046084/284318
  115. textArea.contentEditable = 'true';
  116. textArea.readOnly = false;
  117. const range = document.createRange();
  118. const selection = self.getSelection();
  119. selection.removeAllRanges();
  120. selection.addRange(range);
  121. textArea.setSelectionRange(0, 999999);
  122. } else {
  123. textArea.focus();
  124. textArea.select();
  125. }
  126. // Copy selection to clipboard
  127. let toastString = 'messenger.COPY_ERROR';
  128. try {
  129. const successful = document.execCommand('copy');
  130. if (!successful) {
  131. $log.warn(this.logTag, 'Could not copy text to clipboard');
  132. } else {
  133. toastString = 'messenger.COPIED';
  134. }
  135. } catch (err) {
  136. $log.warn(this.logTag, 'Could not copy text to clipboard:', err);
  137. }
  138. document.body.removeChild(textArea);
  139. // Show toast
  140. const toast = $mdToast.simple()
  141. .textContent($translate.instant(toastString))
  142. .position('bottom center');
  143. $mdToast.show(toast);
  144. };
  145. this.download = (ev) => {
  146. this.downloading = true;
  147. webClientService.requestBlob(this.message.id, this.receiver)
  148. .then((blobInfo: threema.BlobInfo) => {
  149. $rootScope.$apply(() => {
  150. this.downloading = false;
  151. switch (this.message.type) {
  152. case 'image':
  153. case 'video':
  154. case 'file':
  155. case 'audio':
  156. saveAs(new Blob([blobInfo.buffer]), blobInfo.filename);
  157. break;
  158. default:
  159. $log.warn(this.logTag, 'Ignored download request for message type', this.message.type);
  160. }
  161. });
  162. })
  163. .catch((error) => {
  164. // TODO: Handle this properly / show an error message
  165. $log.error(this.logTag, `Error downloading blob: ${error}`);
  166. this.downloading = false;
  167. });
  168. };
  169. this.isDownloading = () => {
  170. return this.downloading;
  171. };
  172. this.showHistory = (ev) => {
  173. const getEvents = () => this.message.events;
  174. $mdDialog.show({
  175. controllerAs: 'ctrl',
  176. controller: function() {
  177. this.getEvents = getEvents;
  178. this.close = () => {
  179. $mdDialog.hide();
  180. };
  181. },
  182. template: `
  183. <md-dialog class="message-history-dialog" translate-attr="{'aria-label': 'messenger.MSG_HISTORY'}">
  184. <form ng-cloak>
  185. <md-toolbar>
  186. <div class="md-toolbar-tools">
  187. <h2 translate>messenger.MSG_HISTORY</h2>
  188. </div>
  189. </md-toolbar>
  190. <md-dialog-content>
  191. <p ng-repeat="event in ctrl.getEvents()">
  192. <span class="event-type" ng-if="event.type === 'created'" translate>messenger.MSG_HISTORY_CREATED</span>
  193. <span class="event-type" ng-if="event.type === 'sent'" translate>messenger.MSG_HISTORY_SENT</span>
  194. <span class="event-type" ng-if="event.type === 'delivered'" translate>messenger.MSG_HISTORY_DELIVERED</span>
  195. <span class="event-type" ng-if="event.type === 'read'" translate>messenger.MSG_HISTORY_READ</span>
  196. <span class="event-type" ng-if="event.type === 'acked'" translate>messenger.MSG_HISTORY_ACKED</span>
  197. <span class="event-type" ng-if="event.type === 'modified'" translate>messenger.MSG_HISTORY_MODIFIED</span>
  198. {{ event.date | unixToTimestring:true }}
  199. </p>
  200. </md-dialog-content>
  201. <md-dialog-actions layout="row" >
  202. <md-button ng-click="ctrl.close()">
  203. <span translate>common.OK</span>
  204. </md-button>
  205. </md-dialog-actions>
  206. </form>
  207. </md-dialog>
  208. `,
  209. parent: angular.element(document.body),
  210. targetEvent: ev,
  211. clickOutsideToClose: true,
  212. });
  213. };
  214. };
  215. }],
  216. link: function(scope: any, element: ng.IAugmentedJQuery, attrs) {
  217. // Prevent status messages from being draggable
  218. const domElement: HTMLElement = element[0];
  219. if (scope.ctrl.isStatusMessage) {
  220. domElement.ondragstart = () => false;
  221. }
  222. },
  223. templateUrl: 'directives/message.html',
  224. };
  225. },
  226. ];