message_media.ts 13 KB

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