diff --git a/_locales/en/messages.json b/_locales/en/messages.json index b9d68892a..52f12f17b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -729,6 +729,18 @@ "message": "New Messages", "description": "Displayed in notifications for multiple messages" }, + "notificationMostRecentFrom": { + "message": "Most recent from:", + "description": "Displayed in notifications when setting is 'name only' and more than one message is waiting" + }, + "notificationFrom": { + "message": "From:", + "description": "Displayed in notifications when setting is 'name only' and one message is waiting" + }, + "notificationMostRecent": { + "message": "Most recent:", + "description": "Displayed in notifications when setting is 'name and message' and " + }, "messageNotSent": { "message": "Message not sent.", "description": "Informational label, appears on messages that failed to send" diff --git a/js/background.js b/js/background.js index eb6a50c8b..ff7761f74 100644 --- a/js/background.js +++ b/js/background.js @@ -238,6 +238,10 @@ appView.openInbox(); } }); + + window.addEventListener('focus', () => Whisper.Notifications.clear()); + window.addEventListener('unload', () => Whisper.Notifications.clear()); + Whisper.events.on('showConversation', function(conversation) { if (appView) { appView.openConversation(conversation); diff --git a/js/models/conversations.js b/js/models/conversations.js index 6741f012e..ff9ec684c 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1642,14 +1642,27 @@ 'private' ).then(sender => sender.getNotificationIcon().then(iconUrl => { - console.log('adding notification'); + const messageJSON = message.toJSON(); + const messageSentAt = messageJSON.sent_at; + const messageId = message.id; + const isExpiringMessage = Signal.Types.Message.hasExpiration( + messageJSON + ); + + console.log('Add notification', { + conversationId: this.idForLogging(), + isExpiringMessage, + messageSentAt, + }); Whisper.Notifications.add({ - title: sender.getTitle(), - message: message.getNotificationText(), + conversationId, iconUrl, imageUrl: message.getImageUrl(), - conversationId, - messageId: message.id, + isExpiringMessage, + message: message.getNotificationText(), + messageId, + messageSentAt, + title: sender.getTitle(), }); }) ); diff --git a/js/modules/types/message.js b/js/modules/types/message.js index bb03f0d11..fd339cb6f 100644 --- a/js/modules/types/message.js +++ b/js/modules/types/message.js @@ -7,6 +7,7 @@ const SchemaVersion = require('./schema_version'); const { initializeAttachmentMetadata, } = require('../../../ts/types/message/initializeAttachmentMetadata'); +const MessageTS = require('../../../ts/types/Message'); const GROUP = 'group'; const PRIVATE = 'private'; @@ -381,3 +382,5 @@ exports.createAttachmentDataWriter = writeExistingAttachmentData => { return messageWithoutAttachmentData; }; }; + +exports.hasExpiration = MessageTS.hasExpiration; diff --git a/js/notifications.js b/js/notifications.js index 3d6fb6ec2..759af602c 100644 --- a/js/notifications.js +++ b/js/notifications.js @@ -1,3 +1,4 @@ +/* global Signal:false */ /* global Backbone: false */ /* global ConversationController: false */ @@ -16,7 +17,6 @@ const { Settings } = Signal.Types; const SettingNames = { - OFF: 'off', COUNT: 'count', NAME: 'name', MESSAGE: 'message', @@ -27,6 +27,8 @@ this.isEnabled = false; this.on('add', this.update); this.on('remove', this.onRemove); + + this.lastNotification = null; }, onClick(conversationId) { const conversation = ConversationController.get(conversationId); @@ -74,36 +76,56 @@ // distinguishing between zero (0) and other (non-zero), // e.g. Russian: // http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html - const newMessageCount = [ - numNotifications, - numNotifications === 1 ? i18n('newMessage') : i18n('newMessages'), - ].join(' '); + const newMessageCountLabel = `${numNotifications} ${ + numNotifications === 1 ? i18n('newMessage') : i18n('newMessages') + }`; - const last = this.last(); + const last = this.last().toJSON(); switch (userSetting) { case SettingNames.COUNT: title = 'Signal'; - message = newMessageCount; + message = newMessageCountLabel; break; - case SettingNames.NAME: - title = newMessageCount; - message = `Most recent from ${last.get('title')}`; - iconUrl = last.get('iconUrl'); + case SettingNames.NAME: { + const lastMessageTitle = last.title; + title = newMessageCountLabel; + // eslint-disable-next-line prefer-destructuring + iconUrl = last.iconUrl; + if (numNotifications === 1) { + message = `${i18n('notificationFrom')} ${lastMessageTitle}`; + } else { + message = `${i18n( + 'notificationMostRecentFrom' + )} ${lastMessageTitle}`; + } break; + } case SettingNames.MESSAGE: if (numNotifications === 1) { - title = last.get('title'); + // eslint-disable-next-line prefer-destructuring + title = last.title; + // eslint-disable-next-line prefer-destructuring + message = last.message; } else { - title = newMessageCount; + title = newMessageCountLabel; + message = `${i18n('notificationMostRecent')} ${last.message}`; } - message = last.get('message'); - iconUrl = last.get('iconUrl'); + // eslint-disable-next-line prefer-destructuring + iconUrl = last.iconUrl; break; default: - console.log(`Error: Unknown user setting: '${userSetting}'`); + console.log( + `Error: Unknown user notification setting: '${userSetting}'` + ); break; } + const shouldHideExpiringMessageBody = + last.isExpiringMessage && Signal.OS.isMacOS(); + if (shouldHideExpiringMessageBody) { + message = i18n('newMessage'); + } + drawAttention(); const notification = new Notification(title, { @@ -112,18 +134,31 @@ tag: isNotificationGroupingSupported ? 'signal' : undefined, silent: !status.shouldPlayNotificationSound, }); - notification.onclick = () => this.onClick(last.get('conversationId')); + notification.onclick = () => this.onClick(last.conversationId); + this.lastNotification = notification; - this.clear(); + // We continue to build up more and more messages for our notifications + // until the user comes back to our app or closes the app. Then we’ll + // clear everything out. The good news is that we'll have a maximum of + // 1 notification in the Notification area (something like + // ‘10 new messages’) assuming that `Notification::close` does its job. }, getUserSetting() { return storage.get('notification-setting') || SettingNames.MESSAGE; }, onRemove() { console.log('Remove notification'); + if (this.length === 0) { + this.clear(); + } else { + this.update(); + } }, clear() { console.log('Remove all notifications'); + if (this.lastNotification) { + this.lastNotification.close(); + } this.reset([]); }, enable() { diff --git a/js/read_syncs.js b/js/read_syncs.js index 9f13125c4..5f123505a 100644 --- a/js/read_syncs.js +++ b/js/read_syncs.js @@ -25,6 +25,7 @@ ); }); if (message) { + Whisper.Notifications.remove(message); return message.markRead(receipt.get('read_at')).then( function() { this.notifyConversation(message); diff --git a/ts/types/Message.ts b/ts/types/Message.ts index 09f0f39de..ea5417de0 100644 --- a/ts/types/Message.ts +++ b/ts/types/Message.ts @@ -17,6 +17,7 @@ export type IncomingMessage = Readonly< body?: string; decrypted_at?: number; errors?: Array; + expireTimer?: number; flags?: number; source?: string; sourceDevice?: number; @@ -89,3 +90,15 @@ type MessageSchemaVersion6 = Partial< contact: Array; }> >; + +export const isUserMessage = (message: Message): message is UserMessage => + message.type === 'incoming' || message.type === 'outgoing'; + +export const hasExpiration = (message: Message): boolean => { + if (!isUserMessage(message)) { + return false; + } + + const { expireTimer } = message; + return typeof expireTimer === 'number' && expireTimer > 0; +};