avatar.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  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, hasValue} from '../helpers';
  18. import {isEchoContact, isGatewayContact} from '../receiver_helpers';
  19. import {LogService} from '../services/log';
  20. import {TimeoutService} from '../services/timeout';
  21. import {WebClientService} from '../services/webclient';
  22. import {isContactReceiver} from '../typeguards';
  23. export default [
  24. '$rootScope',
  25. 'LogService',
  26. 'TimeoutService',
  27. 'WebClientService',
  28. function($rootScope: ng.IRootScopeService,
  29. logService: LogService,
  30. timeoutService: TimeoutService,
  31. webClientService: WebClientService) {
  32. const log = logService.getLogger('Avatar-C');
  33. return {
  34. restrict: 'E',
  35. scope: {},
  36. bindToController: {
  37. receiver: '=eeeReceiver',
  38. resolution: '=eeeResolution',
  39. },
  40. link: function(scope, elem, attrs) {
  41. scope.$watch(
  42. () => scope.ctrl.receiver,
  43. (newReceiver: threema.Receiver, oldReceiver: threema.Receiver) => {
  44. // Register for receiver changes. When something relevant changes, call the update function.
  45. // This prevents processing the avatar more often than necessary.
  46. if (!hasValue(newReceiver)) {
  47. // New receiver has no value
  48. return;
  49. }
  50. if (!hasValue(oldReceiver)) {
  51. // New receiver has value, old receiver doesn't
  52. scope.ctrl.update(false);
  53. return;
  54. }
  55. // Check for changes in relevant attributes
  56. if (newReceiver.id !== oldReceiver.id ||
  57. newReceiver.type !== oldReceiver.type ||
  58. newReceiver.color !== oldReceiver.color ||
  59. newReceiver.displayName !== oldReceiver.displayName) {
  60. scope.ctrl.update(false);
  61. return;
  62. }
  63. // Check for changes in the avatar itself
  64. if (hasValue(newReceiver.avatar)) {
  65. if (hasValue(oldReceiver.avatar)) {
  66. if (newReceiver.avatar.high !== oldReceiver.avatar.high ||
  67. newReceiver.avatar.low !== oldReceiver.avatar.low) {
  68. scope.ctrl.update(false);
  69. return;
  70. }
  71. } else {
  72. scope.ctrl.update(false);
  73. return;
  74. }
  75. }
  76. },
  77. );
  78. },
  79. controllerAs: 'ctrl',
  80. controller: [function() {
  81. let loadingPromise: ng.IPromise<any> = null;
  82. /**
  83. * Convert avatar bytes to an URI.
  84. */
  85. const avatarUri = {
  86. high: null,
  87. low: null,
  88. };
  89. this.avatarToUri = (data: ArrayBuffer, res: 'high' | 'low') => {
  90. if (!hasValue(data)) {
  91. return '';
  92. }
  93. if (avatarUri[res] === null) {
  94. // Cache avatar image URI
  95. avatarUri[res] = bufferToUrl(
  96. data, webClientService.appCapabilities.imageFormat.avatar, log);
  97. }
  98. return avatarUri[res];
  99. };
  100. /**
  101. * Update data when the receiver changes.
  102. */
  103. this.update = (initial: boolean) => {
  104. // Reset avatar cache
  105. avatarUri.high = null;
  106. avatarUri.low = null;
  107. // Get receiver
  108. const receiver: threema.Receiver = this.receiver;
  109. // Set initial values
  110. this.highResolution = this.resolution === 'high';
  111. this.isLoading = this.highResolution;
  112. this.receiverName = receiver.displayName;
  113. };
  114. this.$onInit = function() {
  115. this.update(true);
  116. /**
  117. * Return the CSS class for the avatar.
  118. */
  119. this.avatarClass = () => 'avatar-' + this.resolution + (this.isLoading ? ' is-loading' : '');
  120. /**
  121. * Return whether or not an avatar is available.
  122. */
  123. this.avatarExists = () => {
  124. if (this.receiver.avatar === undefined
  125. || this.receiver.avatar[this.resolution] === undefined
  126. || this.receiver.avatar[this.resolution] === null) {
  127. return false;
  128. }
  129. this.isLoading = false;
  130. return true;
  131. };
  132. /**
  133. * Return path to the default avatar.
  134. */
  135. this.getDefaultAvatarUri = (type: threema.ReceiverType, highResolution: boolean) => {
  136. switch (type) {
  137. case 'group':
  138. return highResolution
  139. ? 'img/ic_group_picture_big.png'
  140. : 'img/ic_group_t.png';
  141. case 'distributionList':
  142. return highResolution
  143. ? 'img/ic_distribution_list_t.png'
  144. : 'img/ic_distribution_list_t.png';
  145. case 'contact':
  146. case 'me':
  147. default:
  148. return highResolution
  149. ? 'img/ic_contact_picture_big.png'
  150. : 'img/ic_contact_picture_t.png';
  151. }
  152. };
  153. /**
  154. * Return an avatar URI.
  155. *
  156. * This will fall back to a low resolution version or to the
  157. * default avatar if no avatar for the desired resolution could
  158. * be found.
  159. */
  160. this.getAvatarUri = () => {
  161. /// If an avatar for the chosen resolution exists, convert it to an URI and return
  162. if (this.avatarExists()) {
  163. return this.avatarToUri(this.receiver.avatar[this.resolution], this.resolution);
  164. }
  165. // Otherwise, if we requested a high res avatar but
  166. // there is only a low-res version, show that.
  167. if (this.highResolution
  168. && this.receiver.avatar !== undefined
  169. && this.receiver.avatar.low !== undefined
  170. && this.receiver.avatar.low !== null) {
  171. return this.avatarToUri(this.receiver.avatar.low, 'low');
  172. }
  173. // As a fallback, get the default avatar.
  174. return this.getDefaultAvatarUri(this.receiver.type, this.highResolution);
  175. };
  176. this.requestAvatar = (inView: boolean) => {
  177. if (this.avatarExists()) {
  178. // do not request
  179. return;
  180. }
  181. if (inView) {
  182. if (loadingPromise === null) {
  183. // Do not wait on high resolution avatar
  184. const loadingTimeout = this.highResolution ? 0 : 500;
  185. loadingPromise = timeoutService.register(() => {
  186. // show loading only on high res images!
  187. webClientService.requestAvatar({
  188. type: this.receiver.type,
  189. id: this.receiver.id,
  190. } as threema.Receiver, this.highResolution)
  191. .then((avatar) => {
  192. $rootScope.$apply(() => {
  193. this.isLoading = false;
  194. });
  195. loadingPromise = null;
  196. })
  197. .catch((error) => {
  198. // TODO: Handle this properly / show an error message
  199. log.error(`Avatar request has been rejected: ${error}`);
  200. $rootScope.$apply(() => {
  201. this.isLoading = false;
  202. });
  203. loadingPromise = null;
  204. });
  205. }, loadingTimeout, false, 'avatar');
  206. }
  207. } else if (loadingPromise !== null) {
  208. // Cancel pending avatar loading
  209. timeoutService.cancel(loadingPromise);
  210. loadingPromise = null;
  211. }
  212. };
  213. const isWorkApp = webClientService.clientInfo.isWork;
  214. this.showWorkIndicator = () => {
  215. if (!isContactReceiver(this.receiver)) { return false; }
  216. const contact: threema.ContactReceiver = this.receiver;
  217. return isWorkApp === false
  218. && !this.highResolution
  219. && contact.identityType === threema.IdentityType.Work;
  220. };
  221. this.showHomeIndicator = () => {
  222. if (!isContactReceiver(this.receiver)) { return false; }
  223. const contact: threema.ContactReceiver = this.receiver;
  224. return isWorkApp === true
  225. && !isGatewayContact(contact)
  226. && !isEchoContact(contact)
  227. && contact.identityType === threema.IdentityType.Regular
  228. && !this.highResolution;
  229. };
  230. this.showBlocked = () => {
  231. if (!isContactReceiver(this.receiver)) { return false; }
  232. const contact: threema.ContactReceiver = this.receiver;
  233. return !this.highResolution && contact.isBlocked;
  234. };
  235. };
  236. }],
  237. template: `
  238. <div class="avatar" ng-class="ctrl.avatarClass()">
  239. <div class="avatar-loading" ng-if="ctrl.isLoading">
  240. <span></span>
  241. </div>
  242. <div class="work-indicator" ng-if="ctrl.showWorkIndicator()"
  243. translate-attr="{'aria-label': 'messenger.THREEMA_WORK_CONTACT',
  244. 'title': 'messenger.THREEMA_WORK_CONTACT'}">
  245. <img src="img/ic_work_round.svg" alt="Threema Work user">
  246. </div>
  247. <div class="home-indicator" ng-if="ctrl.showHomeIndicator()"
  248. translate-attr="{'aria-label': 'messenger.THREEMA_HOME_CONTACT',
  249. 'title': 'messenger.THREEMA_HOME_CONTACT'}">
  250. <img src="img/ic_home_round.svg" alt="Private Threema contact">
  251. </div>
  252. <div class="blocked-indicator" ng-if="ctrl.showBlocked()"
  253. translate-attr="{'aria-label': 'messenger.THREEMA_BLOCKED_RECEIVER',
  254. 'title': 'messenger.THREEMA_BLOCKED_RECEIVER'}">
  255. <img src="img/ic_blocked_24px.svg" alt="blocked icon">
  256. </div>
  257. <img
  258. ng-class="ctrl.avatarClass()"
  259. ng-src="{{ ctrl.getAvatarUri() }}"
  260. in-view="ctrl.requestAvatar($inview)"
  261. aria-label="avatar {{ ctrl.receiverName }}">
  262. </div>
  263. `,
  264. };
  265. },
  266. ];