notification.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  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 {SettingsService} from './settings';
  18. export class NotificationService {
  19. private static SETTINGS_NOTIFICATIONS = 'notifications';
  20. private static SETTINGS_NOTIFICATION_PREVIEW = 'notificationPreview';
  21. private static SETTINGS_NOTIFICATION_SOUND = 'notificationSound';
  22. private static NOTIFICATION_SOUND = 'sounds/notification.mp3';
  23. private $log: ng.ILogService;
  24. private $window: ng.IWindowService;
  25. private $state: ng.ui.IStateService;
  26. private settingsService: SettingsService;
  27. private logTag = '[NotificationService]';
  28. // Whether user has granted notification permission
  29. private notificationPermission: boolean = null;
  30. // Whether user wants to receive desktop notifications
  31. private desktopNotifications: boolean = null;
  32. // Whether the browser supports notifications
  33. private notificationAPIAvailable: boolean = null;
  34. // Whether the user wants notification preview
  35. private notificationPreview: boolean = null;
  36. // Whether the user wants notification sound
  37. private notificationSound: boolean = null;
  38. // Cache notifications
  39. private notificationCache: any = {};
  40. public static $inject = ['$log', '$window', '$state', 'SettingsService'];
  41. constructor($log: ng.ILogService, $window: ng.IWindowService,
  42. $state: ng.ui.IStateService, settingsService: SettingsService) {
  43. this.$log = $log;
  44. this.$window = $window;
  45. this.$state = $state;
  46. this.settingsService = settingsService;
  47. }
  48. public init(): void {
  49. this.checkNotificationAPI();
  50. this.fetchSettings();
  51. }
  52. /**
  53. * Ask user for desktop notification permissions.
  54. *
  55. * Possible values for 'notificationPermission'
  56. * - denied: User has denied the notification permission
  57. * - granted: User has granted the notification permission
  58. * - default: User has visits Threema Web the first time.
  59. * It stays default unless he denies/grants us the
  60. * notification permission or if the result is sth. else
  61. * If the user grants the permission, the 'desktopNotification' flag
  62. * becomes true and is stored in the local storage.
  63. */
  64. private requestNotificationPermission(): void {
  65. if (this.notificationAPIAvailable) {
  66. const Notification = this.$window.Notification;
  67. this.$log.debug(this.logTag, 'Requesting notification permission...');
  68. Notification.requestPermission((result) => {
  69. switch (result) {
  70. case 'denied':
  71. this.notificationPermission = false;
  72. break;
  73. case 'granted':
  74. this.notificationPermission = true;
  75. this.desktopNotifications = true;
  76. this.storeSetting(NotificationService.SETTINGS_NOTIFICATIONS, 'true');
  77. break;
  78. case 'default':
  79. this.desktopNotifications = false;
  80. this.notificationPermission = null;
  81. break;
  82. default:
  83. this.notificationPermission = false;
  84. break;
  85. }
  86. this.$log.debug(this.logTag, 'Notification permission', this.notificationPermission);
  87. });
  88. }
  89. }
  90. /**
  91. * Check the notification api availability and permission state
  92. *
  93. * If the api is available, 'notificationAPIAvailable' becomes true and
  94. * the permission state is checked
  95. */
  96. private checkNotificationAPI(): void {
  97. this.notificationAPIAvailable = ('Notification' in this.$window);
  98. this.$log.debug(this.logTag, 'Notification API available:', this.notificationAPIAvailable);
  99. if (this.notificationAPIAvailable) {
  100. const Notification = this.$window.Notification;
  101. switch (Notification.permission) {
  102. // denied means the user must manually re-grant permission via browser settings
  103. case 'denied':
  104. this.notificationPermission = false;
  105. break;
  106. case 'granted':
  107. this.notificationPermission = true;
  108. break;
  109. case 'default':
  110. this.notificationPermission = null;
  111. break;
  112. default:
  113. this.notificationPermission = false;
  114. break;
  115. }
  116. }
  117. this.$log.debug(this.logTag, 'Initial notificationPermission', this.notificationPermission);
  118. }
  119. /**
  120. * Get the initial settings from local storage
  121. */
  122. private fetchSettings(): void {
  123. this.$log.debug(this.logTag, 'Fetching settings...');
  124. let notifications = this.retrieveSetting(NotificationService.SETTINGS_NOTIFICATIONS);
  125. let preview = this.retrieveSetting(NotificationService.SETTINGS_NOTIFICATION_PREVIEW);
  126. let sound = this.retrieveSetting(NotificationService.SETTINGS_NOTIFICATION_SOUND);
  127. if (notifications === 'true') {
  128. this.$log.debug(this.logTag, 'Desktop notifications:', notifications);
  129. this.desktopNotifications = true;
  130. // check permission because user may have revoked them
  131. this.requestNotificationPermission();
  132. } else if (notifications === 'false') {
  133. this.$log.debug(this.logTag, 'Desktop notifications:', notifications);
  134. // user does not want notifications
  135. this.desktopNotifications = false;
  136. } else {
  137. this.$log.debug(this.logTag, 'Desktop notifications:', notifications, 'Asking user...');
  138. // Neither true nor false was in local storage, so we have to ask the user if he wants notifications
  139. // If he grants (or already has granted) us the permission, we will set the flag true (default setting)
  140. this.requestNotificationPermission();
  141. }
  142. if (preview === 'false') {
  143. this.$log.debug(this.logTag, 'Notification preview:', preview);
  144. this.notificationPreview = false;
  145. } else {
  146. // set the flag true if true/nothing or sth. else is in local storage (default setting)
  147. this.$log.debug(this.logTag, 'Notification preview:', preview, 'Using default value (true)');
  148. this.notificationPreview = true;
  149. this.storeSetting(NotificationService.SETTINGS_NOTIFICATION_PREVIEW, 'true');
  150. }
  151. if (sound === 'true') {
  152. this.$log.debug(this.logTag, 'Notification sound:', sound);
  153. this.notificationSound = true;
  154. } else {
  155. // set the flag false if false/nothing or sth. else is in local storage (default setting)
  156. this.$log.debug(this.logTag, 'Notification sound:', sound, 'Using default value (false)');
  157. this.notificationSound = false;
  158. this.storeSetting(NotificationService.SETTINGS_NOTIFICATION_SOUND, 'false');
  159. }
  160. }
  161. /**
  162. * Returns if the user has granted the notification permission
  163. * @returns {boolean}
  164. */
  165. public getNotificationPermission(): boolean {
  166. return this.notificationPermission;
  167. }
  168. /**
  169. * Returns if the user wants to receive notifications
  170. * @returns {boolean}
  171. */
  172. public getWantsNotifications(): boolean {
  173. return this.desktopNotifications;
  174. }
  175. /**
  176. * Returns if the user wants a message preview in the notification
  177. * @returns {boolean}
  178. */
  179. public getWantsPreview(): boolean {
  180. return this.notificationPreview;
  181. }
  182. /**
  183. * Returns if the user wants sound when a new message arrives
  184. * @returns {boolean}
  185. */
  186. public getWantsSound(): boolean {
  187. return this.notificationSound;
  188. }
  189. /**
  190. * Returns if the notification api is available
  191. * @returns {boolean}
  192. */
  193. public isNotificationApiAvailable(): boolean {
  194. return this.notificationAPIAvailable;
  195. }
  196. /**
  197. * Sets if the user wants desktop notifications
  198. * @param wantsNotifications
  199. */
  200. public setWantsNotifications(wantsNotifications: boolean): void {
  201. this.$log.debug(this.logTag, 'Requesting notification preference change to', wantsNotifications);
  202. if (wantsNotifications) {
  203. this.requestNotificationPermission();
  204. } else {
  205. this.desktopNotifications = false;
  206. this.storeSetting(NotificationService.SETTINGS_NOTIFICATIONS, 'false');
  207. }
  208. }
  209. /**
  210. * Sets if the user wants a message preview
  211. * @param wantsPreview
  212. */
  213. public setWantsPreview(wantsPreview: boolean): void {
  214. this.$log.debug(this.logTag, 'Requesting preview preference change to', wantsPreview);
  215. this.notificationPreview = wantsPreview;
  216. this.storeSetting(NotificationService.SETTINGS_NOTIFICATION_PREVIEW, wantsPreview ? 'true' : 'false');
  217. }
  218. /**
  219. * Sets if the user wants sound when a new message arrives
  220. * @param wantsSound
  221. */
  222. public setWantsSound(wantsSound: boolean): void {
  223. this.$log.debug(this.logTag, 'Requesting sound preference change to', wantsSound);
  224. this.notificationSound = wantsSound;
  225. this.storeSetting(NotificationService.SETTINGS_NOTIFICATION_SOUND, wantsSound ? 'true' : 'false');
  226. }
  227. /**
  228. * Stores the given key/value pair in local storage
  229. * @param key
  230. * @param value
  231. */
  232. private storeSetting(key: string, value: string): void {
  233. this.settingsService.storeUntrustedKeyValuePair(key, value);
  234. }
  235. /**
  236. * Retrieves the value for the given key from local storage
  237. * @param key
  238. * @returns {string}
  239. */
  240. private retrieveSetting(key: string): string {
  241. return this.settingsService.retrieveUntrustedKeyValuePair(key);
  242. }
  243. /**
  244. * Notify the user via Desktop Notification API.
  245. *
  246. * Return a boolean indicating whether the notification has been shown.
  247. *
  248. * @param tag A tag used to group similar notifications.
  249. * @param title The notification title
  250. * @param body The notification body
  251. * @param avatar URL to the avatar file
  252. * @param clickCallback Callback function to be executed on click
  253. */
  254. public showNotification(tag: string, title: string, body: string,
  255. avatar: string = '/img/threema-64x64.png', clickCallback: any): boolean {
  256. // Play sound on new message if the user wants to
  257. if (this.notificationSound) {
  258. const audio = new Audio(NotificationService.NOTIFICATION_SOUND);
  259. audio.play();
  260. }
  261. // Only show notifications if user granted permission to do so
  262. if (this.notificationPermission !== true || this.desktopNotifications !== true) {
  263. return false;
  264. }
  265. // Clear body string if the user does not want a notification preview
  266. if (!this.notificationPreview) {
  267. body = '';
  268. // Clear notification cache
  269. if (this.notificationCache[tag]) {
  270. this.clearCache(tag);
  271. }
  272. }
  273. // If the cache is not empty, append old messages
  274. if (this.notificationCache[tag]) {
  275. body += '\n' + this.notificationCache[tag].body;
  276. }
  277. // Show notification
  278. this.$log.debug(this.logTag, 'Showing notification', tag);
  279. const notification = new this.$window.Notification(title, {
  280. icon: avatar,
  281. body: body.trim(),
  282. tag: tag,
  283. });
  284. // Hide notification on click
  285. notification.onclick = () => {
  286. this.$window.focus();
  287. // Redirect to welcome screen
  288. if (clickCallback !== undefined) {
  289. clickCallback();
  290. }
  291. this.$log.debug(this.logTag, 'Hiding notification', tag, 'on click');
  292. notification.close();
  293. this.clearCache(tag);
  294. };
  295. // Update notification cache
  296. this.notificationCache[tag] = notification;
  297. return true;
  298. }
  299. /**
  300. * Hide the notification with the specified tag.
  301. *
  302. * Return whether the notification was hidden.
  303. */
  304. public hideNotification(tag: string): boolean {
  305. const notification = this.notificationCache[tag];
  306. if (notification !== undefined) {
  307. this.$log.debug(this.logTag, 'Hiding notification', tag);
  308. notification.close();
  309. this.clearCache(tag);
  310. return true;
  311. } else {
  312. return false;
  313. }
  314. }
  315. /**
  316. * Clear the notification cache for the specified tag.
  317. */
  318. public clearCache(tag: string) {
  319. delete this.notificationCache[tag];
  320. }
  321. }