Merge pull request #1672 from Bilb/data-extraction-notification

Data extraction notification
pull/1682/head
Audric Ackermann 4 years ago committed by GitHub
commit 5fc0849395
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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"
}
}
}
}

@ -37,6 +37,8 @@ message Content {
optional ReceiptMessage receiptMessage = 5;
optional TypingMessage typingMessage = 6;
optional ConfigurationMessage configurationMessage = 7;
optional DataExtractionNotification dataExtractionNotification = 8;
}
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 {

@ -18,12 +18,20 @@ export interface MediaItemType {
index: number;
attachment: AttachmentType;
message: Message;
messageTimestamp: number;
messageSender: string;
}
type Props = {
close: () => void;
media: Array<MediaItemType>;
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,
});
};

@ -29,6 +29,41 @@ export const Text = (props: TextProps) => {
return <StyledDefaultText {...props}>{props.text}</StyledDefaultText>;
};
type SpacerProps = {
size: 'lg' | 'md' | 'sm' | 'xs';
theme?: DefaultTheme;
};
const SpacerStyled = styled.div<SpacerProps>`
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 <SpacerStyled {...props} />;
};
export const SpacerLG = () => {
return <Spacer size="lg" />;
};
export const SpacerMD = () => {
return <Spacer size="md" />;
};
export const SpacerSM = () => {
return <Spacer size="sm" />;
};
export const SpacerXS = () => {
return <Spacer size="xs" />;
};
type H3Props = {
text: string;
opposite?: boolean;

@ -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 (
<Flex
container={true}
flexDirection="column"
alignItems="center"
margin={theme.common.margins.sm}
>
<SessionIcon
iconType={SessionIconType.Upload}
theme={theme}
iconSize={SessionIconSize.Small}
iconRotation={180}
/>
<SpacerXS />
<Text text={contentText} subtle={true} />
</Flex>
);
};

@ -4,4 +4,5 @@ export type Message = {
id: string;
attachments: Array<Attachment>;
received_at: number;
sent_at: number;
};

@ -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<Props, State> {
/>
</div>
<div className="spacer-md" />
<SpacerMD />
<h2>{title}</h2>
@ -201,7 +202,7 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
{isClosedGroupView && (
<>
<div className="spacer-lg" />
<SpacerLG />
<div className="group-member-list__container">
{noContactsForClosedGroup ? (
<div className="group-member-list__no-contacts">
@ -214,7 +215,7 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
)}
</div>
<div className="spacer-lg" />
<SpacerLG />
</>
)}

@ -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<MediaItemType>;
attachment: any;
}
interface Props {
@ -520,7 +526,7 @@ export class SessionConversation extends React.Component<Props, State> {
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<Props, State> {
}
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<Props, State> {
});
}
private renderLightBox({ media, attachment }: { media: Array<MediaItemType>; 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<Props, State> {
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<Props, State> {
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<File>) {

@ -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;
@ -45,7 +46,14 @@ interface Props {
replyToMessage: (messageId: number) => Promise<void>;
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<void>;
}
@ -197,6 +205,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
const timerProps = message.propsForTimerNotification;
const propsForGroupInvitation = message.propsForGroupInvitation;
const propsForDataExtractionNotification = message.propsForDataExtractionNotification;
const groupNotificationProps = message.propsForGroupNotification;
@ -236,6 +245,18 @@ export class SessionMessagesList extends React.Component<Props, State> {
);
}
if (propsForDataExtractionNotification) {
return (
<>
<DataExtractionNotification
{...propsForDataExtractionNotification}
key={message.id}
/>
{unreadIndicator}
</>
);
}
if (timerProps) {
return (
<>
@ -305,7 +326,11 @@ export class SessionMessagesList extends React.Component<Props, State> {
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;

@ -18,6 +18,10 @@ 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';
import { SpacerLG } from '../../basic/Text';
interface Props {
id: string;
@ -44,7 +48,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 +173,7 @@ class SessionRightPanel extends React.Component<Props, State> {
});
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 +181,7 @@ class SessionRightPanel extends React.Component<Props, State> {
getAbsolutePath: window.Signal.Migrations.getAbsoluteAttachmentPath,
timestamp,
});
await sendDataExtractionNotification(this.props.id, message?.source, timestamp);
};
const onItemClick = ({ message, attachment, type }: any) => {
@ -187,11 +192,17 @@ class SessionRightPanel extends React.Component<Props, State> {
}
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 +219,8 @@ class SessionRightPanel extends React.Component<Props, State> {
};
}
public onShowLightBox(options: any) {
this.props.onShowLightBox(options);
public onShowLightBox(lightboxOptions: LightBoxOptions) {
this.props.onShowLightBox(lightboxOptions);
}
// tslint:disable-next-line: cyclomatic-complexity
@ -260,11 +271,11 @@ class SessionRightPanel extends React.Component<Props, State> {
<h2>{name}</h2>
{showMemberCount && (
<>
<div className="spacer-lg" />
<SpacerLG />
<div role="button" className="subtle">
{window.i18n('members', memberCount)}
</div>
<div className="spacer-lg" />
<SpacerLG />
</>
)}
<input className="description" placeholder={window.i18n('description')} />

@ -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<StyledSvgProps>`
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 (
<Svg {...propsToPick}>

@ -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 (
<div>
<RestoreUsingRecoveryPhraseButton onRecoveryButtonClicked={props.onRecoveryButtonClicked} />
<div className="spacer-lg" />
<SpacerLG />
<div className="or">{window.i18n('or')}</div>
<div className="spacer-lg" />
<SpacerLG />
<LinkDeviceButton onLinkDeviceButtonClicked={props.onLinkDeviceButtonClicked} />
</div>
);

@ -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<SettingsViewProps, State> {
{this.state.pwdLockError && (
<>
<div className="session-label warning">{this.state.pwdLockError}</div>
<div className="spacer-lg" />
<SpacerLG />
</>
)}

@ -902,6 +902,9 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
messageModel: model,
})
);
const unreadCount = await this.getUnreadCount();
this.set({ unreadCount });
await this.commit();
return model;
}

@ -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,
@ -30,13 +32,14 @@ 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<MessageAttributes> {
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<MessageAttributes> {
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<MessageAttributes> {
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<MessageAttributes> {
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<MessageAttributes> {
};
}
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<MessageAttributes> {
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';

@ -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;

@ -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,13 @@ export async function innerHandleContentMessage(
);
return;
}
if (content.dataExtractionNotification) {
await handleDataExtractionNotification(
envelope,
content.dataExtractionNotification as SignalService.DataExtractionNotification
);
return;
}
} catch (e) {
window?.log?.warn(e);
}
@ -422,17 +429,18 @@ async function handleReceiptMessage(
async function handleTypingMessage(
envelope: EnvelopePlus,
iTypingMessage: SignalService.ITypingMessage
typingMessage: SignalService.TypingMessage
): Promise<void> {
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 +453,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);
@ -462,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<void> {
// 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();
}
}

@ -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';

@ -0,0 +1,78 @@
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
return new SignalService.DataExtractionNotification({
type: action,
timestamp: this.referencedAttachmentTimestamp,
});
}
}
/**
* 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);
}
};

@ -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 [];
}
}

@ -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');

@ -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',

@ -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,

Loading…
Cancel
Save