mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
Merge branch 'clearnet' into theming
This commit is contained in:
commit
0f3a23779a
23 changed files with 415 additions and 118 deletions
|
@ -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?",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 ? (
|
||||
<p className="text-center">{window.i18n('deleteAccountWarning')}</p>
|
||||
<p className="text-center">{window.i18n('deleteAccountFromLogin')}</p>
|
||||
) : (
|
||||
<input
|
||||
id="password-prompt-input"
|
||||
|
@ -87,19 +88,7 @@ class SessionPasswordPromptInner extends React.PureComponent<{}, State> {
|
|||
const infoIcon = this.state.clearDataView ?? (
|
||||
<SessionIcon iconType="warning" iconSize={35} iconColor="var(--danger-color)" />
|
||||
);
|
||||
const errorSection = !this.state.clearDataView && (
|
||||
<div className="password-prompt-error-section">
|
||||
{this.state.error && (
|
||||
<>
|
||||
{showResetElements ? (
|
||||
<div className="session-label warning">{window.i18n('maxPasswordAttempts')}</div>
|
||||
) : (
|
||||
<div className="session-label primary">{this.state.error}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const spinner = isLoading ? <SessionSpinner loading={true} /> : null;
|
||||
|
||||
return (
|
||||
|
@ -114,7 +103,6 @@ class SessionPasswordPromptInner extends React.PureComponent<{}, State> {
|
|||
{spinner || featureElement}
|
||||
<TextPleaseWait isLoading={isLoading} />
|
||||
|
||||
{errorSection}
|
||||
{buttonGroup}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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 && (
|
||||
<>
|
||||
<SessionButton
|
||||
text="Reset Database"
|
||||
text={window.i18n('clearDevice')}
|
||||
buttonColor={SessionButtonColor.Danger}
|
||||
buttonType={SessionButtonType.Simple}
|
||||
onClick={this.initClearDataView}
|
||||
|
@ -189,7 +181,7 @@ class SessionPasswordPromptInner extends React.PureComponent<{}, State> {
|
|||
)}
|
||||
{/* TODO Theming - Fix */}
|
||||
<SessionButton
|
||||
text={window.i18n('done')}
|
||||
text={showResetElements ? window.i18n('tryAgain') : window.i18n('done')}
|
||||
buttonType={SessionButtonType.Simple}
|
||||
onClick={this.initLogin}
|
||||
/>
|
||||
|
@ -201,7 +193,7 @@ class SessionPasswordPromptInner extends React.PureComponent<{}, State> {
|
|||
return (
|
||||
<div className="button-group">
|
||||
<SessionButton
|
||||
text={window.i18n('clearAllData')}
|
||||
text={window.i18n('clearDevice')}
|
||||
buttonColor={SessionButtonColor.Danger}
|
||||
buttonType={SessionButtonType.Simple}
|
||||
onClick={window.clearLocalData}
|
||||
|
@ -227,6 +219,7 @@ export const SessionPasswordPrompt = () => {
|
|||
|
||||
return (
|
||||
<SessionTheme>
|
||||
<SessionToastContainer />
|
||||
<StyledContent>
|
||||
<SessionPasswordPromptInner />
|
||||
</StyledContent>
|
||||
|
|
|
@ -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<Props, State> {
|
|||
}
|
||||
|
||||
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;
|
||||
|
|
|
@ -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 (
|
||||
<Flex container={true}>
|
||||
<StyledAuthorContainer container={true}>
|
||||
<ContactName
|
||||
pubkey={displayedPubkey}
|
||||
name={authorName}
|
||||
|
@ -47,6 +53,6 @@ export const MessageAuthorText = (props: Props) => {
|
|||
boldProfileName={true}
|
||||
shouldShowPubkey={Boolean(isPublic)}
|
||||
/>
|
||||
</Flex>
|
||||
</StyledAuthorContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ export class SessionPasswordDialog extends React.Component<Props, State> {
|
|||
|
||||
<div className="session-modal__button-group">
|
||||
<SessionButton
|
||||
text={window.i18n('ok')}
|
||||
text={window.i18n('done')}
|
||||
buttonColor={passwordAction === 'remove' ? SessionButtonColor.Danger : undefined}
|
||||
buttonType={SessionButtonType.Simple}
|
||||
onClick={this.setPassword}
|
||||
|
|
|
@ -15,6 +15,7 @@ import { matchesHash } from '../../util/passwordUtils';
|
|||
import { SettingsCategoryPermissions } from './section/CategoryPermissions';
|
||||
import { SettingsCategoryHelp } from './section/CategoryHelp';
|
||||
import styled from 'styled-components';
|
||||
import { ToastUtils } from '../../session/utils';
|
||||
|
||||
export function getMediaPermissionsSettings() {
|
||||
return window.getSettingValue('media-permissions');
|
||||
|
@ -42,9 +43,6 @@ export interface SettingsViewProps {
|
|||
|
||||
interface State {
|
||||
hasPassword: boolean | null;
|
||||
pwdLockError: string | null;
|
||||
mediaSetting: boolean | null;
|
||||
callMediaSetting: boolean | null;
|
||||
shouldLockSettings: boolean | null;
|
||||
}
|
||||
|
||||
|
@ -105,8 +103,7 @@ const StyledPasswordInput = styled.input`
|
|||
border: 1px solid var(--border-color);
|
||||
border-radius: 7px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
letter-spacing: 5px;
|
||||
font-size: 16px;
|
||||
font-family: var(--font-default);
|
||||
|
||||
::placeholder {
|
||||
|
@ -120,30 +117,26 @@ const StyledH3 = styled.h3`
|
|||
`;
|
||||
|
||||
const PasswordLock = ({
|
||||
pwdLockError,
|
||||
validatePasswordLock,
|
||||
}: {
|
||||
pwdLockError: string | null;
|
||||
validatePasswordLock: () => Promise<boolean>;
|
||||
}) => {
|
||||
return (
|
||||
<div className="session-settings__password-lock">
|
||||
<div className="session-settings__password-lock-box">
|
||||
<StyledH3>{window.i18n('password')}</StyledH3>
|
||||
<StyledH3>{window.i18n('passwordViewTitle')}</StyledH3>
|
||||
<StyledPasswordInput
|
||||
type="password"
|
||||
id="password-lock-input"
|
||||
defaultValue=""
|
||||
placeholder="Password"
|
||||
placeholder={window.i18n('enterPassword')}
|
||||
data-testid="password-lock-input"
|
||||
autoFocus={true}
|
||||
/>
|
||||
|
||||
{pwdLockError && <div className="session-label danger">{pwdLockError}</div>}
|
||||
|
||||
<SessionButton
|
||||
buttonType={SessionButtonType.Simple}
|
||||
text={window.i18n('ok')}
|
||||
text={window.i18n('done')}
|
||||
onClick={validatePasswordLock}
|
||||
/>
|
||||
</div>
|
||||
|
@ -210,9 +203,6 @@ export class SessionSettingsView extends React.Component<SettingsViewProps, Stat
|
|||
|
||||
this.state = {
|
||||
hasPassword: null,
|
||||
pwdLockError: null,
|
||||
mediaSetting: null,
|
||||
callMediaSetting: null,
|
||||
shouldLockSettings: true,
|
||||
};
|
||||
|
||||
|
@ -228,10 +218,6 @@ export class SessionSettingsView extends React.Component<SettingsViewProps, Stat
|
|||
|
||||
public componentDidMount() {
|
||||
window.addEventListener('keyup', this.onKeyUp);
|
||||
|
||||
const mediaSetting = getMediaPermissionsSettings();
|
||||
const callMediaSetting = getCallMediaPermissionsSettings();
|
||||
this.setState({ mediaSetting, callMediaSetting });
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -244,9 +230,7 @@ export class SessionSettingsView extends React.Component<SettingsViewProps, Stat
|
|||
);
|
||||
|
||||
if (!enteredPassword) {
|
||||
this.setState({
|
||||
pwdLockError: window.i18n('noGivenPassword'),
|
||||
});
|
||||
ToastUtils.pushToastError('validatePassword', window.i18n('noGivenPassword'));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -254,17 +238,13 @@ export class SessionSettingsView extends React.Component<SettingsViewProps, Stat
|
|||
// Check if the password matches the hash we have stored
|
||||
const hash = await Data.getPasswordHash();
|
||||
if (hash && !matchesHash(enteredPassword, hash)) {
|
||||
this.setState({
|
||||
pwdLockError: window.i18n('invalidPassword'),
|
||||
});
|
||||
|
||||
ToastUtils.pushToastError('validatePassword', window.i18n('invalidPassword'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unlocked settings
|
||||
this.setState({
|
||||
shouldLockSettings: false,
|
||||
pwdLockError: null,
|
||||
});
|
||||
|
||||
return true;
|
||||
|
@ -280,10 +260,7 @@ export class SessionSettingsView extends React.Component<SettingsViewProps, Stat
|
|||
|
||||
<StyledSettingsView>
|
||||
{shouldRenderPasswordLock ? (
|
||||
<PasswordLock
|
||||
pwdLockError={this.state.pwdLockError}
|
||||
validatePasswordLock={this.validatePasswordLock}
|
||||
/>
|
||||
<PasswordLock validatePasswordLock={this.validatePasswordLock} />
|
||||
) : (
|
||||
<StyledSettingsList ref={this.settingsViewRef}>
|
||||
<SettingInCategory
|
||||
|
@ -304,7 +281,6 @@ export class SessionSettingsView extends React.Component<SettingsViewProps, Stat
|
|||
this.setState({
|
||||
hasPassword: true,
|
||||
shouldLockSettings: true,
|
||||
pwdLockError: null,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -148,6 +148,7 @@ export const Data = {
|
|||
getMessageBySenderAndTimestamp,
|
||||
getUnreadByConversation,
|
||||
getUnreadCountByConversation,
|
||||
markAllAsReadByConversationNoExpiration,
|
||||
getMessageCountByType,
|
||||
getMessagesByConversation,
|
||||
getLastMessagesByConversation,
|
||||
|
@ -472,6 +473,7 @@ async function getMessageBySenderAndTimestamp({
|
|||
source,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
if (!messages || !messages.length) {
|
||||
return null;
|
||||
}
|
||||
|
@ -484,6 +486,13 @@ async function getUnreadByConversation(conversationId: string): Promise<MessageC
|
|||
return new MessageCollection(messages);
|
||||
}
|
||||
|
||||
async function markAllAsReadByConversationNoExpiration(
|
||||
conversationId: string
|
||||
): Promise<Array<number>> {
|
||||
const messagesIds = await channels.markAllAsReadByConversationNoExpiration(conversationId);
|
||||
return messagesIds;
|
||||
}
|
||||
|
||||
// might throw
|
||||
async function getUnreadCountByConversation(conversationId: string): Promise<number> {
|
||||
return channels.getUnreadCountByConversation(conversationId);
|
||||
|
|
|
@ -40,6 +40,7 @@ const channelsToMake = new Set([
|
|||
'removeMessage',
|
||||
'_removeMessages',
|
||||
'getUnreadByConversation',
|
||||
'markAllAsReadByConversationNoExpiration',
|
||||
'getUnreadCountByConversation',
|
||||
'getMessageCountByType',
|
||||
'removeAllMessagesInConversation',
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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<ConversationAttributes> {
|
|||
|
||||
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<ConversationAttributes> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<ConversationAttributes> {
|
|||
|
||||
// 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<ConversationAttributes> {
|
|||
// 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<number>;
|
||||
await this.sendReadReceiptsIfNeeded(timestamps);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<PreviewWithAttachmentUrl>;
|
||||
|
||||
/// 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,
|
||||
};
|
||||
}
|
||||
|
|
160
ts/test/session/unit/messages/MessageRequestResponse_test.ts
Normal file
160
ts/test/session/unit/messages/MessageRequestResponse_test.ts
Normal file
|
@ -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;
|
||||
});
|
||||
});
|
|
@ -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'
|
||||
|
|
Loading…
Reference in a new issue