diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index bd7e2f439..100ea14de 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -114,6 +114,7 @@
"clear": "Clear",
"clearAllData": "Clear All Data",
"deleteAccountWarning": "This will permanently delete your messages and contacts.",
+ "deleteAccountFromLogin": "Are you sure you want to clear your device?",
"deleteContactConfirmation": "Are you sure you want to delete this conversation?",
"quoteThumbnailAlt": "Thumbnail of image from quoted message",
"imageAttachmentAlt": "Image attached to message",
@@ -266,16 +267,16 @@
"updateGroupDialogTitle": "Updating $name$...",
"showRecoveryPhrase": "Recovery Phrase",
"yourSessionID": "Your Session ID",
- "setAccountPasswordTitle": "Set Account Password",
+ "setAccountPasswordTitle": "Password",
"setAccountPasswordDescription": "Require password to unlock Session.",
- "changeAccountPasswordTitle": "Change Account Password",
+ "changeAccountPasswordTitle": "Change Password",
"changeAccountPasswordDescription": "Change the password required to unlock Session.",
- "removeAccountPasswordTitle": "Remove Account Password",
+ "removeAccountPasswordTitle": "Remove Password",
"removeAccountPasswordDescription": "Remove the password required to unlock Session.",
"enterPassword": "Please enter your password",
"confirmPassword": "Confirm password",
"enterNewPassword": "Please enter your new password",
- "confirmNewPassword": "Confirm new password",
+ "confirmNewPassword": "Confirm password",
"showRecoveryPhrasePasswordRequest": "Please enter your password",
"recoveryPhraseSavePromptMain": "Your recovery phrase is the master key to your Session ID — you can use it to restore your Session ID if you lose access to your device. Store your recovery phrase in a safe place, and don't give it to anyone.",
"invalidOpenGroupUrl": "Invalid URL",
@@ -295,12 +296,12 @@
"setPasswordInvalid": "Passwords do not match",
"changePasswordInvalid": "The old password you entered is incorrect",
"removePasswordInvalid": "Incorrect password",
- "setPasswordTitle": "Set Password",
- "changePasswordTitle": "Changed Password",
- "removePasswordTitle": "Removed Password",
+ "setPasswordTitle": "Password Set",
+ "changePasswordTitle": "Password Changed",
+ "removePasswordTitle": "Password Removed",
"setPasswordToastDescription": "Your password has been set. Please keep it safe.",
"changePasswordToastDescription": "Your password has been changed. Please keep it safe.",
- "removePasswordToastDescription": "You have removed your password.",
+ "removePasswordToastDescription": "Your password has been removed.",
"publicChatExists": "You are already connected to this community",
"connectToServerFail": "Couldn't join community",
"connectingToServer": "Connecting...",
@@ -414,6 +415,9 @@
"dialogClearAllDataDeletionFailedTitleQuestion": "Do you want to delete data from just this device?",
"dialogClearAllDataDeletionFailedMultiple": "Data not deleted by those Service Nodes: $snodes$",
"dialogClearAllDataDeletionQuestion": "Would you like to clear this device only, or delete your data from the network as well?",
+ "clearDevice": "Clear Device",
+ "tryAgain": "Try Again",
+ "areYouSureClearDevice": "Are you sure you want to clear your device?",
"deviceOnly": "Clear Device Only",
"entireAccount": "Clear Device and Network",
"areYouSureDeleteDeviceOnly": "Are you sure you want to delete your device data only?",
diff --git a/protos/SignalService.proto b/protos/SignalService.proto
index 7527f6b54..d94cbdfc5 100644
--- a/protos/SignalService.proto
+++ b/protos/SignalService.proto
@@ -37,7 +37,9 @@ message Unsend {
message MessageRequestResponse {
// @required
- required bool isApproved = 1;
+ required bool isApproved = 1;
+ optional bytes profileKey = 2;
+ optional DataMessage.LokiProfile profile = 3;
}
message Content {
diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss
index 808a6fade..0937c3118 100644
--- a/stylesheets/_modules.scss
+++ b/stylesheets/_modules.scss
@@ -172,13 +172,17 @@
.module-image {
margin: -1px;
}
+
+ &__text {
+ padding-block: var(--margins-sm);
+ }
}
.module-message__link-preview__content {
padding: 0 0 var(--margins-xs) 0;
display: flex;
flex-direction: row;
- align-items: flex-start;
+ align-items: center;
flex-grow: 1;
margin-left: var(--margins-sm);
}
diff --git a/stylesheets/_quote.scss b/stylesheets/_quote.scss
index 51a23f394..fee3d1048 100644
--- a/stylesheets/_quote.scss
+++ b/stylesheets/_quote.scss
@@ -221,7 +221,7 @@
}
.module-quote-container {
- margin-bottom: 5px;
+ margin-bottom: var(--margins-xs);
margin-top: var(--margins-xs);
min-width: 300px; // if the quoted content is small it doesn't look very good so we set a minimum
padding-right: var(--margins-xs);
diff --git a/stylesheets/_session_password.scss b/stylesheets/_session_password.scss
index 854b2fbbb..9cf738cb7 100644
--- a/stylesheets/_session_password.scss
+++ b/stylesheets/_session_password.scss
@@ -1,8 +1,16 @@
.password {
height: 100vh;
+ color: var(--white-color); //TODO theming update
.clear-data-wrapper {
- margin: auto;
+ display: flex;
+ height: 100%;
+ width: 100%;
+ background-color: var(--black-color);
+
+ .clear-data-container {
+ margin: auto;
+ }
.warning-info-area {
display: flex;
diff --git a/stylesheets/_session_theme.scss b/stylesheets/_session_theme.scss
index 9e368a6d2..1bc8d3aaf 100644
--- a/stylesheets/_session_theme.scss
+++ b/stylesheets/_session_theme.scss
@@ -33,6 +33,9 @@
padding: var(--padding-message-content);
border-radius: var(--border-radius-message-box);
+ padding: var(--padding-message-content);
+ border-radius: var(--border-radius-message-box);
+
a {
text-decoration: underline;
}
diff --git a/ts/components/SessionPasswordPrompt.tsx b/ts/components/SessionPasswordPrompt.tsx
index 74f18dd7b..cffa02d2f 100644
--- a/ts/components/SessionPasswordPrompt.tsx
+++ b/ts/components/SessionPasswordPrompt.tsx
@@ -8,15 +8,18 @@ import { SessionSpinner } from './basic/SessionSpinner';
import { SessionTheme } from '../themes/SessionTheme';
import { switchThemeTo } from '../session/utils/Theme';
import styled from 'styled-components';
+import { ToastUtils } from '../session/utils';
+import { isString } from 'lodash';
+import { SessionToastContainer } from './SessionToastContainer';
interface State {
- error: string;
errorCount: number;
clearDataView: boolean;
loading: boolean;
}
export const MAX_LOGIN_TRIES = 3;
+// tslint:disable: use-simple-attributes
const TextPleaseWait = (props: { isLoading: boolean }) => {
if (!props.isLoading) {
@@ -38,7 +41,6 @@ class SessionPasswordPromptInner extends React.PureComponent<{}, State> {
super(props);
this.state = {
- error: '',
errorCount: 0,
clearDataView: false,
loading: false,
@@ -54,7 +56,6 @@ class SessionPasswordPromptInner extends React.PureComponent<{}, State> {
}
public render() {
- const showResetElements = this.state.errorCount >= MAX_LOGIN_TRIES;
const isLoading = this.state.loading;
const wrapperClass = this.state.clearDataView
@@ -65,13 +66,13 @@ class SessionPasswordPromptInner extends React.PureComponent<{}, State> {
: 'password-prompt-container';
const infoAreaClass = this.state.clearDataView ? 'warning-info-area' : 'password-info-area';
const infoTitle = this.state.clearDataView
- ? window.i18n('clearAllData')
+ ? window.i18n('clearDevice')
: window.i18n('passwordViewTitle');
const buttonGroup = this.state.clearDataView
? this.renderClearDataViewButtons()
: this.renderPasswordViewButtons();
const featureElement = this.state.clearDataView ? (
-
{window.i18n('deleteAccountWarning')}
+ {window.i18n('deleteAccountFromLogin')}
) : (
{
const infoIcon = this.state.clearDataView ?? (
);
- const errorSection = !this.state.clearDataView && (
-
- {this.state.error && (
- <>
- {showResetElements ? (
-
{window.i18n('maxPasswordAttempts')}
- ) : (
-
{this.state.error}
- )}
- >
- )}
-
- );
+
const spinner = isLoading ? : null;
return (
@@ -114,7 +103,6 @@ class SessionPasswordPromptInner extends React.PureComponent<{}, State> {
{spinner || featureElement}
- {errorSection}
{buttonGroup}
@@ -143,7 +131,12 @@ class SessionPasswordPromptInner extends React.PureComponent<{}, State> {
errorCount: this.state.errorCount + 1,
});
- this.setState({ error });
+ if (error && isString(error)) {
+ ToastUtils.pushToastError('onLogin', error);
+ } else if (error?.message && isString(error.message)) {
+ ToastUtils.pushToastError('onLogin', error.message);
+ }
+
global.setTimeout(() => {
document.getElementById('password-prompt-input')?.focus();
}, 50);
@@ -166,7 +159,6 @@ class SessionPasswordPromptInner extends React.PureComponent<{}, State> {
private initClearDataView() {
this.setState({
- error: '',
errorCount: 0,
clearDataView: true,
});
@@ -180,7 +172,7 @@ class SessionPasswordPromptInner extends React.PureComponent<{}, State> {
{showResetElements && (
<>
{
)}
{/* TODO Theming - Fix */}
@@ -201,7 +193,7 @@ class SessionPasswordPromptInner extends React.PureComponent<{}, State> {
return (
{
return (
+
diff --git a/ts/components/conversation/SessionConversation.tsx b/ts/components/conversation/SessionConversation.tsx
index 97ae138db..c202e0c79 100644
--- a/ts/components/conversation/SessionConversation.tsx
+++ b/ts/components/conversation/SessionConversation.tsx
@@ -54,6 +54,8 @@ import { ConversationMessageRequestButtons } from './ConversationRequestButtons'
import { ConversationRequestinfo } from './ConversationRequestInfo';
import { getCurrentRecoveryPhrase } from '../../util/storage';
import loadImage from 'blueimp-load-image';
+import { markAllReadByConvoId } from '../../interactions/conversationInteractions';
+
import { SessionSpinner } from '../basic/SessionSpinner';
import styled from 'styled-components';
// tslint:disable: jsx-curly-spacing
@@ -307,17 +309,18 @@ export class SessionConversation extends React.Component {
}
private async scrollToNow() {
- if (!this.props.selectedConversationKey) {
+ const conversationKey = this.props.selectedConversationKey;
+ if (!conversationKey) {
return;
}
- const mostNowMessage = await Data.getLastMessageInConversation(
- this.props.selectedConversationKey
- );
- if (mostNowMessage) {
+ await markAllReadByConvoId(conversationKey);
+ const mostRecentMessage = await Data.getLastMessageInConversation(conversationKey);
+
+ if (mostRecentMessage) {
await openConversationToSpecificMessage({
- conversationKey: this.props.selectedConversationKey,
- messageIdToNavigateTo: mostNowMessage.id,
+ conversationKey,
+ messageIdToNavigateTo: mostRecentMessage.id,
shouldHighlightMessage: false,
});
const messageContainer = this.messageContainerRef.current;
diff --git a/ts/components/conversation/message/message-content/MessageAuthorText.tsx b/ts/components/conversation/message/message-content/MessageAuthorText.tsx
index 4f3c6f3b1..bacf3f0ee 100644
--- a/ts/components/conversation/message/message-content/MessageAuthorText.tsx
+++ b/ts/components/conversation/message/message-content/MessageAuthorText.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import { useSelector } from 'react-redux';
+import styled from 'styled-components';
import { MessageRenderingProps } from '../../../../models/messageType';
import { PubKey } from '../../../../session/types';
import {
@@ -19,6 +20,11 @@ type Props = {
messageId: string;
};
+const StyledAuthorContainer = styled(Flex)`
+ /* TODO Theming - Verify */
+ color: var(--text-primary-color);
+`;
+
export const MessageAuthorText = (props: Props) => {
const selected = useSelector(state => getMessageAuthorProps(state as any, props.messageId));
@@ -38,7 +44,7 @@ export const MessageAuthorText = (props: Props) => {
const displayedPubkey = authorProfileName ? PubKey.shorten(sender) : sender;
return (
-
+
{
boldProfileName={true}
shouldShowPubkey={Boolean(isPublic)}
/>
-
+
);
};
diff --git a/ts/components/conversation/message/message-content/MessageContent.tsx b/ts/components/conversation/message/message-content/MessageContent.tsx
index e4f1b2a22..28ad3d166 100644
--- a/ts/components/conversation/message/message-content/MessageContent.tsx
+++ b/ts/components/conversation/message/message-content/MessageContent.tsx
@@ -88,6 +88,7 @@ const StyledMessageOpaqueContent = styled.div<{
`;
export const IsMessageVisibleContext = createContext(false);
+// tslint:disable: use-simple-attributes
export const MessageContent = (props: Props) => {
const [highlight, setHighlight] = useState(false);
diff --git a/ts/components/conversation/message/message-item/ReadableMessage.tsx b/ts/components/conversation/message/message-item/ReadableMessage.tsx
index bcaaeb966..a57cead51 100644
--- a/ts/components/conversation/message/message-item/ReadableMessage.tsx
+++ b/ts/components/conversation/message/message-item/ReadableMessage.tsx
@@ -144,17 +144,17 @@ export const ReadableMessage = (props: ReadableMessageProps) => {
const found = await Data.getMessageById(messageId);
if (found && Boolean(found.get('unread'))) {
- const foundReceivedAt = found.get('received_at');
+ const foundSentAt = found.get('sent_at');
// mark the message as read.
// this will trigger the expire timer.
await found.markRead(Date.now());
// we should stack those and send them in a single message once every 5secs or something.
// this would be part of an redesign of the sending pipeline
- if (foundReceivedAt) {
+ if (foundSentAt && selectedConversationKey) {
void getConversationController()
- .get(found.id)
- ?.sendReadReceiptsIfNeeded([foundReceivedAt]);
+ .get(selectedConversationKey)
+ ?.sendReadReceiptsIfNeeded([foundSentAt]);
}
}
}
diff --git a/ts/components/dialog/SessionPasswordDialog.tsx b/ts/components/dialog/SessionPasswordDialog.tsx
index 39c0a33fe..082c655a2 100644
--- a/ts/components/dialog/SessionPasswordDialog.tsx
+++ b/ts/components/dialog/SessionPasswordDialog.tsx
@@ -114,7 +114,7 @@ export class SessionPasswordDialog extends React.Component {
Promise;
}) => {
return (
-
{window.i18n('password')}
+
{window.i18n('passwordViewTitle')}
- {pwdLockError &&
{pwdLockError}
}
-
@@ -210,9 +203,6 @@ export class SessionSettingsView extends React.Component
{shouldRenderPasswordLock ? (
-
+
) : (
> {
+ const messagesIds = await channels.markAllAsReadByConversationNoExpiration(conversationId);
+ return messagesIds;
+}
+
// might throw
async function getUnreadCountByConversation(conversationId: string): Promise {
return channels.getUnreadCountByConversation(conversationId);
diff --git a/ts/data/dataInit.ts b/ts/data/dataInit.ts
index 8c9b3903f..1bd0f40b7 100644
--- a/ts/data/dataInit.ts
+++ b/ts/data/dataInit.ts
@@ -40,6 +40,7 @@ const channelsToMake = new Set([
'removeMessage',
'_removeMessages',
'getUnreadByConversation',
+ 'markAllAsReadByConversationNoExpiration',
'getUnreadCountByConversation',
'getMessageCountByType',
'removeAllMessagesInConversation',
diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts
index 859c42036..1455c01bb 100644
--- a/ts/interactions/conversationInteractions.ts
+++ b/ts/interactions/conversationInteractions.ts
@@ -284,7 +284,8 @@ export async function markAllReadByConvoId(conversationId: string) {
const conversation = getConversationController().get(conversationId);
perfStart(`markAllReadByConvoId-${conversationId}`);
- await conversation.markReadBouncy(Date.now());
+ await conversation?.markAllAsRead();
+
perfEnd(`markAllReadByConvoId-${conversationId}`, 'markAllReadByConvoId');
}
diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts
index 27658b0f0..fe3942186 100644
--- a/ts/models/conversation.ts
+++ b/ts/models/conversation.ts
@@ -25,6 +25,7 @@ import { SignalService } from '../protobuf';
import { MessageModel, sliceQuoteText } from './message';
import { MessageAttributesOptionals, MessageDirection } from './messageType';
import autoBind from 'auto-bind';
+
import { Data } from '../../ts/data/data';
import { toHex } from '../session/utils/String';
import {
@@ -925,7 +926,7 @@ export class ConversationModel extends Backbone.Model {
const messageRequestResponseParams: MessageRequestResponseParams = {
timestamp,
- // lokiProfile: UserUtils.getOurProfile(), // we can't curently include our profile in that response
+ lokiProfile: UserUtils.getOurProfile(),
};
const messageRequestResponse = new MessageRequestResponse(messageRequestResponseParams);
@@ -1223,15 +1224,49 @@ export class ConversationModel extends Backbone.Model {
}
}
+ /**
+ * Mark everything as read efficiently if possible.
+ *
+ * For convos with a expiration timer enable, start the timer as of now.
+ * Send read receipt if needed.
+ */
+ public async markAllAsRead() {
+ if (this.isOpenGroupV2()) {
+ // for opengroups, we batch everything as there is no expiration timer to take care (and potentially a lot of messages)
+
+ await Data.markAllAsReadByConversationNoExpiration(this.id);
+ this.set({ mentionedUs: false, unreadCount: 0 });
+
+ await this.commit();
+ return;
+ }
+
+ // if the conversation has no expiration timer, we can also batch everything, but we also need to send read receipts potentially
+ // so we grab them from the db
+ if (!this.get('expireTimer')) {
+ const allReadMessages = await Data.markAllAsReadByConversationNoExpiration(this.id);
+ this.set({ mentionedUs: false, unreadCount: 0 });
+ await this.commit();
+ if (allReadMessages.length) {
+ await this.sendReadReceiptsIfNeeded(uniq(allReadMessages));
+ }
+ return;
+ }
+ await this.markReadBouncy(Date.now());
+ }
+
// tslint:disable-next-line: cyclomatic-complexity
- public async markReadBouncy(newestUnreadDate: number, providedOptions: any = {}) {
+ public async markReadBouncy(
+ newestUnreadDate: number,
+ providedOptions: { sendReadReceipts?: boolean; readAt?: number } = {}
+ ) {
const lastReadTimestamp = this.lastReadTimestamp;
if (newestUnreadDate < lastReadTimestamp) {
return;
}
- const options = providedOptions || {};
- defaults(options, { sendReadReceipts: true });
+ const readAt = providedOptions?.readAt || Date.now();
+ const sendReadReceipts = providedOptions?.sendReadReceipts || true;
const conversationId = this.id;
Notifications.clearByConversationID(conversationId);
@@ -1245,7 +1280,7 @@ export class ConversationModel extends Backbone.Model {
// Build the list of updated message models so we can mark them all as read on a single sqlite call
for (const nowRead of oldUnreadNowRead) {
- nowRead.markReadNoCommit(options.readAt);
+ nowRead.markReadNoCommit(readAt);
const errors = nowRead.get('errors');
read.push({
@@ -1307,7 +1342,7 @@ export class ConversationModel extends Backbone.Model {
// conversation is viewed, another error message shows up for the contact
read = read.filter(item => !item.hasErrors);
- if (read.length && options.sendReadReceipts) {
+ if (read.length && sendReadReceipts) {
const timestamps = map(read, 'timestamp').filter(t => !!t) as Array;
await this.sendReadReceiptsIfNeeded(timestamps);
}
diff --git a/ts/node/sql.ts b/ts/node/sql.ts
index e71b1da05..f7270e7f7 100644
--- a/ts/node/sql.ts
+++ b/ts/node/sql.ts
@@ -6,6 +6,7 @@ import { app, clipboard, dialog, Notification } from 'electron';
import {
chunk,
+ compact,
difference,
forEach,
fromPairs,
@@ -1112,6 +1113,38 @@ function getUnreadByConversation(conversationId: string) {
return map(rows, row => jsonToObject(row.json));
}
+/**
+ * Warning: This does not start expiration timer
+ */
+function markAllAsReadByConversationNoExpiration(
+ conversationId: string
+): Array<{ id: string; timestamp: number }> {
+ const messagesUnreadBefore = assertGlobalInstance()
+ .prepare(
+ `SELECT json FROM ${MESSAGES_TABLE} WHERE
+ unread = $unread AND
+ conversationId = $conversationId;`
+ )
+ .all({
+ unread: 1,
+ conversationId,
+ });
+
+ assertGlobalInstance()
+ .prepare(
+ `UPDATE ${MESSAGES_TABLE} SET
+ unread = 0, json = json_set(json, '$.unread', 0)
+ WHERE unread = $unread AND
+ conversationId = $conversationId;`
+ )
+ .run({
+ unread: 1,
+ conversationId,
+ });
+
+ return compact(messagesUnreadBefore.map(row => jsonToObject(row.json).sent_at));
+}
+
function getUnreadCountByConversation(conversationId: string) {
const row = assertGlobalInstance()
.prepare(
@@ -1346,7 +1379,7 @@ function getFirstUnreadMessageWithMention(
function getMessagesBySentAt(sentAt: number) {
const rows = assertGlobalInstance()
.prepare(
- `SELECT * FROM ${MESSAGES_TABLE}
+ `SELECT json FROM ${MESSAGES_TABLE}
WHERE sent_at = $sent_at
ORDER BY received_at DESC;`
)
@@ -2403,6 +2436,7 @@ export const sqlNode = {
saveMessages,
removeMessage,
getUnreadByConversation,
+ markAllAsReadByConversationNoExpiration,
getUnreadCountByConversation,
getMessageCountByType,
diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts
index 3511e3c28..24724c350 100644
--- a/ts/receiver/contentMessage.ts
+++ b/ts/receiver/contentMessage.ts
@@ -27,6 +27,7 @@ import {
} from '../interactions/conversations/unsendingInteractions';
import { ConversationTypeEnum } from '../models/conversationAttributes';
import { findCachedBlindedMatchOrLookupOnAllServers } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys';
+import { appendFetchAvatarAndProfileJob } from './userProfileImageUpdates';
export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageHash: string) {
try {
@@ -605,6 +606,11 @@ async function handleMessageRequestResponse(
messageRequestResponse: SignalService.MessageRequestResponse
) {
const { isApproved } = messageRequestResponse;
+ if (!isApproved) {
+ window?.log?.error('handleMessageRequestResponse: isApproved is false -- dropping message.');
+ await removeFromCache(envelope);
+ return;
+ }
if (!messageRequestResponse) {
window?.log?.error('handleMessageRequestResponse: Invalid parameters -- dropping message.');
await removeFromCache(envelope);
@@ -675,6 +681,14 @@ async function handleMessageRequestResponse(
}
}
+ if (messageRequestResponse.profile && !isEmpty(messageRequestResponse.profile)) {
+ void appendFetchAvatarAndProfileJob(
+ conversationToApprove,
+ messageRequestResponse.profile,
+ messageRequestResponse.profileKey
+ );
+ }
+
if (!conversationToApprove || conversationToApprove.didApproveMe() === isApproved) {
if (conversationToApprove) {
await conversationToApprove.commit();
diff --git a/ts/session/messages/outgoing/controlMessage/MessageRequestResponse.ts b/ts/session/messages/outgoing/controlMessage/MessageRequestResponse.ts
index 95bde6a94..dcedf5269 100644
--- a/ts/session/messages/outgoing/controlMessage/MessageRequestResponse.ts
+++ b/ts/session/messages/outgoing/controlMessage/MessageRequestResponse.ts
@@ -1,18 +1,28 @@
import { SignalService } from '../../../../protobuf';
+import { LokiProfile } from '../../../../types/Message';
import { ContentMessage } from '../ContentMessage';
import { MessageParams } from '../Message';
+import { buildProfileForOutgoingMessage } from '../visibleMessage/VisibleMessage';
// tslint:disable-next-line: no-empty-interface
-export interface MessageRequestResponseParams extends MessageParams {}
+export interface MessageRequestResponseParams extends MessageParams {
+ lokiProfile?: LokiProfile;
+}
export class MessageRequestResponse extends ContentMessage {
// we actually send a response only if it is an accept
// private readonly isApproved: boolean;
+ private readonly profileKey?: Uint8Array;
+ private readonly profile?: SignalService.DataMessage.ILokiProfile;
constructor(params: MessageRequestResponseParams) {
super({
timestamp: params.timestamp,
} as MessageRequestResponseParams);
+
+ const profile = buildProfileForOutgoingMessage(params);
+ this.profile = profile.lokiProfile;
+ this.profileKey = profile.profileKey;
}
public contentProto(): SignalService.Content {
@@ -24,6 +34,8 @@ export class MessageRequestResponse extends ContentMessage {
public messageRequestResponseProto(): SignalService.MessageRequestResponse {
return new SignalService.MessageRequestResponse({
isApproved: true,
+ profileKey: this.profileKey?.length ? this.profileKey : undefined,
+ profile: this.profile,
});
}
}
diff --git a/ts/session/messages/outgoing/visibleMessage/VisibleMessage.ts b/ts/session/messages/outgoing/visibleMessage/VisibleMessage.ts
index c5fe43d0c..43e718dc5 100644
--- a/ts/session/messages/outgoing/visibleMessage/VisibleMessage.ts
+++ b/ts/session/messages/outgoing/visibleMessage/VisibleMessage.ts
@@ -1,4 +1,5 @@
import ByteBuffer from 'bytebuffer';
+import { isEmpty } from 'lodash';
import { DataMessage } from '..';
import { SignalService } from '../../../../protobuf';
import { LokiProfile } from '../../../../types/Message';
@@ -80,8 +81,7 @@ export class VisibleMessage extends DataMessage {
private readonly body?: string;
private readonly quote?: Quote;
private readonly profileKey?: Uint8Array;
- private readonly displayName?: string;
- private readonly avatarPointer?: string;
+ private readonly profile?: SignalService.DataMessage.ILokiProfile;
private readonly preview?: Array;
/// In the case of a sync message, the public key of the person the message was targeted at.
@@ -94,21 +94,12 @@ export class VisibleMessage extends DataMessage {
this.body = params.body;
this.quote = params.quote;
this.expireTimer = params.expireTimer;
- if (params.lokiProfile && params.lokiProfile.profileKey) {
- if (
- params.lokiProfile.profileKey instanceof Uint8Array ||
- (params.lokiProfile.profileKey as any) instanceof ByteBuffer
- ) {
- this.profileKey = new Uint8Array(params.lokiProfile.profileKey);
- } else {
- this.profileKey = new Uint8Array(
- ByteBuffer.wrap(params.lokiProfile.profileKey).toArrayBuffer()
- );
- }
- }
- this.displayName = params.lokiProfile && params.lokiProfile.displayName;
- this.avatarPointer = params.lokiProfile && params.lokiProfile.avatarPointer;
+ const profile = buildProfileForOutgoingMessage(params);
+
+ this.profile = profile.lokiProfile;
+ this.profileKey = profile.profileKey;
+
this.preview = params.preview;
this.reaction = params.reaction;
this.syncTarget = params.syncTarget;
@@ -137,18 +128,10 @@ export class VisibleMessage extends DataMessage {
dataMessage.syncTarget = this.syncTarget;
}
- if (this.avatarPointer || this.displayName) {
- const profile = new SignalService.DataMessage.LokiProfile();
-
- if (this.avatarPointer) {
- profile.profilePicture = this.avatarPointer;
- }
-
- if (this.displayName) {
- profile.displayName = this.displayName;
- }
- dataMessage.profile = profile;
+ if (this.profile) {
+ dataMessage.profile = this.profile;
}
+
if (this.profileKey && this.profileKey.length) {
dataMessage.profileKey = this.profileKey;
}
@@ -201,3 +184,47 @@ export class VisibleMessage extends DataMessage {
return this.identifier === comparator.identifier && this.timestamp === comparator.timestamp;
}
}
+
+export function buildProfileForOutgoingMessage(params: { lokiProfile?: LokiProfile }) {
+ let profileKey: Uint8Array | undefined;
+ if (params.lokiProfile && params.lokiProfile.profileKey) {
+ if (
+ params.lokiProfile.profileKey instanceof Uint8Array ||
+ (params.lokiProfile.profileKey as any) instanceof ByteBuffer
+ ) {
+ profileKey = new Uint8Array(params.lokiProfile.profileKey);
+ } else {
+ profileKey = new Uint8Array(ByteBuffer.wrap(params.lokiProfile.profileKey).toArrayBuffer());
+ }
+ }
+
+ const displayName = params.lokiProfile?.displayName;
+
+ // no need to iclude the avatarPointer if there is no profileKey associated with it.
+ const avatarPointer =
+ params.lokiProfile?.avatarPointer &&
+ !isEmpty(profileKey) &&
+ params.lokiProfile.avatarPointer &&
+ !isEmpty(params.lokiProfile.avatarPointer)
+ ? params.lokiProfile.avatarPointer
+ : undefined;
+
+ let lokiProfile: SignalService.DataMessage.ILokiProfile | undefined;
+ if (avatarPointer || displayName) {
+ lokiProfile = new SignalService.DataMessage.LokiProfile();
+
+ // we always need a profileKey tom decode an avatar pointer
+ if (avatarPointer && avatarPointer.length && profileKey) {
+ lokiProfile.profilePicture = avatarPointer;
+ }
+
+ if (displayName) {
+ lokiProfile.displayName = displayName;
+ }
+ }
+
+ return {
+ lokiProfile,
+ profileKey: lokiProfile?.profilePicture ? profileKey : undefined,
+ };
+}
diff --git a/ts/test/session/unit/messages/MessageRequestResponse_test.ts b/ts/test/session/unit/messages/MessageRequestResponse_test.ts
new file mode 100644
index 000000000..85da9a965
--- /dev/null
+++ b/ts/test/session/unit/messages/MessageRequestResponse_test.ts
@@ -0,0 +1,160 @@
+import { expect } from 'chai';
+import { v4 } from 'uuid';
+
+import { SignalService } from '../../../../protobuf';
+import { Constants } from '../../../../session';
+import { MessageRequestResponse } from '../../../../session/messages/outgoing/controlMessage/MessageRequestResponse';
+// tslint:disable: no-unused-expression
+
+// tslint:disable-next-line: max-func-body-length
+describe('MessageRequestResponse', () => {
+ let message: MessageRequestResponse | undefined;
+ it('correct ttl', () => {
+ message = new MessageRequestResponse({
+ timestamp: Date.now(),
+ });
+
+ expect(message.ttl()).to.equal(Constants.TTL_DEFAULT.TTL_MAX);
+ });
+
+ it('has an identifier', () => {
+ message = new MessageRequestResponse({
+ timestamp: Date.now(),
+ });
+
+ expect(message.identifier).to.not.equal(null, 'identifier cannot be null');
+ expect(message.identifier).to.not.equal(undefined, 'identifier cannot be undefined');
+ });
+
+ it('has an identifier matching if given', () => {
+ const identifier = v4();
+ message = new MessageRequestResponse({
+ timestamp: Date.now(),
+ identifier,
+ });
+
+ expect(message.identifier).to.not.equal(identifier, 'identifier should match');
+ });
+
+ it('isApproved is always true', () => {
+ message = new MessageRequestResponse({
+ timestamp: Date.now(),
+ });
+ const plainText = message.plainTextBuffer();
+ const decoded = SignalService.Content.decode(plainText);
+ expect(decoded.messageRequestResponse)
+ .to.have.property('isApproved')
+ .to.be.eq(true, 'isApproved is true');
+ });
+
+ it('can create response without lokiProfile', () => {
+ message = new MessageRequestResponse({
+ timestamp: Date.now(),
+ });
+ const plainText = message.plainTextBuffer();
+ const decoded = SignalService.Content.decode(plainText);
+ expect(decoded.messageRequestResponse)
+ .to.have.property('profile')
+ .to.be.eq(null, 'no profile field if no profile given');
+ });
+
+ it('can create response with display name only', () => {
+ message = new MessageRequestResponse({
+ timestamp: Date.now(),
+ lokiProfile: { displayName: 'Jane', profileKey: null },
+ });
+ const plainText = message.plainTextBuffer();
+ const decoded = SignalService.Content.decode(plainText);
+
+ expect(decoded.messageRequestResponse?.profile?.displayName).to.be.deep.eq('Jane');
+ expect(decoded.messageRequestResponse?.profile?.profilePicture).to.be.empty;
+ expect(decoded.messageRequestResponse?.profileKey).to.be.empty;
+ });
+
+ it('empty profileKey does not get included', () => {
+ message = new MessageRequestResponse({
+ timestamp: Date.now(),
+ lokiProfile: { displayName: 'Jane', profileKey: new Uint8Array(0) },
+ });
+ const plainText = message.plainTextBuffer();
+ const decoded = SignalService.Content.decode(plainText);
+
+ expect(decoded.messageRequestResponse?.profile?.displayName).to.be.eq('Jane');
+
+ expect(decoded.messageRequestResponse?.profile?.profilePicture).to.be.empty;
+ expect(decoded.messageRequestResponse?.profileKey).to.be.empty;
+ });
+
+ it('can create response with display name and profileKey and profileImage', () => {
+ message = new MessageRequestResponse({
+ timestamp: Date.now(),
+ lokiProfile: {
+ displayName: 'Jane',
+ profileKey: new Uint8Array([1, 2, 3, 4, 5, 6]),
+ avatarPointer: 'https://somevalidurl.com',
+ },
+ });
+ const plainText = message.plainTextBuffer();
+ const decoded = SignalService.Content.decode(plainText);
+
+ expect(decoded.messageRequestResponse?.profile?.displayName).to.be.deep.eq('Jane');
+
+ expect(decoded.messageRequestResponse?.profileKey).to.be.not.empty;
+
+ if (!decoded.messageRequestResponse?.profileKey?.buffer) {
+ throw new Error('decoded.messageRequestResponse?.profileKey?.buffer should be set');
+ }
+ expect(decoded.messageRequestResponse?.profile?.profilePicture).to.be.eq(
+ 'https://somevalidurl.com'
+ );
+ // don't ask me why deep.eq ([1,2,3, ...]) gives nothing interesting but a 8192 buffer not matching
+ expect(decoded.messageRequestResponse?.profileKey.length).to.be.eq(6);
+ expect(decoded.messageRequestResponse?.profileKey[0]).to.be.eq(1);
+ expect(decoded.messageRequestResponse?.profileKey[1]).to.be.eq(2);
+ expect(decoded.messageRequestResponse?.profileKey[2]).to.be.eq(3);
+ expect(decoded.messageRequestResponse?.profileKey[3]).to.be.eq(4);
+ expect(decoded.messageRequestResponse?.profileKey[4]).to.be.eq(5);
+ expect(decoded.messageRequestResponse?.profileKey[5]).to.be.eq(6);
+ });
+
+ it('profileKey not included if profileUrl not set', () => {
+ message = new MessageRequestResponse({
+ timestamp: Date.now(),
+ lokiProfile: { displayName: 'Jane', profileKey: new Uint8Array([1, 2, 3, 4, 5, 6]) },
+ });
+ const plainText = message.plainTextBuffer();
+ const decoded = SignalService.Content.decode(plainText);
+
+ expect(decoded.messageRequestResponse?.profile?.displayName).to.be.deep.eq('Jane');
+
+ if (!decoded.messageRequestResponse?.profileKey?.buffer) {
+ throw new Error('decoded.messageRequestResponse?.profileKey?.buffer should be set');
+ }
+
+ expect(decoded.messageRequestResponse?.profile?.profilePicture).to.be.empty;
+ expect(decoded.messageRequestResponse?.profileKey).to.be.empty;
+ });
+
+ it('url not included if profileKey not set', () => {
+ message = new MessageRequestResponse({
+ timestamp: Date.now(),
+ lokiProfile: {
+ displayName: 'Jane',
+ profileKey: null,
+ avatarPointer: 'https://somevalidurl.com',
+ },
+ });
+ const plainText = message.plainTextBuffer();
+ const decoded = SignalService.Content.decode(plainText);
+
+ expect(decoded.messageRequestResponse?.profile?.displayName).to.be.deep.eq('Jane');
+
+ if (!decoded.messageRequestResponse?.profileKey?.buffer) {
+ throw new Error('decoded.messageRequestResponse?.profileKey?.buffer should be set');
+ }
+
+ expect(decoded.messageRequestResponse?.profile?.displayName).to.be.eq('Jane');
+ expect(decoded.messageRequestResponse?.profile?.profilePicture).to.be.empty;
+ expect(decoded.messageRequestResponse?.profileKey).to.be.empty;
+ });
+});
diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts
index 8394717c3..95a12580f 100644
--- a/ts/types/LocalizerKeys.ts
+++ b/ts/types/LocalizerKeys.ts
@@ -162,6 +162,7 @@ export type LocalizerKeys =
| 'spellCheckDirty'
| 'debugLogExplanation'
| 'closedGroupInviteFailTitle'
+ | 'areYouSureClearDevice'
| 'setAccountPasswordDescription'
| 'removeAccountPasswordDescription'
| 'establishingConnection'
@@ -348,6 +349,8 @@ export type LocalizerKeys =
| 'openGroupInvitation'
| 'callMissedCausePermission'
| 'mediaPermissionsDescription'
+ | 'tryAgain'
+ | 'clearDevice'
| 'media'
| 'noMembersInThisGroup'
| 'saveLogToDesktop'
@@ -478,6 +481,7 @@ export type LocalizerKeys =
| 'titleIsNow'
| 'removePasswordToastDescription'
| 'recoveryPhrase'
+ | 'deleteAccountFromLogin'
| 'newMessages'
| 'you'
| 'pruneSettingTitle'