message_media.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  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. this.type = this.message.type;
  101. // Downloading
  102. this.downloading = false;
  103. this.thumbnailDownloading = false;
  104. this.downloaded = false;
  105. // Uploading
  106. this.uploading = this.message.temporaryId !== undefined
  107. && this.message.temporaryId !== null;
  108. // AnimGIF detection
  109. this.isAnimGif = !this.uploading
  110. && (this.message as threema.Message).type === 'file'
  111. && (this.message as threema.Message).file.type === 'image/gif';
  112. // Preview thumbnail
  113. let thumbnailPreviewUri = null;
  114. this.getThumbnailPreviewUri = () => {
  115. // Cache thumbnail preview URI
  116. if (thumbnailPreviewUri === null
  117. && hasValue(this.message) && hasValue(this.message.thumbnail)) {
  118. thumbnailPreviewUri = bufferToUrl(
  119. (this.message as threema.Message).thumbnail.preview,
  120. webClientService.appCapabilities.imageFormat.thumbnail,
  121. log,
  122. );
  123. }
  124. return thumbnailPreviewUri;
  125. };
  126. // Thumbnail loading
  127. //
  128. // Do not show thumbnail in file messages (except anim gif).
  129. // If a thumbnail in file messages are available, the thumbnail
  130. // will be shown in the file circle
  131. this.showThumbnail = this.message.thumbnail !== undefined
  132. && ((this.message as threema.Message).type !== 'file' || this.isAnimGif);
  133. this.thumbnail = null;
  134. this.thumbnailFormat = webClientService.appCapabilities.imageFormat.thumbnail;
  135. if (this.message.thumbnail !== undefined) {
  136. this.thumbnailStyle = {
  137. width: this.message.thumbnail.width + 'px',
  138. height: this.message.thumbnail.height + 'px',
  139. };
  140. }
  141. let loadingThumbnailTimeout: ng.IPromise<void> = null;
  142. this.wasInView = false;
  143. this.thumbnailInView = (inView: boolean) => {
  144. if (this.message.thumbnail === undefined
  145. || this.wasInView === inView) {
  146. // do nothing
  147. return;
  148. }
  149. this.wasInView = inView;
  150. if (!inView) {
  151. if (loadingThumbnailTimeout !== null) {
  152. timeoutService.cancel(loadingThumbnailTimeout);
  153. }
  154. this.thumbnailDownloading = false;
  155. this.thumbnail = null;
  156. } else {
  157. if (this.thumbnail === null) {
  158. const setThumbnail = (buf: ArrayBuffer) => {
  159. this.thumbnail = bufferToUrl(
  160. buf,
  161. webClientService.appCapabilities.imageFormat.thumbnail,
  162. log,
  163. );
  164. };
  165. if (this.message.thumbnail.img !== undefined) {
  166. setThumbnail(this.message.thumbnail.img);
  167. return;
  168. } else {
  169. this.thumbnailDownloading = true;
  170. loadingThumbnailTimeout = timeoutService.register(() => {
  171. webClientService
  172. .requestThumbnail(this.receiver, this.message)
  173. .then((img) => $timeout(() => {
  174. setThumbnail(img);
  175. this.thumbnailDownloading = false;
  176. }))
  177. .catch((error) => {
  178. // TODO: Handle this properly / show an error message
  179. const message = `Thumbnail request has been rejected: ${error}`;
  180. this.log.error(message);
  181. });
  182. }, 1000, false, 'thumbnail');
  183. }
  184. }
  185. }
  186. };
  187. // For locations, retrieve the coordinates
  188. this.location = null;
  189. if (this.message.location !== undefined) {
  190. this.location = this.message.location;
  191. this.downloaded = true;
  192. }
  193. // Open map link in new window using mapLink-filter
  194. this.openMapLink = () => {
  195. $window.open($filter<any>('mapLink')(this.location), '_blank');
  196. };
  197. // Play a Audio file in a dialog
  198. this.playAudio = (blobInfo: threema.BlobInfo) => showAudioDialog($mdDialog, logService, blobInfo);
  199. // Download function
  200. this.download = () => {
  201. log.debug('Download blob');
  202. if (this.downloading) {
  203. log.debug('Download already in progress...');
  204. return;
  205. }
  206. const message: threema.Message = this.message;
  207. const receiver: threema.Receiver = this.receiver;
  208. this.downloading = true;
  209. webClientService.requestBlob(message.id, receiver)
  210. .then((blobInfo: threema.BlobInfo) => {
  211. $rootScope.$apply(() => {
  212. log.debug('Blob loaded');
  213. this.downloading = false;
  214. this.downloaded = true;
  215. switch (this.message.type) {
  216. case 'image':
  217. const caption = message.caption || '';
  218. mediaboxService.setMedia(
  219. blobInfo.buffer,
  220. blobInfo.filename,
  221. blobInfo.mimetype,
  222. caption,
  223. );
  224. break;
  225. case 'video':
  226. saveAs(new Blob([blobInfo.buffer]), blobInfo.filename);
  227. break;
  228. case 'file':
  229. if (this.message.file.type === 'image/gif') {
  230. // Show inline
  231. this.blobBufferUrl = bufferToUrl(
  232. blobInfo.buffer, 'image/gif', log);
  233. // Hide thumbnail
  234. this.showThumbnail = false;
  235. } else {
  236. saveAs(new Blob([blobInfo.buffer]), blobInfo.filename);
  237. }
  238. break;
  239. case 'audio':
  240. // Show inline
  241. this.playAudio(blobInfo);
  242. break;
  243. default:
  244. log.warn('Ignored download request for message type', this.message.type);
  245. }
  246. });
  247. })
  248. .catch((error) => {
  249. $rootScope.$apply(() => {
  250. this.downloading = false;
  251. let contentString;
  252. switch (error) {
  253. case 'blobDownloadFailed':
  254. contentString = 'error.BLOB_DOWNLOAD_FAILED';
  255. break;
  256. case 'blobDecryptFailed':
  257. contentString = 'error.BLOB_DECRYPT_FAILED';
  258. break;
  259. default:
  260. contentString = 'error.ERROR_OCCURRED';
  261. break;
  262. }
  263. const confirm = $mdDialog.alert()
  264. .title($translate.instant('common.ERROR'))
  265. .textContent($translate.instant(contentString))
  266. .ok($translate.instant('common.OK'));
  267. $mdDialog.show(confirm);
  268. });
  269. });
  270. };
  271. this.isDownloading = () => {
  272. return this.downloading
  273. || this.thumbnailDownloading
  274. || (this.showDownloading && this.showDownloading());
  275. };
  276. };
  277. }],
  278. templateUrl: 'directives/message_media.html',
  279. };
  280. },
  281. ];