message_media.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  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. import {Transition as UiTransition, TransitionService as UiTransitionService} from '@uirouter/angularjs';
  18. import {saveAs} from 'file-saver';
  19. import {bufferToUrl, hasValue} from '../helpers';
  20. import {LogService} from '../services/log';
  21. import {MediaboxService} from '../services/mediabox';
  22. import {MessageService} from '../services/message';
  23. import {TimeoutService} from '../services/timeout';
  24. import {WebClientService} from '../services/webclient';
  25. function showAudioDialog(
  26. $mdDialog: ng.material.IDialogService,
  27. logService: LogService,
  28. blobInfo: threema.BlobInfo,
  29. ): void {
  30. const log = logService.getLogger('AudioPlayerDialog-C');
  31. $mdDialog.show({
  32. controllerAs: 'ctrl',
  33. controller: function() {
  34. this.cancel = () => $mdDialog.cancel();
  35. this.audioSrc = bufferToUrl(blobInfo.buffer, blobInfo.mimetype, log);
  36. },
  37. template: `
  38. <md-dialog translate-attr="{'aria-label': 'messageTypes.AUDIO_MESSAGE'}">
  39. <md-toolbar>
  40. <div class="md-toolbar-tools">
  41. <h2 translate>messageTypes.AUDIO_MESSAGE</h2>
  42. </div>
  43. </md-toolbar>
  44. <md-dialog-content layout="row" layout-align="center">
  45. <audio controls autoplay ng-src="{{ ctrl.audioSrc | unsafeResUrl }}">
  46. Your browser does not support the <code>audio</code> element.
  47. </audio>
  48. </md-dialog-content>
  49. <md-dialog-actions layout="row" >
  50. <md-button ng-click="ctrl.cancel()">
  51. <span translate>common.OK</span>
  52. </md-button>
  53. </md-dialog-actions>
  54. </md-dialog>`,
  55. parent: angular.element(document.body),
  56. clickOutsideToClose: true,
  57. });
  58. }
  59. export default [
  60. 'LogService',
  61. 'WebClientService',
  62. 'MediaboxService',
  63. 'MessageService',
  64. 'TimeoutService',
  65. '$rootScope',
  66. '$mdDialog',
  67. '$timeout',
  68. '$transitions',
  69. '$translate',
  70. '$filter',
  71. '$window',
  72. function(logService: LogService,
  73. webClientService: WebClientService,
  74. mediaboxService: MediaboxService,
  75. messageService: MessageService,
  76. timeoutService: TimeoutService,
  77. $rootScope: ng.IRootScopeService,
  78. $mdDialog: ng.material.IDialogService,
  79. $timeout: ng.ITimeoutService,
  80. $transitions: UiTransitionService,
  81. $translate: ng.translate.ITranslateService,
  82. $filter: ng.IFilterService,
  83. $window: ng.IWindowService) {
  84. const log = logService.getLogger('MessageMedia-C');
  85. return {
  86. restrict: 'EA',
  87. scope: {},
  88. bindToController: {
  89. message: '=eeeMessage',
  90. receiver: '=eeeReceiver',
  91. showDownloading: '=eeeShowDownloading',
  92. },
  93. controllerAs: 'ctrl',
  94. controller: [function() {
  95. // On state transitions, clear mediabox
  96. $transitions.onStart({}, function(trans: UiTransition) {
  97. mediaboxService.clearMedia();
  98. });
  99. this.$onInit = function() {
  100. const message = this.message as threema.Message;
  101. this.type = message.type;
  102. // Downloading
  103. this.downloading = false;
  104. this.thumbnailDownloading = false;
  105. this.downloaded = false;
  106. // Uploading
  107. this.uploading = message.temporaryId !== undefined
  108. && message.temporaryId !== null;
  109. // AnimGIF detection
  110. this.isGif = message.type === 'file' && message.file.type === 'image/gif';
  111. // Has a preview thumbnail
  112. this.hasPreviewThumbnail = (): boolean => {
  113. return hasValue(message.thumbnail) && (
  114. hasValue(message.thumbnail.previewDataUrl) || hasValue(message.thumbnail.preview));
  115. };
  116. // Preview thumbnail
  117. this.getThumbnailPreviewUri = (): string | null => {
  118. // Cache thumbnail preview URI
  119. if (hasValue(message.thumbnail.previewDataUrl)) {
  120. return message.thumbnail.previewDataUrl;
  121. }
  122. if (hasValue(message.thumbnail.preview)) {
  123. message.thumbnail.previewDataUrl = bufferToUrl(
  124. message.thumbnail.preview,
  125. webClientService.appCapabilities.imageFormat.thumbnail,
  126. log,
  127. );
  128. return message.thumbnail.previewDataUrl;
  129. }
  130. return null;
  131. };
  132. // TODO: Uuuuugly!
  133. this.getThumbnailPreviewUriStyle = (): string => {
  134. const previewUri = hasValue(message.thumbnail) ? this.getThumbnailPreviewUri() : null;
  135. return previewUri !== null ? `url(${previewUri})` : 'none';
  136. };
  137. // Only show thumbnails for images, videos and GIFs
  138. // If a preview image is not available, we fall back to
  139. // icons depending on the type.
  140. this.thumbnail = null;
  141. if (message.thumbnail !== undefined) {
  142. this.thumbnailStyle = {
  143. width: this.message.thumbnail.width + 'px',
  144. height: this.message.thumbnail.height + 'px',
  145. };
  146. }
  147. let loadingThumbnailTimeout: ng.IPromise<void> = null;
  148. this.wasInView = false;
  149. this.thumbnailInView = (inView: boolean) => {
  150. if (this.uploading || message.thumbnail === undefined || this.wasInView === inView) {
  151. // do nothing
  152. return;
  153. }
  154. this.wasInView = inView;
  155. if (!inView) {
  156. if (loadingThumbnailTimeout !== null) {
  157. timeoutService.cancel(loadingThumbnailTimeout);
  158. }
  159. this.thumbnailDownloading = false;
  160. this.thumbnail = null;
  161. } else {
  162. if (this.thumbnail === null) {
  163. const setThumbnail = (buf: ArrayBuffer) => {
  164. this.thumbnail = bufferToUrl(
  165. buf,
  166. webClientService.appCapabilities.imageFormat.thumbnail,
  167. log,
  168. );
  169. };
  170. if (message.thumbnail.img !== undefined) {
  171. setThumbnail(message.thumbnail.img);
  172. return;
  173. } else {
  174. this.thumbnailDownloading = true;
  175. loadingThumbnailTimeout = timeoutService.register(() => {
  176. webClientService
  177. .requestThumbnail(this.receiver, message)
  178. .then((img) => $timeout(() => {
  179. setThumbnail(img);
  180. this.thumbnailDownloading = false;
  181. }))
  182. .catch((error) => {
  183. // TODO: Handle this properly / show an error message
  184. const description = `Thumbnail request has been rejected: ${error}`;
  185. this.log.error(description);
  186. });
  187. }, 1000, false, 'thumbnail');
  188. }
  189. }
  190. }
  191. };
  192. // For locations, retrieve the coordinates
  193. this.location = null;
  194. if (message.location !== undefined) {
  195. this.location = message.location;
  196. this.downloaded = true;
  197. }
  198. // Open map link in new window using mapLink-filter
  199. this.openMapLink = () => {
  200. $window.open($filter<any>('mapLink')(this.location), '_blank');
  201. };
  202. // Play a Audio file in a dialog
  203. this.playAudio = (blobInfo: threema.BlobInfo) => showAudioDialog($mdDialog, logService, blobInfo);
  204. // Download function
  205. this.download = () => {
  206. log.debug('Download blob');
  207. if (this.uploading) {
  208. log.debug('Cannot download, still uploading');
  209. return;
  210. }
  211. if (this.downloading) {
  212. log.debug('Download already in progress...');
  213. return;
  214. }
  215. const receiver: threema.Receiver = this.receiver;
  216. this.downloading = true;
  217. webClientService.requestBlob(message.id, receiver)
  218. .then((blobInfo: threema.BlobInfo) => {
  219. $rootScope.$apply(() => {
  220. log.debug('Blob loaded');
  221. this.downloading = false;
  222. this.downloaded = true;
  223. switch (message.type) {
  224. case 'image':
  225. const caption = message.caption || '';
  226. mediaboxService.setMedia(
  227. blobInfo.buffer,
  228. blobInfo.filename,
  229. blobInfo.mimetype,
  230. caption,
  231. );
  232. break;
  233. case 'video':
  234. saveAs(new Blob([blobInfo.buffer]), blobInfo.filename);
  235. break;
  236. case 'file':
  237. if (message.file.type === 'image/gif') {
  238. // Show inline
  239. this.blobBufferUrl = bufferToUrl(
  240. blobInfo.buffer, 'image/gif', log);
  241. // Hide thumbnail
  242. this.showThumbnail = false;
  243. } else {
  244. saveAs(new Blob([blobInfo.buffer]), blobInfo.filename);
  245. }
  246. break;
  247. case 'audio':
  248. // Show inline
  249. this.playAudio(blobInfo);
  250. break;
  251. default:
  252. log.warn('Ignored download request for message type', message.type);
  253. }
  254. });
  255. })
  256. .catch((error) => {
  257. $rootScope.$apply(() => {
  258. this.downloading = false;
  259. let contentString;
  260. switch (error) {
  261. case 'blobDownloadFailed':
  262. contentString = 'error.BLOB_DOWNLOAD_FAILED';
  263. break;
  264. case 'blobDecryptFailed':
  265. contentString = 'error.BLOB_DECRYPT_FAILED';
  266. break;
  267. default:
  268. contentString = 'error.ERROR_OCCURRED';
  269. break;
  270. }
  271. const confirm = $mdDialog.alert()
  272. .title($translate.instant('common.ERROR'))
  273. .textContent($translate.instant(contentString))
  274. .ok($translate.instant('common.OK'));
  275. $mdDialog.show(confirm);
  276. });
  277. });
  278. };
  279. this.isLoading = () => {
  280. return this.uploading || this.isDownloading();
  281. };
  282. this.isDownloading = () => {
  283. return this.downloading
  284. || this.thumbnailDownloading
  285. || (this.showDownloading && this.showDownloading());
  286. };
  287. };
  288. }],
  289. templateUrl: 'directives/message_media.html',
  290. };
  291. },
  292. ];