message_media.ts 15 KB

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