From b87faa9291e8287cf11ed418e9e6cc712c063358 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 31 May 2021 15:40:23 +1000 Subject: [PATCH 1/3] fix SessionIcon viewbox being out of bounds --- ts/components/session/icon/SessionIcon.tsx | 23 +++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/ts/components/session/icon/SessionIcon.tsx b/ts/components/session/icon/SessionIcon.tsx index d9d069b17..f04d8f98c 100644 --- a/ts/components/session/icon/SessionIcon.tsx +++ b/ts/components/session/icon/SessionIcon.tsx @@ -35,13 +35,6 @@ const getIconDimensionFromIconSize = (iconSize: SessionIconSize | number) => { } }; -type StyledSvgProps = { - width: string | number; - height: string | number; - iconRotation: number; - rotateDuration?: number; -}; - const rotate = keyframes` from { transform: rotate(0deg); @@ -61,11 +54,17 @@ const animation = (props: { rotateDuration?: any }) => { } }; +type StyledSvgProps = { + width: string | number; + iconRotation: number; + rotateDuration?: number; +}; + //tslint:disable no-unnecessary-callback-wrapper const Svg = styled.svg` width: ${props => props.width}; - animation: ${props => animation(props)}; transform: ${props => `rotate(${props.iconRotation}deg)`}; + animation: ${props => animation(props)}; `; //tslint:enable no-unnecessary-callback-wrapper @@ -81,7 +80,13 @@ const SessionSvg = (props: { }) => { const colorSvg = props.iconColor || props?.theme?.colors.textColor; const pathArray = props.path instanceof Array ? props.path : [props.path]; - const propsToPick = _.pick(props, 'width', 'animation', 'transform') as any; + const propsToPick = { + width: props.width, + height: props.height, + rotateDuration: props.rotateDuration, + iconRotation: props.iconRotation, + viewBox: props.viewBox, + }; return ( From 810ccdf675df0140484bc438dbcd28385e94d3a1 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 1 Jun 2021 10:08:16 +1000 Subject: [PATCH 2/3] send dataExtractionNotificaionMessage on saving attachment on priv chats --- protos/SignalService.proto | 13 +++ ts/components/LightboxGallery.tsx | 13 ++- .../media-gallery/types/Message.ts | 1 + .../conversation/SessionConversation.tsx | 30 +++++-- .../conversation/SessionMessagesList.tsx | 15 +++- .../conversation/SessionRightPanel.tsx | 22 ++++-- ts/models/message.ts | 2 +- ts/receiver/contentMessage.ts | 26 +++--- .../controlMessage/ConfigurationMessage.ts | 1 - .../DataExtractionNotificationMessage.ts | 79 +++++++++++++++++++ .../media-gallery/groupMessagesByDate_test.ts | 45 +++++++++++ 11 files changed, 219 insertions(+), 28 deletions(-) create mode 100644 ts/session/messages/outgoing/controlMessage/DataExtractionNotificationMessage.ts diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 87560cb83..e05050960 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -37,6 +37,8 @@ message Content { optional ReceiptMessage receiptMessage = 5; optional TypingMessage typingMessage = 6; optional ConfigurationMessage configurationMessage = 7; + optional DataExtractionNotification dataExtractionNotification = 82; + } message KeyPair { @@ -46,6 +48,17 @@ message KeyPair { required bytes privateKey = 2; } +message DataExtractionNotification { + + enum Type { + SCREENSHOT = 1; // no way to know this on Desktop + MEDIA_SAVED = 2; // timestamp + } + + // @required + required Type type = 1; + optional uint64 timestamp = 2; +} message DataMessage { diff --git a/ts/components/LightboxGallery.tsx b/ts/components/LightboxGallery.tsx index 91e31ba13..8519cb1b4 100644 --- a/ts/components/LightboxGallery.tsx +++ b/ts/components/LightboxGallery.tsx @@ -18,12 +18,20 @@ export interface MediaItemType { index: number; attachment: AttachmentType; message: Message; + messageTimestamp: number; + messageSender: string; } type Props = { close: () => void; media: Array; - onSave?: (options: { attachment: AttachmentType; message: Message; index: number }) => void; + onSave?: (options: { + attachment: AttachmentType; + message: Message; + index: number; + messageTimestamp?: number; + messageSender: string; + }) => void; selectedIndex: number; }; @@ -58,11 +66,12 @@ export const LightboxGallery = (props: Props) => { } const mediaItem = media[currentIndex]; - onSave({ attachment: mediaItem.attachment, message: mediaItem.message, index: mediaItem.index, + messageTimestamp: mediaItem.messageTimestamp || mediaItem?.message?.sent_at, + messageSender: mediaItem.messageSender || (mediaItem?.message as any)?.source, }); }; diff --git a/ts/components/conversation/media-gallery/types/Message.ts b/ts/components/conversation/media-gallery/types/Message.ts index dd773402a..954987d2e 100644 --- a/ts/components/conversation/media-gallery/types/Message.ts +++ b/ts/components/conversation/media-gallery/types/Message.ts @@ -4,4 +4,5 @@ export type Message = { id: string; attachments: Array; received_at: number; + sent_at: number; }; diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index bc00863fb..32bd72aba 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -32,6 +32,7 @@ import { getDecryptedMediaUrl } from '../../../session/crypto/DecryptedAttachmen import { deleteOpenGroupMessages } from '../../../interactions/conversation'; import { ConversationTypeEnum } from '../../../models/conversation'; import { updateMentionsMembers } from '../../../state/ducks/mentionsInput'; +import { sendDataExtractionNotification } from '../../../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage'; interface State { // Message sending progress @@ -63,7 +64,12 @@ interface State { quotedMessageProps?: any; // lightbox options - lightBoxOptions?: any; + lightBoxOptions?: LightBoxOptions; +} + +export interface LightBoxOptions { + media: Array; + attachment: any; } interface Props { @@ -520,7 +526,7 @@ export class SessionConversation extends React.Component { onRemoveModerators: () => { window.Whisper.events.trigger('removeModerators', conversation); }, - onShowLightBox: (lightBoxOptions = {}) => { + onShowLightBox: (lightBoxOptions?: LightBoxOptions) => { this.setState({ lightBoxOptions }); }, }; @@ -760,14 +766,18 @@ export class SessionConversation extends React.Component { } private onClickAttachment(attachment: any, message: any) { + // message is MessageTypeInConvo.propsForMessage I think const media = (message.attachments || []).map((attachmentForMedia: any) => { return { objectURL: attachmentForMedia.url, contentType: attachmentForMedia.contentType, attachment: attachmentForMedia, + messageSender: message.authorPhoneNumber, + messageTimestamp: message.direction !== 'outgoing' ? message.timestamp : undefined, // do not set this field when the message was sent from us + // if it is set, this will trigger a sending of DataExtractionNotification to that user, but for an attachment we sent ourself. }; }); - const lightBoxOptions = { + const lightBoxOptions: LightBoxOptions = { media, attachment, }; @@ -858,7 +868,7 @@ export class SessionConversation extends React.Component { }); } - private renderLightBox({ media, attachment }: { media: Array; attachment: any }) { + private renderLightBox({ media, attachment }: LightBoxOptions) { const selectedIndex = media.length > 1 ? media.findIndex((mediaMessage: any) => mediaMessage.attachment.path === attachment.path) @@ -880,10 +890,14 @@ export class SessionConversation extends React.Component { attachment, message, index, + messageTimestamp, + messageSender, }: { attachment: AttachmentType; message?: Message; index?: number; + messageTimestamp?: number; + messageSender: string; }) { const { getAbsoluteAttachmentPath } = window.Signal.Migrations; attachment.url = await getDecryptedMediaUrl(attachment.url, attachment.contentType); @@ -891,8 +905,14 @@ export class SessionConversation extends React.Component { attachment, document, getAbsolutePath: getAbsoluteAttachmentPath, - timestamp: message?.received_at || Date.now(), + timestamp: messageTimestamp || message?.received_at, }); + + await sendDataExtractionNotification( + this.props.selectedConversationKey, + messageSender, + messageTimestamp + ); } private async onChoseAttachments(attachmentsFileList: Array) { diff --git a/ts/components/session/conversation/SessionMessagesList.tsx b/ts/components/session/conversation/SessionMessagesList.tsx index 15b761ac1..8c0875d83 100644 --- a/ts/components/session/conversation/SessionMessagesList.tsx +++ b/ts/components/session/conversation/SessionMessagesList.tsx @@ -45,7 +45,14 @@ interface Props { replyToMessage: (messageId: number) => Promise; showMessageDetails: (messageProps: any) => void; onClickAttachment: (attachment: any, message: any) => void; - onDownloadAttachment: ({ attachment }: { attachment: any }) => void; + onDownloadAttachment: ({ + attachment, + messageTimestamp, + }: { + attachment: any; + messageTimestamp: number; + messageSender: string; + }) => void; onDeleteSelectedMessages: () => Promise; } @@ -305,7 +312,11 @@ export class SessionMessagesList extends React.Component { this.props.onClickAttachment(attachment, messageProps); }; messageProps.onDownload = (attachment: AttachmentType) => { - this.props.onDownloadAttachment({ attachment }); + this.props.onDownloadAttachment({ + attachment, + messageTimestamp: messageProps.timestamp, + messageSender: messageProps.authorPhoneNumber, + }); }; messageProps.isQuotedMessageToAnimate = messageProps.id === this.state.animateQuotedMessageId; diff --git a/ts/components/session/conversation/SessionRightPanel.tsx b/ts/components/session/conversation/SessionRightPanel.tsx index 4b47f5002..44dd4ea9d 100644 --- a/ts/components/session/conversation/SessionRightPanel.tsx +++ b/ts/components/session/conversation/SessionRightPanel.tsx @@ -18,6 +18,9 @@ import { getMessagesWithVisualMediaAttachments, } from '../../../data/data'; import { getDecryptedMediaUrl } from '../../../session/crypto/DecryptedAttachmentsManager'; +import { LightBoxOptions } from './SessionConversation'; +import { UserUtils } from '../../../session/utils'; +import { sendDataExtractionNotification } from '../../../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage'; interface Props { id: string; @@ -44,7 +47,7 @@ interface Props { onAddModerators: () => void; onRemoveModerators: () => void; onUpdateGroupMembers: () => void; - onShowLightBox: (options: any) => void; + onShowLightBox: (lightboxOptions?: LightBoxOptions) => void; onSetDisappearingMessages: (seconds: number) => void; theme: DefaultTheme; } @@ -169,7 +172,7 @@ class SessionRightPanel extends React.Component { }); const saveAttachment = async ({ attachment, message }: any = {}) => { - const timestamp = message.received_at; + const timestamp = message.received_at as number | undefined; attachment.url = await getDecryptedMediaUrl(attachment.url, attachment.contentType); save({ attachment, @@ -177,6 +180,7 @@ class SessionRightPanel extends React.Component { getAbsolutePath: window.Signal.Migrations.getAbsoluteAttachmentPath, timestamp, }); + await sendDataExtractionNotification(this.props.id, message?.source, timestamp); }; const onItemClick = ({ message, attachment, type }: any) => { @@ -187,11 +191,17 @@ class SessionRightPanel extends React.Component { } case 'media': { + // don't set the messageTimestamp when we are the sender, so we don't trigger a notification when + // we save the same attachment we sent ourself to another user + const messageTimestamp = + message.source !== UserUtils.getOurPubKeyStrFromCache() + ? message.sent_at || message.received_at + : undefined; const lightBoxOptions = { media, attachment, - message, - }; + messageTimestamp, + } as LightBoxOptions; this.onShowLightBox(lightBoxOptions); break; } @@ -208,8 +218,8 @@ class SessionRightPanel extends React.Component { }; } - public onShowLightBox(options: any) { - this.props.onShowLightBox(options); + public onShowLightBox(lightboxOptions: LightBoxOptions) { + this.props.onShowLightBox(lightboxOptions); } // tslint:disable-next-line: cyclomatic-complexity diff --git a/ts/models/message.ts b/ts/models/message.ts index a41693d40..597f50f73 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -30,9 +30,9 @@ import { uploadQuoteThumbnailsV2, } from '../session/utils/AttachmentsV2'; import { acceptOpenGroupInvitation } from '../interactions/message'; -import { OpenGroupMessageV2 } from '../opengroup/opengroupV2/OpenGroupMessageV2'; import { OpenGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/OpenGroupVisibleMessage'; import { getV2OpenGroupRoom } from '../data/opengroups'; + export class MessageModel extends Backbone.Model { public propsForTimerNotification: any; public propsForGroupNotification: any; diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 32d71552a..3ed0c5875 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -342,7 +342,7 @@ export async function innerHandleContentMessage( return; } if (content.typingMessage) { - await handleTypingMessage(envelope, content.typingMessage); + await handleTypingMessage(envelope, content.typingMessage as SignalService.TypingMessage); return; } if (content.configurationMessage) { @@ -353,6 +353,14 @@ export async function innerHandleContentMessage( ); return; } + if (content.dataExtractionNotification) { + window?.log?.warn('content.dataExtractionNotification', content.dataExtractionNotification); + void handleDataExtractionNotification( + envelope, + content.dataExtractionNotification as SignalService.DataExtractionNotification + ); + return; + } } catch (e) { window?.log?.warn(e); } @@ -422,17 +430,18 @@ async function handleReceiptMessage( async function handleTypingMessage( envelope: EnvelopePlus, - iTypingMessage: SignalService.ITypingMessage + typingMessage: SignalService.TypingMessage ): Promise { - const ev = new Event('typing'); - - const typingMessage = iTypingMessage as SignalService.TypingMessage; - const { timestamp, action } = typingMessage; const { source } = envelope; await removeFromCache(envelope); + // We don't do anything with incoming typing messages if the setting is disabled + if (!window.storage.get('typing-indicators-setting')) { + return; + } + if (envelope.timestamp && timestamp) { const envelopeTimestamp = Lodash.toNumber(envelope.timestamp); const typingTimestamp = Lodash.toNumber(timestamp); @@ -445,11 +454,6 @@ async function handleTypingMessage( } } - // We don't do anything with incoming typing messages if the setting is disabled - if (!window.storage.get('typing-indicators-setting')) { - return; - } - // typing message are only working with direct chats/ not groups const conversation = ConversationController.getInstance().get(source); diff --git a/ts/session/messages/outgoing/controlMessage/ConfigurationMessage.ts b/ts/session/messages/outgoing/controlMessage/ConfigurationMessage.ts index 9d6106792..82c6845db 100644 --- a/ts/session/messages/outgoing/controlMessage/ConfigurationMessage.ts +++ b/ts/session/messages/outgoing/controlMessage/ConfigurationMessage.ts @@ -2,7 +2,6 @@ import { SignalService } from '../../../../protobuf'; import { MessageParams } from '../Message'; -import { Constants } from '../../..'; import { ECKeyPair } from '../../../../receiver/keypairs'; import { fromHexToArray } from '../../../utils/String'; import { PubKey } from '../../../types'; diff --git a/ts/session/messages/outgoing/controlMessage/DataExtractionNotificationMessage.ts b/ts/session/messages/outgoing/controlMessage/DataExtractionNotificationMessage.ts new file mode 100644 index 000000000..69e0c8daa --- /dev/null +++ b/ts/session/messages/outgoing/controlMessage/DataExtractionNotificationMessage.ts @@ -0,0 +1,79 @@ +import { SignalService } from '../../../../protobuf'; +import { MessageParams } from '../Message'; +import { ContentMessage } from '..'; +import { v4 as uuid } from 'uuid'; +import { PubKey } from '../../../types'; +import { getMessageQueue } from '../../..'; +import { ConversationController } from '../../../conversations'; +import { UserUtils } from '../../../utils'; +interface DataExtractionNotificationMessageParams extends MessageParams { + referencedAttachmentTimestamp: number; +} + +export class DataExtractionNotificationMessage extends ContentMessage { + public readonly referencedAttachmentTimestamp: number; + + constructor(params: DataExtractionNotificationMessageParams) { + super({ timestamp: params.timestamp, identifier: params.identifier }); + this.referencedAttachmentTimestamp = params.referencedAttachmentTimestamp; + // this does not make any sense + if (!this.referencedAttachmentTimestamp) { + throw new Error('referencedAttachmentTimestamp must be set'); + } + } + + public contentProto(): SignalService.Content { + return new SignalService.Content({ + dataExtractionNotification: this.dataExtractionProto(), + }); + } + + protected dataExtractionProto(): SignalService.DataExtractionNotification { + const ACTION_ENUM = SignalService.DataExtractionNotification.Type; + + const action = ACTION_ENUM.MEDIA_SAVED; // we cannot know when user screenshots, so it can only be a media saved + + const dataExtraction = new SignalService.DataExtractionNotification(); + dataExtraction.type = action; + dataExtraction.timestamp = this.referencedAttachmentTimestamp; + + return dataExtraction; + } +} + +/** + * Currently only enabled for private chats + */ +export const sendDataExtractionNotification = async ( + conversationId: string, + attachmentSender: string, + referencedAttachmentTimestamp?: number +) => { + const convo = ConversationController.getInstance().get(conversationId); + if ( + !convo || + !convo.isPrivate() || + convo.isMe() || + UserUtils.isUsFromCache(PubKey.cast(attachmentSender)) || + !referencedAttachmentTimestamp + ) { + window.log.warn('Not sending saving attachment notification for', attachmentSender); + return; + } + + const dataExtractionNotificationMessage = new DataExtractionNotificationMessage({ + referencedAttachmentTimestamp, + identifier: uuid(), + timestamp: Date.now(), + }); + const pubkey = PubKey.cast(conversationId); + window.log.info( + `Sending DataExtractionNotification to ${conversationId} about attachment: ${referencedAttachmentTimestamp}` + ); + + try { + await getMessageQueue().sendToPubKey(pubkey, dataExtractionNotificationMessage); + } catch (e) { + window.log.warn('failed to send data extraction notification', e); + } +}; diff --git a/ts/test/components/media-gallery/groupMessagesByDate_test.ts b/ts/test/components/media-gallery/groupMessagesByDate_test.ts index d6244e19f..e6ec4ceed 100644 --- a/ts/test/components/media-gallery/groupMessagesByDate_test.ts +++ b/ts/test/components/media-gallery/groupMessagesByDate_test.ts @@ -7,6 +7,10 @@ import { Section, } from '../../../components/conversation/media-gallery/groupMediaItemsByDate'; import { MediaItemType } from '../../../components/LightboxGallery'; +import { TestUtils } from '../../test-utils'; + +const generatedMessageSenderKey = TestUtils.generateFakePubKey().key; +const generatedMessageTimestamp = Date.now(); const toMediaItem = (date: Date): MediaItemType => ({ objectURL: date.toUTCString(), @@ -14,6 +18,7 @@ const toMediaItem = (date: Date): MediaItemType => ({ message: { id: 'id', received_at: date.getTime(), + sent_at: date.getTime(), attachments: [], }, attachment: { @@ -22,6 +27,8 @@ const toMediaItem = (date: Date): MediaItemType => ({ url: 'url', }, contentType: IMAGE_JPEG, + messageSender: generatedMessageSenderKey, + messageTimestamp: generatedMessageTimestamp, }); // tslint:disable: max-func-body-length @@ -57,6 +64,7 @@ describe('groupMediaItemsByDate', () => { message: { id: 'id', + sent_at: 1523534400000, received_at: 1523534400000, attachments: [], }, @@ -65,6 +73,8 @@ describe('groupMediaItemsByDate', () => { contentType: IMAGE_JPEG, url: 'url', }, + messageSender: generatedMessageSenderKey, + messageTimestamp: generatedMessageTimestamp, }, { objectURL: 'Thu, 12 Apr 2018 00:01:00 GMT', @@ -73,6 +83,7 @@ describe('groupMediaItemsByDate', () => { message: { id: 'id', received_at: 1523491260000, + sent_at: 1523491260000, attachments: [], }, attachment: { @@ -80,6 +91,8 @@ describe('groupMediaItemsByDate', () => { contentType: IMAGE_JPEG, url: 'url', }, + messageSender: generatedMessageSenderKey, + messageTimestamp: generatedMessageTimestamp, }, ], }, @@ -93,6 +106,8 @@ describe('groupMediaItemsByDate', () => { message: { id: 'id', received_at: 1523491140000, + sent_at: 1523491140000, + attachments: [], }, attachment: { @@ -100,6 +115,8 @@ describe('groupMediaItemsByDate', () => { contentType: IMAGE_JPEG, url: 'url', }, + messageSender: generatedMessageSenderKey, + messageTimestamp: generatedMessageTimestamp, }, ], }, @@ -113,6 +130,8 @@ describe('groupMediaItemsByDate', () => { message: { id: 'id', received_at: 1523232060000, + sent_at: 1523232060000, + attachments: [], }, attachment: { @@ -120,6 +139,8 @@ describe('groupMediaItemsByDate', () => { contentType: IMAGE_JPEG, url: 'url', }, + messageSender: generatedMessageSenderKey, + messageTimestamp: generatedMessageTimestamp, }, ], }, @@ -133,6 +154,8 @@ describe('groupMediaItemsByDate', () => { message: { id: 'id', received_at: 1523231940000, + sent_at: 1523231940000, + attachments: [], }, attachment: { @@ -140,6 +163,8 @@ describe('groupMediaItemsByDate', () => { contentType: IMAGE_JPEG, url: 'url', }, + messageSender: generatedMessageSenderKey, + messageTimestamp: generatedMessageTimestamp, }, { objectURL: 'Sun, 01 Apr 2018 00:01:00 GMT', @@ -147,6 +172,8 @@ describe('groupMediaItemsByDate', () => { message: { id: 'id', received_at: 1522540860000, + sent_at: 1522540860000, + attachments: [], }, contentType: IMAGE_JPEG, @@ -155,6 +182,8 @@ describe('groupMediaItemsByDate', () => { contentType: IMAGE_JPEG, url: 'url', }, + messageSender: generatedMessageSenderKey, + messageTimestamp: generatedMessageTimestamp, }, ], }, @@ -170,6 +199,8 @@ describe('groupMediaItemsByDate', () => { message: { id: 'id', received_at: 1522540740000, + sent_at: 1522540740000, + attachments: [], }, attachment: { @@ -177,6 +208,8 @@ describe('groupMediaItemsByDate', () => { contentType: IMAGE_JPEG, url: 'url', }, + messageSender: generatedMessageSenderKey, + messageTimestamp: generatedMessageTimestamp, }, { objectURL: 'Thu, 01 Mar 2018 14:00:00 GMT', @@ -185,6 +218,8 @@ describe('groupMediaItemsByDate', () => { message: { id: 'id', received_at: 1519912800000, + sent_at: 1519912800000, + attachments: [], }, attachment: { @@ -192,6 +227,8 @@ describe('groupMediaItemsByDate', () => { contentType: IMAGE_JPEG, url: 'url', }, + messageSender: generatedMessageSenderKey, + messageTimestamp: generatedMessageTimestamp, }, ], }, @@ -206,6 +243,8 @@ describe('groupMediaItemsByDate', () => { message: { id: 'id', received_at: 1298937540000, + sent_at: 1298937540000, + attachments: [], }, attachment: { @@ -214,6 +253,8 @@ describe('groupMediaItemsByDate', () => { url: 'url', }, contentType: IMAGE_JPEG, + messageSender: generatedMessageSenderKey, + messageTimestamp: generatedMessageTimestamp, }, { objectURL: 'Tue, 01 Feb 2011 10:00:00 GMT', @@ -222,8 +263,12 @@ describe('groupMediaItemsByDate', () => { message: { id: 'id', received_at: 1296554400000, + sent_at: 1296554400000, + attachments: [], }, + messageSender: generatedMessageSenderKey, + messageTimestamp: generatedMessageTimestamp, attachment: { fileName: 'fileName', contentType: IMAGE_JPEG, From b055165a5d1420d6fc9a06f6b0cfb03c7698e95b Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 1 Jun 2021 15:46:29 +1000 Subject: [PATCH 3/3] display a message when the recipient screenshots an attachemnt --- _locales/en/messages.json | 18 +++++++ protos/SignalService.proto | 10 ++-- ts/components/basic/Text.tsx | 35 +++++++++++++ .../DataExtractionNotification.tsx | 39 ++++++++++++++ .../session/SessionClosableOverlay.tsx | 7 +-- .../conversation/SessionMessagesList.tsx | 14 +++++ .../conversation/SessionRightPanel.tsx | 5 +- .../session/registration/SignInTab.tsx | 5 +- .../session/settings/SessionSettings.tsx | 3 +- ts/models/conversation.ts | 3 ++ ts/models/message.ts | 49 +++++++++++++++++ ts/models/messageType.ts | 21 ++++++++ ts/receiver/contentMessage.ts | 52 ++++++++++++++++++- .../DataExtractionNotificationMessage.ts | 9 ++-- ts/session/snode_api/SNodeAPI.ts | 5 +- ts/session/snode_api/snodePool.ts | 8 ++- ts/state/ducks/conversations.ts | 2 +- 17 files changed, 261 insertions(+), 24 deletions(-) create mode 100644 ts/components/conversation/DataExtractionNotification.tsx diff --git a/_locales/en/messages.json b/_locales/en/messages.json index cc1d15d13..7f42594d0 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1859,5 +1859,23 @@ }, "orJoinOneOfThese": { "message": "Or join one of these..." + }, + "tookAScreenshot": { + "message": "$name$ took a screenshot", + "placeholders": { + "name": { + "content": "$1", + "example": "Alice" + } + } + }, + "savedTheFile": { + "message": "Media saved by $name$", + "placeholders": { + "name": { + "content": "$1", + "example": "Alice" + } + } } } diff --git a/protos/SignalService.proto b/protos/SignalService.proto index e05050960..9cd970f19 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -33,11 +33,11 @@ message TypingMessage { message Content { - optional DataMessage dataMessage = 1; - optional ReceiptMessage receiptMessage = 5; - optional TypingMessage typingMessage = 6; - optional ConfigurationMessage configurationMessage = 7; - optional DataExtractionNotification dataExtractionNotification = 82; + optional DataMessage dataMessage = 1; + optional ReceiptMessage receiptMessage = 5; + optional TypingMessage typingMessage = 6; + optional ConfigurationMessage configurationMessage = 7; + optional DataExtractionNotification dataExtractionNotification = 8; } diff --git a/ts/components/basic/Text.tsx b/ts/components/basic/Text.tsx index d1a0904b0..b84083f65 100644 --- a/ts/components/basic/Text.tsx +++ b/ts/components/basic/Text.tsx @@ -29,6 +29,41 @@ export const Text = (props: TextProps) => { return {props.text}; }; +type SpacerProps = { + size: 'lg' | 'md' | 'sm' | 'xs'; + theme?: DefaultTheme; +}; + +const SpacerStyled = styled.div` + height: ${props => + props.size === 'lg' + ? props.theme.common.margins.lg + : props.size === 'md' + ? props.theme.common.margins.md + : props.size === 'sm' + ? props.theme.common.margins.sm + : props.theme.common.margins.xs}; +`; + +const Spacer = (props: SpacerProps) => { + return ; +}; + +export const SpacerLG = () => { + return ; +}; + +export const SpacerMD = () => { + return ; +}; +export const SpacerSM = () => { + return ; +}; + +export const SpacerXS = () => { + return ; +}; + type H3Props = { text: string; opposite?: boolean; diff --git a/ts/components/conversation/DataExtractionNotification.tsx b/ts/components/conversation/DataExtractionNotification.tsx new file mode 100644 index 000000000..f9ce12561 --- /dev/null +++ b/ts/components/conversation/DataExtractionNotification.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { useTheme } from 'styled-components'; +import { DataExtractionNotificationProps } from '../../models/messageType'; +import { SignalService } from '../../protobuf'; +import { Flex } from '../basic/Flex'; +import { SessionIcon, SessionIconSize, SessionIconType } from '../session/icon'; +import { SpacerXS, Text } from '../basic/Text'; + +type Props = DataExtractionNotificationProps; + +export const DataExtractionNotification = (props: Props) => { + const theme = useTheme(); + const { name, type, source } = props; + + let contentText: string; + if (type === SignalService.DataExtractionNotification.Type.MEDIA_SAVED) { + contentText = window.i18n('savedTheFile', name || source); + } else { + contentText = window.i18n('tookAScreenshot', name || source); + } + + return ( + + + + + + ); +}; diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index 9fc9bf7f2..a44514390 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -13,6 +13,7 @@ import { DefaultTheme } from 'styled-components'; import { UserUtils } from '../../session/utils'; import { ConversationTypeEnum } from '../../models/conversation'; import { SessionJoinableRooms } from './SessionJoinableDefaultRooms'; +import { SpacerLG, SpacerMD } from '../basic/Text'; export enum SessionClosableOverlayType { Message = 'message', @@ -165,7 +166,7 @@ export class SessionClosableOverlay extends React.Component { /> -
+

{title}

@@ -201,7 +202,7 @@ export class SessionClosableOverlay extends React.Component { {isClosedGroupView && ( <> -
+
{noContactsForClosedGroup ? (
@@ -214,7 +215,7 @@ export class SessionClosableOverlay extends React.Component { )}
-
+ )} diff --git a/ts/components/session/conversation/SessionMessagesList.tsx b/ts/components/session/conversation/SessionMessagesList.tsx index 8c0875d83..c2e127093 100644 --- a/ts/components/session/conversation/SessionMessagesList.tsx +++ b/ts/components/session/conversation/SessionMessagesList.tsx @@ -20,6 +20,7 @@ import { MessageRegularProps } from '../../../models/messageType'; import { getMessagesBySentAt } from '../../../data/data'; import autoBind from 'auto-bind'; import { ConversationTypeEnum } from '../../../models/conversation'; +import { DataExtractionNotification } from '../../conversation/DataExtractionNotification'; interface State { showScrollButton: boolean; @@ -204,6 +205,7 @@ export class SessionMessagesList extends React.Component { const timerProps = message.propsForTimerNotification; const propsForGroupInvitation = message.propsForGroupInvitation; + const propsForDataExtractionNotification = message.propsForDataExtractionNotification; const groupNotificationProps = message.propsForGroupNotification; @@ -243,6 +245,18 @@ export class SessionMessagesList extends React.Component { ); } + if (propsForDataExtractionNotification) { + return ( + <> + + {unreadIndicator} + + ); + } + if (timerProps) { return ( <> diff --git a/ts/components/session/conversation/SessionRightPanel.tsx b/ts/components/session/conversation/SessionRightPanel.tsx index 44dd4ea9d..a7119594b 100644 --- a/ts/components/session/conversation/SessionRightPanel.tsx +++ b/ts/components/session/conversation/SessionRightPanel.tsx @@ -21,6 +21,7 @@ import { getDecryptedMediaUrl } from '../../../session/crypto/DecryptedAttachmen import { LightBoxOptions } from './SessionConversation'; import { UserUtils } from '../../../session/utils'; import { sendDataExtractionNotification } from '../../../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage'; +import { SpacerLG } from '../../basic/Text'; interface Props { id: string; @@ -270,11 +271,11 @@ class SessionRightPanel extends React.Component {

{name}

{showMemberCount && ( <> -
+
{window.i18n('members', memberCount)}
-
+ )} diff --git a/ts/components/session/registration/SignInTab.tsx b/ts/components/session/registration/SignInTab.tsx index bc9e6673c..40571a2bc 100644 --- a/ts/components/session/registration/SignInTab.tsx +++ b/ts/components/session/registration/SignInTab.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { Flex } from '../../basic/Flex'; +import { SpacerLG } from '../../basic/Text'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../SessionButton'; import { SessionSpinner } from '../SessionSpinner'; import { signInWithLinking, signInWithRecovery, validatePassword } from './RegistrationTabs'; @@ -78,9 +79,9 @@ const SignInButtons = (props: { return (
-
+
{window.i18n('or')}
-
+
); diff --git a/ts/components/session/settings/SessionSettings.tsx b/ts/components/session/settings/SessionSettings.tsx index 691780177..9c2d71789 100644 --- a/ts/components/session/settings/SessionSettings.tsx +++ b/ts/components/session/settings/SessionSettings.tsx @@ -11,6 +11,7 @@ import { ConversationController } from '../../../session/conversations'; import { getConversationLookup, getConversations } from '../../../state/selectors/conversations'; import { connect } from 'react-redux'; import { getPasswordHash } from '../../../../ts/data/data'; +import { SpacerLG } from '../../basic/Text'; export enum SessionSettingCategory { Appearance = 'appearance', @@ -165,7 +166,7 @@ class SettingsViewInner extends React.Component { {this.state.pwdLockError && ( <>
{this.state.pwdLockError}
-
+ )} diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index c4aa02639..1233801d3 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -902,6 +902,9 @@ export class ConversationModel extends Backbone.Model { messageModel: model, }) ); + const unreadCount = await this.getUnreadCount(); + this.set({ unreadCount }); + await this.commit(); return model; } diff --git a/ts/models/message.ts b/ts/models/message.ts index 597f50f73..b54081e0b 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -11,6 +11,8 @@ import { ClosedGroupVisibleMessage } from '../session/messages/outgoing/visibleM import { PubKey } from '../../ts/session/types'; import { UserUtils } from '../../ts/session/utils'; import { + DataExtractionNotificationMsg, + DataExtractionNotificationProps, fillMessageAttributesWithDefaults, MessageAttributes, MessageAttributesOptionals, @@ -37,6 +39,7 @@ export class MessageModel extends Backbone.Model { public propsForTimerNotification: any; public propsForGroupNotification: any; public propsForGroupInvitation: any; + public propsForDataExtractionNotification?: DataExtractionNotificationProps; public propsForSearchResult: any; public propsForMessage: any; @@ -75,6 +78,8 @@ export class MessageModel extends Backbone.Model { this.propsForGroupNotification = this.getPropsForGroupNotification(); } else if (this.isGroupInvitation()) { this.propsForGroupInvitation = this.getPropsForGroupInvitation(); + } else if (this.isDataExtractionNotification()) { + this.propsForDataExtractionNotification = this.getPropsForDataExtractionNotification(); } else { this.propsForSearchResult = this.getPropsForSearchResult(); this.propsForMessage = this.getPropsForMessage(); @@ -193,6 +198,26 @@ export class MessageModel extends Backbone.Model { if (this.isGroupInvitation()) { return `😎 ${window.i18n('openGroupInvitation')}`; } + if (this.isDataExtractionNotification()) { + const dataExtraction = this.get( + 'dataExtractionNotification' + ) as DataExtractionNotificationMsg; + if (dataExtraction.type === SignalService.DataExtractionNotification.Type.SCREENSHOT) { + return window.i18n( + 'tookAScreenshot', + ConversationController.getInstance().getContactProfileNameOrShortenedPubKey( + dataExtraction.source + ) + ); + } + + return window.i18n( + 'savedTheFile', + ConversationController.getInstance().getContactProfileNameOrShortenedPubKey( + dataExtraction.source + ) + ); + } return this.get('body'); } @@ -200,6 +225,10 @@ export class MessageModel extends Backbone.Model { return !!this.get('groupInvitation'); } + public isDataExtractionNotification() { + return !!this.get('dataExtractionNotification'); + } + public getNotificationText() { let description = this.getDescription(); if (description) { @@ -305,6 +334,22 @@ export class MessageModel extends Backbone.Model { }; } + public getPropsForDataExtractionNotification(): DataExtractionNotificationProps | undefined { + const dataExtractionNotification = this.get('dataExtractionNotification'); + + if (!dataExtractionNotification) { + window.log.warn('dataExtractionNotification should not happen'); + return; + } + + const contact = this.findAndFormatContact(dataExtractionNotification.source); + + return { + ...dataExtractionNotification, + name: contact.profileName || contact.name || dataExtractionNotification.source, + }; + } + public findContact(pubkey: string) { return ConversationController.getInstance().get(pubkey); } @@ -414,6 +459,10 @@ export class MessageModel extends Backbone.Model { return null; } + if (this.isDataExtractionNotification()) { + return null; + } + const readBy = this.get('read_by') || []; if (window.storage.get('read-receipt-setting') && readBy.length > 0) { return 'read'; diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts index 0f09aa499..4f723d051 100644 --- a/ts/models/messageType.ts +++ b/ts/models/messageType.ts @@ -98,8 +98,24 @@ export interface MessageAttributes { */ snippet?: any; direction: any; + + /** + * This is used for when a user screenshots or saves an attachment you sent. + * We display a small message just below the message referenced + */ + dataExtractionNotification?: DataExtractionNotificationMsg; +} + +export interface DataExtractionNotificationMsg { + type: number; // screenshot or saving event, based on SignalService.DataExtractionNotification.Type + source: string; // the guy who made a screenshot + referencedAttachmentTimestamp: number; // the attachment timestamp he screenshot } +export type DataExtractionNotificationProps = DataExtractionNotificationMsg & { + name: string; +}; + export interface MessageAttributesOptionals { id?: string; source?: string; @@ -134,6 +150,11 @@ export interface MessageAttributesOptionals { source: string; fromSync?: boolean; }; + dataExtractionNotification?: { + type: number; + source: string; + referencedAttachmentTimestamp: number; + }; unread?: number; group?: any; timestamp?: number; diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 3ed0c5875..5937fd0bc 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -354,8 +354,7 @@ export async function innerHandleContentMessage( return; } if (content.dataExtractionNotification) { - window?.log?.warn('content.dataExtractionNotification', content.dataExtractionNotification); - void handleDataExtractionNotification( + await handleDataExtractionNotification( envelope, content.dataExtractionNotification as SignalService.DataExtractionNotification ); @@ -466,3 +465,52 @@ async function handleTypingMessage( }); } } + +/** + * A DataExtractionNotification message can only come from a 1 o 1 conversation. + * + * We drop them if the convo is not a 1 o 1 conversation. + */ +export async function handleDataExtractionNotification( + envelope: EnvelopePlus, + dataNotificationMessage: SignalService.DataExtractionNotification +): Promise { + // we currently don't care about the timestamp included in the field itself, just the timestamp of the envelope + const { type, timestamp: referencedAttachment } = dataNotificationMessage; + + const { source, timestamp } = envelope; + await removeFromCache(envelope); + + const convo = ConversationController.getInstance().get(source); + if (!convo || !convo.isPrivate()) { + window?.log?.info('Got DataNotification for unknown or non private convo'); + return; + } + + if (!type || !source) { + window?.log?.info('DataNotification pre check failed'); + + return; + } + + if (timestamp) { + const envelopeTimestamp = Lodash.toNumber(timestamp); + const referencedAttachmentTimestamp = Lodash.toNumber(referencedAttachment); + const now = Date.now(); + + await convo.addSingleMessage({ + conversationId: convo.get('id'), + type: 'outgoing', // mark it as outgoing just so it appears below our sent attachment + sent_at: envelopeTimestamp, + received_at: now, + dataExtractionNotification: { + type, + referencedAttachmentTimestamp, // currently unused + source, + }, + unread: 1, // 1 means unread + expireTimer: 0, + }); + convo.updateLastMessage(); + } +} diff --git a/ts/session/messages/outgoing/controlMessage/DataExtractionNotificationMessage.ts b/ts/session/messages/outgoing/controlMessage/DataExtractionNotificationMessage.ts index 69e0c8daa..55fb1eb3b 100644 --- a/ts/session/messages/outgoing/controlMessage/DataExtractionNotificationMessage.ts +++ b/ts/session/messages/outgoing/controlMessage/DataExtractionNotificationMessage.ts @@ -33,11 +33,10 @@ export class DataExtractionNotificationMessage extends ContentMessage { const action = ACTION_ENUM.MEDIA_SAVED; // we cannot know when user screenshots, so it can only be a media saved - const dataExtraction = new SignalService.DataExtractionNotification(); - dataExtraction.type = action; - dataExtraction.timestamp = this.referencedAttachmentTimestamp; - - return dataExtraction; + return new SignalService.DataExtractionNotification({ + type: action, + timestamp: this.referencedAttachmentTimestamp, + }); } } diff --git a/ts/session/snode_api/SNodeAPI.ts b/ts/session/snode_api/SNodeAPI.ts index 0ccda7a41..177767af5 100644 --- a/ts/session/snode_api/SNodeAPI.ts +++ b/ts/session/snode_api/SNodeAPI.ts @@ -413,7 +413,10 @@ export async function retrieveNextMessages( return []; } } catch (e) { - window?.log?.warn('Got an error while retrieving next messages:', e); + window?.log?.warn( + 'Got an error while retrieving next messages. Not retrying as we trigger fetch often:', + e + ); return []; } } diff --git a/ts/session/snode_api/snodePool.ts b/ts/session/snode_api/snodePool.ts index 5ec390b34..073e12143 100644 --- a/ts/session/snode_api/snodePool.ts +++ b/ts/session/snode_api/snodePool.ts @@ -198,14 +198,18 @@ async function getSnodeListFromLokidSeednode( window?.log?.warn('loki_snode_api::getSnodeListFromLokidSeednode - error', e.code, e.message); // handle retries in case of temporary hiccups if (retries < SEED_NODE_RETRIES) { - setTimeout(() => { + setTimeout(async () => { window?.log?.info( 'loki_snode_api::getSnodeListFromLokidSeednode - Retrying initialising random snode pool, try #', retries, 'seed nodes total', seedNodes.length ); - void getSnodeListFromLokidSeednode(seedNodes, retries + 1); + try { + await getSnodeListFromLokidSeednode(seedNodes, retries + 1); + } catch (e) { + window?.log?.warn('getSnodeListFromLokidSeednode failed retr y #', retries, e); + } }, retries * retries * 5000); } else { window?.log?.error('loki_snode_api::getSnodeListFromLokidSeednode - failing'); diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 076a7820c..01bdcea0e 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -427,8 +427,8 @@ const toPickFromMessageModel = [ 'firstMessageOfSeries', 'propsForGroupInvitation', 'propsForTimerNotification', - 'propsForVerificationNotification', 'propsForGroupNotification', + 'propsForDataExtractionNotification', // FIXME below are what is needed to fetch on the fly messageDetails. This is not the react way 'getPropsForMessageDetail', 'get',