diff --git a/_locales/en/messages.json b/_locales/en/messages.json index fe86256570..109594fe84 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1740,6 +1740,11 @@ "description": "Shown in notifications and in the left pane instead of sticker image." }, + "message--getDescription--unsupported-message": { + "message": "Unsupported message", + "description": + "Shown in notifications and in the left pane when a message has features too new for this signal install." + }, "stickers--toast--InstallFailed": { "message": "Sticker pack could not be installed", "description": @@ -1863,5 +1868,38 @@ "confirmation-dialog--Cancel": { "message": "Cancel", "description": "Appears on the cancel button in confirmation dialogs." + }, + "Message--unsupported-message": { + "message": + "$contact$ sent you a message that can't be processed or displayed because it uses a new Signal feature.", + "placeholders": { + "contact": { + "content": "$1", + "example": "Alice" + } + } + }, + "Message--unsupported-message-ask-to-resend": { + "message": + "You can ask $contact$ to re-send this message now that you are using an up-to-date version of Signal.", + "placeholders": { + "contact": { + "content": "$1", + "example": "Alice" + } + } + }, + "Message--from-me-unsupported-message": { + "message": + "One of your devices sent a message that can't be processed or displayed because it uses a new Signal feature." + }, + "Message--from-me-unsupported-message-ask-to-resend": { + "message": + "You’ve updated to the latest version of Signal and will now receive this message type on your device." + }, + "Message--update-signal": { + "message": "Update Signal", + "description": + "Text for a button which will take user to Signal download page" } } diff --git a/js/models/messages.js b/js/models/messages.js index 77bd0c2883..6b1b09167e 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -87,6 +87,10 @@ ); } + this.CURRENT_PROTOCOL_VERSION = + textsecure.protobuf.DataMessage.ProtocolVersion.CURRENT; + this.INITIAL_PROTOCOL_VERSION = + textsecure.protobuf.DataMessage.ProtocolVersion.INITIAL; this.OUR_NUMBER = textsecure.storage.user.getNumber(); this.on('destroy', this.onDestroy); @@ -122,7 +126,12 @@ // Top-level prop generation for the message bubble generateProps() { - if (this.isExpirationTimerUpdate()) { + if (this.isUnsupportedMessage()) { + this.props = { + type: 'unsupportedMessage', + data: this.getPropsForUnsupportedMessage(), + }; + } else if (this.isExpirationTimerUpdate()) { this.props = { type: 'timerNotification', data: this.getPropsForTimerNotification(), @@ -267,6 +276,16 @@ }, // Bucketing messages + isUnsupportedMessage() { + const versionAtReceive = this.get('supportedVersionAtReceive'); + const requiredVersion = this.get('requiredProtocolVersion'); + + return ( + _.isNumber(versionAtReceive) && + _.isNumber(requiredVersion) && + versionAtReceive < requiredVersion + ); + }, isExpirationTimerUpdate() { const flag = textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; @@ -289,6 +308,16 @@ }, // Props for each message type + getPropsForUnsupportedMessage() { + const requiredVersion = this.get('requiredProtocolVersion'); + const canProcessNow = this.CURRENT_PROTOCOL_VERSION >= requiredVersion; + const phoneNumber = this.getSource(); + + return { + canProcessNow, + contact: this.findAndFormatContact(phoneNumber), + }; + }, getPropsForTimerNotification() { const timerUpdate = this.get('expirationTimerUpdate'); if (!timerUpdate) { @@ -479,6 +508,7 @@ this.trigger('download', downloadOptions), openLink: url => this.trigger('navigate-to', url), + downloadNewVersion: () => this.trigger('download-new-version'), scrollToMessage: scrollOptions => this.trigger('scroll-to-message', scrollOptions), }; @@ -694,6 +724,9 @@ // More display logic getDescription() { + if (this.isUnsupportedMessage()) { + return i18n('message--getDescription--unsupported-message'); + } if (this.isGroupUpdate()) { const groupUpdate = this.get('group_update'); if (groupUpdate.left === 'You') { @@ -1733,6 +1766,10 @@ hasFileAttachments: dataMessage.hasFileAttachments, hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments, preview, + requiredProtocolVersion: + dataMessage.requiredProtocolVersion || + this.INITIAL_PROTOCOL_VERSION, + supportedVersionAtReceive: this.CURRENT_PROTOCOL_VERSION, quote: dataMessage.quote, schemaVersion: dataMessage.schemaVersion, sticker: dataMessage.sticker, @@ -1888,10 +1925,12 @@ message.set({ id }); MessageController.register(message.id, message); - // Note that this can save the message again, if jobs were queued. We need to - // call it after we have an id for this message, because the jobs refer back - // to their source message. - await message.queueAttachmentDownloads(); + if (!message.isUnsupportedMessage()) { + // Note that this can save the message again, if jobs were queued. We need to + // call it after we have an id for this message, because the jobs refer back + // to their source message. + await message.queueAttachmentDownloads(); + } await window.Signal.Data.updateConversation( conversationId, diff --git a/js/modules/signal.js b/js/modules/signal.js index 3e85ffc4ff..60626fdb09 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -65,6 +65,9 @@ const { const { TypingBubble, } = require('../../ts/components/conversation/TypingBubble'); +const { + UnsupportedMessage, +} = require('../../ts/components/conversation/UnsupportedMessage'); const { VerificationNotification, } = require('../../ts/components/conversation/VerificationNotification'); @@ -277,6 +280,7 @@ exports.setup = (options = {}) => { Message: MediaGalleryMessage, }, TypingBubble, + UnsupportedMessage, VerificationNotification, }; diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index f293f4e043..fb9e041775 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -144,6 +144,13 @@ this.listenTo(this.model.messageCollection, 'navigate-to', url => { window.location = url; }); + this.listenTo( + this.model.messageCollection, + 'download-new-version', + () => { + window.location = 'https://signal.org/download'; + } + ); this.lazyUpdateVerified = _.debounce( this.model.updateVerified.bind(this.model), diff --git a/js/views/message_view.js b/js/views/message_view.js index ef1b26d57e..8b1301ce49 100644 --- a/js/views/message_view.js +++ b/js/views/message_view.js @@ -51,7 +51,12 @@ const { Components } = window.Signal; const { type, data: props } = this.model.props; - if (type === 'timerNotification') { + if (type === 'unsupportedMessage') { + return { + Component: Components.UnsupportedMessage, + props, + }; + } else if (type === 'timerNotification') { return { Component: Components.TimerNotification, props, diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 83d9646b30..8c2c3bde3c 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -169,17 +169,25 @@ message DataMessage { optional AttachmentPointer data = 4; } - optional string body = 1; - repeated AttachmentPointer attachments = 2; - optional GroupContext group = 3; - optional uint32 flags = 4; - optional uint32 expireTimer = 5; - optional bytes profileKey = 6; - optional uint64 timestamp = 7; - optional Quote quote = 8; - repeated Contact contact = 9; - repeated Preview preview = 10; - optional Sticker sticker = 11; + enum ProtocolVersion { + option allow_alias = true; + + INITIAL = 0; + CURRENT = 0; + } + + optional string body = 1; + repeated AttachmentPointer attachments = 2; + optional GroupContext group = 3; + optional uint32 flags = 4; + optional uint32 expireTimer = 5; + optional bytes profileKey = 6; + optional uint64 timestamp = 7; + optional Quote quote = 8; + repeated Contact contact = 9; + repeated Preview preview = 10; + optional Sticker sticker = 11; + optional uint32 requiredProtocolVersion = 12; } message NullMessage { diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 5589267cda..7577c00da7 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -1200,7 +1200,7 @@ font-weight: 300; } -.module-verification-notification__button { +.module-safety-number-notification__button { margin-top: 5px; display: inline-block; cursor: pointer; @@ -4448,6 +4448,79 @@ } } +// Module: Unsupported Message + +.module-unsupported-message { + margin-top: 14px; + text-align: center; +} + +.module-unsupported-message__icon { + height: 24px; + width: 24px; + margin-left: auto; + margin-right: auto; + margin-bottom: 7px; + + @include light-theme { + @include color-svg('../images/error.svg', $color-gray-60); + } + + @include dark-theme { + @include color-svg('../images/error.svg', $color-dark-30); + } +} + +.module-unsupported-message__icon--can-process { + @include light-theme { + @include color-svg('../images/check-circle-outline.svg', $color-gray-60); + } + + @include dark-theme { + @include color-svg('../images/check-circle-outline.svg', $color-dark-30); + } +} + +.module-unsupported-message__text { + font-size: 14px; + line-height: 20px; + letter-spacing: 0.3px; + max-width: 396px; + margin-left: auto; + margin-right: auto; + + @include light-theme { + color: $color-gray-60; + } + @include dark-theme { + color: $color-dark-30; + } +} + +.module-unsupported-message__contact { + font-weight: 300; +} + +.module-unsupported-message__button { + margin-top: 5px; + display: inline-block; + cursor: pointer; + font-size: 13px; + font-weight: 300; + line-height: 18px; + padding: 12px; + border-radius: 4px; + + @include light-theme { + color: $color-signal-blue; + background-color: $color-light-02; + } + @include dark-theme { + color: $color-signal-blue; + background-color: $color-gray-75; + } +} + // Third-party module: react-contextmenu .react-contextmenu { diff --git a/stylesheets/_theme_dark.scss b/stylesheets/_theme_dark.scss index a55607499e..5c0f9c2501 100644 --- a/stylesheets/_theme_dark.scss +++ b/stylesheets/_theme_dark.scss @@ -1023,7 +1023,7 @@ body.dark-theme { color: $color-dark-30; } - .module-verification-notification__button { + .module-safety-number-notification__button { color: $color-signal-blue; background-color: $color-gray-75; } diff --git a/ts/components/conversation/SafetyNumberNotification.md b/ts/components/conversation/SafetyNumberNotification.md index 54249bcdf6..b2405a2f1d 100644 --- a/ts/components/conversation/SafetyNumberNotification.md +++ b/ts/components/conversation/SafetyNumberNotification.md @@ -1,6 +1,6 @@ ### In group conversation -```js +```jsx { name={contact.name} profileName={contact.profileName} phoneNumber={contact.phoneNumber} - module="module-verification-notification__contact" + module="module-safety-number-notification__contact" /> , ]} @@ -62,7 +62,7 @@ export class SafetyNumberNotification extends React.Component { onClick={() => { showIdentity(contact.id); }} - className="module-verification-notification__button" + className="module-safety-number-notification__button" > {i18n('verifyNewNumber')} diff --git a/ts/components/conversation/UnsupportedMessage.md b/ts/components/conversation/UnsupportedMessage.md new file mode 100644 index 0000000000..2d7bac27a7 --- /dev/null +++ b/ts/components/conversation/UnsupportedMessage.md @@ -0,0 +1,69 @@ +### From someone in your contacts + +```jsx + + console.log('downloadNewVersion')} + /> + +``` + +### After you upgrade + +```jsx + + console.log('downloadNewVersion')} + /> + +``` + +### No name, just profile + +```jsx + + console.log('downloadNewVersion')} + /> + +``` + +### From yourself + +```jsx + + console.log('downloadNewVersion')} + /> + +``` + +### From yourself, after you upgrade + +```jsx + + console.log('downloadNewVersion')} + /> + +``` diff --git a/ts/components/conversation/UnsupportedMessage.tsx b/ts/components/conversation/UnsupportedMessage.tsx new file mode 100644 index 0000000000..528c220383 --- /dev/null +++ b/ts/components/conversation/UnsupportedMessage.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import classNames from 'classnames'; + +import { ContactName } from './ContactName'; +import { Intl } from '../Intl'; +import { LocalizerType } from '../../types/Util'; + +interface ContactType { + id: string; + phoneNumber: string; + profileName?: string; + name?: string; + isMe: boolean; +} + +export type PropsData = { + canProcessNow: boolean; + contact: ContactType; +}; + +type PropsHousekeeping = { + i18n: LocalizerType; +}; + +export type PropsActions = { + downloadNewVersion: () => unknown; +}; + +type Props = PropsData & PropsHousekeeping & PropsActions; + +export class UnsupportedMessage extends React.Component { + public render() { + const { canProcessNow, contact, i18n, downloadNewVersion } = this.props; + const { isMe } = contact; + + const otherStringId = canProcessNow + ? 'Message--unsupported-message-ask-to-resend' + : 'Message--unsupported-message'; + const meStringId = canProcessNow + ? 'Message--from-me-unsupported-message-ask-to-resend' + : 'Message--from-me-unsupported-message'; + const stringId = isMe ? meStringId : otherStringId; + + return ( +
+
+
+ + + , + ]} + i18n={i18n} + /> +
+ {canProcessNow ? null : ( +
{ + downloadNewVersion(); + }} + className="module-unsupported-message__button" + > + {i18n('Message--update-signal')} +
+ )} +
+ ); + } +} diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 95be87139a..49af321e7a 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -872,7 +872,7 @@ "rule": "jQuery-append(", "path": "js/views/message_view.js", "line": " this.$el.append(this.childView.el);", - "lineNumber": 139, + "lineNumber": 144, "reasonCategory": "usageTrusted", "updated": "2018-09-19T18:13:29.628Z", "reasonDetail": "Interacting with already-existing DOM nodes"