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