From b055165a5d1420d6fc9a06f6b0cfb03c7698e95b Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 1 Jun 2021 15:46:29 +1000 Subject: [PATCH] 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',