diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d16a544ad..09a67aa7b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -509,5 +509,6 @@ "reactionPopupThree": "$name$, $name2$ & $name3$", "reactionPopupMany": "$name$, $name2$, $name3$ &", "reactionListCountSingular": "And $otherSingular$ has reacted <span>$emoji$</span> to this message", - "reactionListCountPlural": "And $otherPlural$ have reacted <span>$emoji$</span> to this message" + "reactionListCountPlural": "And $otherPlural$ have reacted <span>$emoji$</span> to this message", + "setDisplayPicture": "Set Display Picture" } diff --git a/tools/updateI18nKeysType.py b/tools/updateI18nKeysType.py index 585a07490..bcd61e8d8 100755 --- a/tools/updateI18nKeysType.py +++ b/tools/updateI18nKeysType.py @@ -6,6 +6,7 @@ from os import path, listdir from glob import glob import json import sys +from collections import OrderedDict LOCALES_FOLDER = './_locales' @@ -16,10 +17,10 @@ LOCALIZED_KEYS_FILE = './ts/types/LocalizerKeys.ts' stringToWrite = "export type LocalizerKeys =\n | " with open(EN_FILE,'r') as jsonFile: - data = json.load(jsonFile) - keys = data.keys() + data = json.loads(jsonFile.read(), object_pairs_hook=OrderedDict) + keys = sorted(list(data.keys())) - stringToWrite += json.dumps(list(keys), sort_keys=True).replace(',', '\n |').replace('"', '\'')[1:-1] + stringToWrite += json.dumps(keys, sort_keys=True).replace(',', '\n |').replace('"', '\'')[1:-1] stringToWrite += ';\n' diff --git a/ts/components/dialog/EditProfileDialog.tsx b/ts/components/dialog/EditProfileDialog.tsx index a1d38b080..197335f40 100644 --- a/ts/components/dialog/EditProfileDialog.tsx +++ b/ts/components/dialog/EditProfileDialog.tsx @@ -1,5 +1,6 @@ -import autoBind from 'auto-bind'; -import React, { ChangeEvent, MouseEvent } from 'react'; +import { useDispatch } from 'react-redux'; +// eslint-disable-next-line import/no-named-default +import { ChangeEvent, MouseEvent, default as React, ReactElement, useState } from 'react'; import { QRCode } from 'react-qr-svg'; import styled from 'styled-components'; import { Avatar, AvatarSize } from '../avatar/Avatar'; @@ -7,15 +8,12 @@ import { Avatar, AvatarSize } from '../avatar/Avatar'; import { SyncUtils, ToastUtils, UserUtils } from '../../session/utils'; import { YourSessionIDPill, YourSessionIDSelectable } from '../basic/YourSessionIDPill'; -import { ConversationModel } from '../../models/conversation'; - -import { uploadOurAvatar } from '../../interactions/conversationInteractions'; +import { useOurAvatarPath, useOurConversationUsername } from '../../hooks/useParamSelector'; import { ConversationTypeEnum } from '../../models/conversationAttributes'; import { MAX_USERNAME_BYTES } from '../../session/constants'; import { getConversationController } from '../../session/conversations'; import { sanitizeSessionUsername } from '../../session/utils/String'; -import { editProfileModal } from '../../state/ducks/modalDialog'; -import { pickFileForAvatar } from '../../types/attachments/VisualAttachment'; +import { editProfileModal, updateEditProfilePictureModel } from '../../state/ducks/modalDialog'; import { saveQRCode } from '../../util/saveQRCode'; import { setLastProfileUpdateTimestamp } from '../../util/storage'; import { SessionWrapperModal } from '../SessionWrapperModal'; @@ -50,311 +48,255 @@ const QRView = ({ sessionID }: { sessionID: string }) => { ); }; -interface State { - profileName: string; - updatedProfileName: string; - oldAvatarPath: string; - newAvatarObjectUrl: string | null; - mode: 'default' | 'edit' | 'qr'; - loading: boolean; -} - -export class EditProfileDialog extends React.Component<object, State> { - private readonly convo: ConversationModel; - - constructor(props: object) { - super(props); - - autoBind(this); - - this.convo = getConversationController().get(UserUtils.getOurPubKeyStrFromCache()); - - this.state = { - profileName: this.convo.getRealSessionUsername() || '', - updatedProfileName: this.convo.getRealSessionUsername() || '', - oldAvatarPath: this.convo.getAvatarPath() || '', - newAvatarObjectUrl: null, - mode: 'default', - loading: false, - }; - } - - public componentDidMount() { - window.addEventListener('keyup', this.onKeyUp); - } +const updateDisplayName = async (newName: string) => { + const ourNumber = UserUtils.getOurPubKeyStrFromCache(); + const conversation = await getConversationController().getOrCreateAndWait( + ourNumber, + ConversationTypeEnum.PRIVATE + ); + conversation.setSessionDisplayNameNoCommit(newName); - public componentWillUnmount() { - window.removeEventListener('keyup', this.onKeyUp); - } + // might be good to not trigger a sync if the name did not change + await conversation.commit(); + await setLastProfileUpdateTimestamp(Date.now()); + await SyncUtils.forceSyncConfigurationNowIfNeeded(true); +}; - public render() { - const i18n = window.i18n; +type ProfileAvatarProps = { + avatarPath: string | null; + newAvatarObjectUrl?: string | null; + profileName: string | undefined; + ourId: string; +}; - const viewDefault = this.state.mode === 'default'; - const viewEdit = this.state.mode === 'edit'; - const viewQR = this.state.mode === 'qr'; +export const ProfileAvatar = (props: ProfileAvatarProps): ReactElement => { + const { newAvatarObjectUrl, avatarPath, profileName, ourId } = props; + return ( + <Avatar + forcedAvatarPath={newAvatarObjectUrl || avatarPath} + forcedName={profileName || ourId} + size={AvatarSize.XL} + pubkey={ourId} + /> + ); +}; - const sessionID = UserUtils.getOurPubKeyStrFromCache(); +type ProfileHeaderProps = ProfileAvatarProps & { + onClick: () => void; + setMode: (mode: ProfileDialogModes) => void; +}; - const backButton = - viewEdit || viewQR - ? [ - { - iconType: 'chevron', - iconRotation: 90, - onClick: () => { - this.setState({ mode: 'default' }); - }, - }, - ] - : undefined; +const ProfileHeader = (props: ProfileHeaderProps): ReactElement => { + const { avatarPath, profileName, ourId, onClick, setMode } = props; - return ( - <div className="edit-profile-dialog" data-testid="edit-profile-dialog"> - <SessionWrapperModal - title={i18n('editProfileModalTitle')} - onClose={this.closeDialog} - headerIconButtons={backButton} - showExitIcon={true} + return ( + <div className="avatar-center"> + <div className="avatar-center-inner"> + <ProfileAvatar avatarPath={avatarPath} profileName={profileName} ourId={ourId} /> + <div + className="image-upload-section" + role="button" + onClick={onClick} + data-testid="image-upload-section" + /> + <div + className="qr-view-button" + onClick={() => { + setMode('qr'); + }} + role="button" > - {viewQR && <QRView sessionID={sessionID} />} - {viewDefault && this.renderDefaultView()} - {viewEdit && this.renderEditView()} - - <div className="session-id-section"> - <YourSessionIDPill /> - <YourSessionIDSelectable /> - - <SessionSpinner loading={this.state.loading} /> - - {viewDefault || viewQR ? ( - <SessionButton - text={window.i18n('editMenuCopy')} - buttonType={SessionButtonType.Simple} - onClick={() => { - window.clipboard.writeText(sessionID); - ToastUtils.pushCopiedToClipBoard(); - }} - dataTestId="copy-button-profile-update" - /> - ) : ( - !this.state.loading && ( - <SessionButton - text={window.i18n('save')} - buttonType={SessionButtonType.Simple} - onClick={this.onClickOK} - disabled={this.state.loading} - dataTestId="save-button-profile-update" - /> - ) - )} - </div> - </SessionWrapperModal> - </div> - ); - } - - private renderProfileHeader() { - return ( - <> - <div className="avatar-center"> - <div className="avatar-center-inner"> - {this.renderAvatar()} - <div - className="image-upload-section" - role="button" - // eslint-disable-next-line @typescript-eslint/no-misused-promises - onClick={this.fireInputEvent} - data-testid="image-upload-section" - /> - <div - className="qr-view-button" - onClick={() => { - this.setState(state => ({ ...state, mode: 'qr' })); - }} - role="button" - > - <SessionIconButton iconType="qr" iconSize="small" iconColor="var(--black-color)" /> - </div> - </div> + <SessionIconButton iconType="qr" iconSize="small" iconColor="var(--black-color)" /> </div> - </> - ); - } - - private async fireInputEvent() { - const scaledAvatarUrl = await pickFileForAvatar(); + </div> + </div> + ); +}; - if (scaledAvatarUrl) { - this.setState({ - newAvatarObjectUrl: scaledAvatarUrl, - mode: 'edit', - }); - } - } +type ProfileDialogModes = 'default' | 'edit' | 'qr'; +// tslint:disable-next-line: max-func-body-length +export const EditProfileDialog = (): ReactElement => { + const dispatch = useDispatch(); - private renderDefaultView() { - const name = this.state.updatedProfileName || this.state.profileName; - return ( - <> - {this.renderProfileHeader()} + const _profileName = useOurConversationUsername() || ''; + const [profileName, setProfileName] = useState(_profileName); + const [updatedProfileName, setUpdateProfileName] = useState(profileName); + const avatarPath = useOurAvatarPath() || ''; + const ourId = UserUtils.getOurPubKeyStrFromCache(); - <div className="profile-name-uneditable"> - <p data-testid="your-profile-name">{name}</p> - <SessionIconButton - iconType="pencil" - iconSize="medium" - onClick={() => { - this.setState({ mode: 'edit' }); - }} - dataTestId="edit-profile-icon" - /> - </div> - </> - ); - } + const [mode, setMode] = useState<ProfileDialogModes>('default'); + const [loading, setLoading] = useState(false); - private renderEditView() { - const placeholderText = window.i18n('displayName'); + const closeDialog = () => { + window.removeEventListener('keyup', handleOnKeyUp); + window.inboxStore?.dispatch(editProfileModal(null)); + }; + + const backButton = + mode === 'edit' || mode === 'qr' + ? [ + { + iconType: 'chevron', + iconRotation: 90, + onClick: () => { + setMode('default'); + }, + }, + ] + : undefined; + + const onClickOK = async () => { + /** + * Tidy the profile name input text and save the new profile name and avatar + */ + try { + const newName = profileName ? profileName.trim() : ''; - return ( - <> - {this.renderProfileHeader()} - <div className="profile-name"> - <input - type="text" - className="profile-name-input" - value={this.state.profileName} - placeholder={placeholderText} - onChange={this.onNameEdited} - maxLength={MAX_USERNAME_BYTES} - tabIndex={0} - required={true} - aria-required={true} - data-testid="profile-name-input" - /> - </div> - </> - ); - } + if (newName.length === 0 || newName.length > MAX_USERNAME_BYTES) { + return; + } - private renderAvatar() { - const { oldAvatarPath, newAvatarObjectUrl, profileName } = this.state; - const userName = profileName || this.convo.id; + // this throw if the length in bytes is too long + const sanitizedName = sanitizeSessionUsername(newName); + const trimName = sanitizedName.trim(); - return ( - <Avatar - forcedAvatarPath={newAvatarObjectUrl || oldAvatarPath} - forcedName={userName} - size={AvatarSize.XL} - pubkey={this.convo.id} - /> - ); - } + setUpdateProfileName(trimName); + setLoading(true); - private onNameEdited(event: ChangeEvent<HTMLInputElement>) { - const displayName = event.target.value; - try { - const newName = sanitizeSessionUsername(displayName); - this.setState({ - profileName: newName, - }); + await updateDisplayName(newName); + setMode('default'); + setUpdateProfileName(profileName); + setLoading(false); } catch (e) { - this.setState({ - profileName: displayName, - }); ToastUtils.pushToastError('nameTooLong', window.i18n('displayNameTooLong')); } - } + }; - private onKeyUp(event: any) { + const handleOnKeyUp = (event: any) => { switch (event.key) { case 'Enter': - if (this.state.mode === 'edit') { - this.onClickOK(); + if (mode === 'edit') { + void onClickOK(); } break; case 'Esc': case 'Escape': - this.closeDialog(); + closeDialog(); break; default: } - } + }; + + const handleProfileHeaderClick = () => { + closeDialog(); + dispatch( + updateEditProfilePictureModel({ + avatarPath, + profileName, + ourId, + }) + ); + }; - /** - * Tidy the profile name input text and save the new profile name and avatar - */ - private onClickOK() { - const { newAvatarObjectUrl, profileName } = this.state; + const onNameEdited = (event: ChangeEvent<HTMLInputElement>) => { + const displayName = event.target.value; try { - const newName = profileName ? profileName.trim() : ''; - - if (newName.length === 0 || newName.length > MAX_USERNAME_BYTES) { - return; - } - - // this throw if the length in bytes is too long - const sanitizedName = sanitizeSessionUsername(newName); - const trimName = sanitizedName.trim(); - - this.setState( - { - profileName: trimName, - loading: true, - }, - // eslint-disable-next-line @typescript-eslint/no-misused-promises - async () => { - await commitProfileEdits(newName, newAvatarObjectUrl); - this.setState({ - loading: false, - - mode: 'default', - updatedProfileName: this.state.profileName, - }); - } - ); + const newName = sanitizeSessionUsername(displayName); + setProfileName(newName); } catch (e) { + setProfileName(displayName); ToastUtils.pushToastError('nameTooLong', window.i18n('displayNameTooLong')); } - } + }; - private closeDialog() { - window.removeEventListener('keyup', this.onKeyUp); - window.inboxStore?.dispatch(editProfileModal(null)); - } -} + return ( + /* The <div> element has a child <input> element that allows keyboard interaction */ + /* tslint:disable-next-line: react-a11y-event-has-role */ + <div className="edit-profile-dialog" data-testid="edit-profile-dialog" onKeyUp={handleOnKeyUp}> + <SessionWrapperModal + title={window.i18n('editProfileModalTitle')} + onClose={closeDialog} + headerIconButtons={backButton} + showExitIcon={true} + > + {mode === 'qr' && <QRView sessionID={ourId} />} + {mode === 'default' && ( + <> + <ProfileHeader + avatarPath={avatarPath} + profileName={profileName} + ourId={ourId} + onClick={handleProfileHeaderClick} + setMode={setMode} + /> + <div className="profile-name-uneditable"> + <p data-testid="your-profile-name">{updatedProfileName || profileName}</p> + <SessionIconButton + iconType="pencil" + iconSize="medium" + onClick={() => { + setMode('edit'); + }} + dataTestId="edit-profile-icon" + /> + </div> + </> + )} + {mode === 'edit' && ( + <> + <ProfileHeader + avatarPath={avatarPath} + profileName={profileName} + ourId={ourId} + onClick={handleProfileHeaderClick} + setMode={setMode} + /> + <div className="profile-name"> + <input + type="text" + className="profile-name-input" + value={profileName} + placeholder={window.i18n('displayName')} + onChange={onNameEdited} + maxLength={MAX_USERNAME_BYTES} + tabIndex={0} + required={true} + aria-required={true} + data-testid="profile-name-input" + /> + </div> + </> + )} -async function commitProfileEdits(newName: string, scaledAvatarUrl: string | null) { - const ourNumber = UserUtils.getOurPubKeyStrFromCache(); - const conversation = await getConversationController().getOrCreateAndWait( - ourNumber, - ConversationTypeEnum.PRIVATE - ); + <div className="session-id-section"> + <YourSessionIDPill /> + <YourSessionIDSelectable /> - if (scaledAvatarUrl?.length) { - try { - const blobContent = await (await fetch(scaledAvatarUrl)).blob(); - if (!blobContent || !blobContent.size) { - throw new Error('Failed to fetch blob content from scaled avatar'); - } - await uploadOurAvatar(await blobContent.arrayBuffer()); - } catch (error) { - if (error.message && error.message.length) { - ToastUtils.pushToastError('edit-profile', error.message); - } - window.log.error( - 'showEditProfileDialog Error ensuring that image is properly sized:', - error && error.stack ? error.stack : error - ); - } - return; - } - // do not update the avatar if it did not change - conversation.setSessionDisplayNameNoCommit(newName); + <SessionSpinner loading={loading} /> - // might be good to not trigger a sync if the name did not change - await conversation.commit(); - await setLastProfileUpdateTimestamp(Date.now()); - await SyncUtils.forceSyncConfigurationNowIfNeeded(true); -} + {mode === 'default' || mode === 'qr' ? ( + <SessionButton + text={window.i18n('editMenuCopy')} + buttonType={SessionButtonType.Simple} + onClick={() => { + window.clipboard.writeText(ourId); + ToastUtils.pushCopiedToClipBoard(); + }} + dataTestId="copy-button-profile-update" + /> + ) : ( + !loading && ( + <SessionButton + text={window.i18n('save')} + buttonType={SessionButtonType.Simple} + onClick={onClickOK} + disabled={loading} + dataTestId="save-button-profile-update" + /> + ) + )} + </div> + </SessionWrapperModal> + </div> + ); +}; diff --git a/ts/components/dialog/EditProfilePictureModal.tsx b/ts/components/dialog/EditProfilePictureModal.tsx new file mode 100644 index 000000000..2554c420f --- /dev/null +++ b/ts/components/dialog/EditProfilePictureModal.tsx @@ -0,0 +1,164 @@ +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; +import { clearOurAvatar, uploadOurAvatar } from '../../interactions/conversationInteractions'; +import { ToastUtils } from '../../session/utils'; +import { editProfileModal, updateEditProfilePictureModel } from '../../state/ducks/modalDialog'; +import { pickFileForAvatar } from '../../types/attachments/VisualAttachment'; +import { SessionWrapperModal } from '../SessionWrapperModal'; +import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; +import { SessionSpinner } from '../basic/SessionSpinner'; +import { SpacerLG } from '../basic/Text'; +import { SessionIconButton } from '../icon'; +import { ProfileAvatar } from './EditProfileDialog'; + +const StyledAvatarContainer = styled.div` + cursor: pointer; +`; +const UploadImageButton = () => { + return ( + <div style={{ position: 'relative' }}> + <div style={{ borderRadius: '50%', overflow: 'hidden' }}> + <SessionIconButton + iconType="thumbnail" + iconSize="max" + iconPadding="16px" + backgroundColor="var(--chat-buttons-background-color)" + /> + </div> + <SessionIconButton + iconType="plusFat" + iconSize="medium" + iconColor="var(--modal-background-content-color)" + iconPadding="4.5px" + borderRadius="50%" + backgroundColor="var(--primary-color)" + style={{ position: 'absolute', bottom: 0, right: 0 }} + /> + </div> + ); +}; + +const uploadProfileAvatar = async (scaledAvatarUrl: string | null) => { + if (scaledAvatarUrl?.length) { + try { + const blobContent = await (await fetch(scaledAvatarUrl)).blob(); + if (!blobContent || !blobContent.size) { + throw new Error('Failed to fetch blob content from scaled avatar'); + } + await uploadOurAvatar(await blobContent.arrayBuffer()); + } catch (error) { + if (error.message && error.message.length) { + ToastUtils.pushToastError('edit-profile', error.message); + } + window.log.error( + 'showEditProfileDialog Error ensuring that image is properly sized:', + error && error.stack ? error.stack : error + ); + } + } +}; + +export type EditProfilePictureModalProps = { + avatarPath: string | null; + profileName: string | undefined; + ourId: string; +}; + +export const EditProfilePictureModal = (props: EditProfilePictureModalProps) => { + const dispatch = useDispatch(); + + const [newAvatarObjectUrl, setNewAvatarObjectUrl] = useState<string | null>(props.avatarPath); + const [loading, setLoading] = useState(false); + + if (!props) { + return null; + } + + const { avatarPath, profileName, ourId } = props; + + const closeDialog = () => { + dispatch(updateEditProfilePictureModel(null)); + dispatch(editProfileModal({})); + }; + + const handleAvatarClick = async () => { + const updatedAvatarObjectUrl = await pickFileForAvatar(); + if (updatedAvatarObjectUrl) { + setNewAvatarObjectUrl(updatedAvatarObjectUrl); + } + }; + + const handleUpload = async () => { + setLoading(true); + if (newAvatarObjectUrl === avatarPath) { + window.log.debug('Avatar Object URL has not changed!'); + return; + } + + await uploadProfileAvatar(newAvatarObjectUrl); + setLoading(false); + dispatch(updateEditProfilePictureModel(null)); + }; + + const handleRemove = async () => { + setLoading(true); + await clearOurAvatar(); + setNewAvatarObjectUrl(null); + setLoading(false); + dispatch(updateEditProfilePictureModel(null)); + }; + + return ( + <SessionWrapperModal + title={window.i18n('setDisplayPicture')} + onClose={closeDialog} + showHeader={true} + showExitIcon={true} + > + <div + className="avatar-center" + role="button" + onClick={() => void handleAvatarClick()} + data-testid={'image-upload-click'} + > + <StyledAvatarContainer className="avatar-center-inner"> + {newAvatarObjectUrl || avatarPath ? ( + <ProfileAvatar + newAvatarObjectUrl={newAvatarObjectUrl} + avatarPath={avatarPath} + profileName={profileName} + ourId={ourId} + /> + ) : ( + <UploadImageButton /> + )} + </StyledAvatarContainer> + </div> + + {loading ? ( + <SessionSpinner loading={loading} /> + ) : ( + <> + <SpacerLG /> + <div className="session-modal__button-group"> + <SessionButton + text={window.i18n('save')} + buttonType={SessionButtonType.Simple} + onClick={handleUpload} + disabled={newAvatarObjectUrl === avatarPath} + dataTestId="save-button-profile-update" + /> + <SessionButton + text={window.i18n('remove')} + buttonColor={SessionButtonColor.Danger} + buttonType={SessionButtonType.Simple} + onClick={handleRemove} + disabled={!avatarPath} + /> + </div> + </> + )} + </SessionWrapperModal> + ); +}; diff --git a/ts/components/dialog/ModalContainer.tsx b/ts/components/dialog/ModalContainer.tsx index d54bbe7e8..9a0d7b6b7 100644 --- a/ts/components/dialog/ModalContainer.tsx +++ b/ts/components/dialog/ModalContainer.tsx @@ -8,6 +8,7 @@ import { getConfirmModal, getDeleteAccountModalState, getEditProfileDialog, + getEditProfilePictureModalState, getInviteContactModal, getOnionPathDialog, getReactClearAllDialog, @@ -36,6 +37,7 @@ import { SessionNicknameDialog } from './SessionNicknameDialog'; import { BanOrUnBanUserDialog } from './BanOrUnbanUserDialog'; import { ReactListModal } from './ReactListModal'; import { ReactClearAllModal } from './ReactClearAllModal'; +import { EditProfilePictureModal } from './EditProfilePictureModal'; export const ModalContainer = () => { const confirmModalState = useSelector(getConfirmModal); @@ -55,6 +57,7 @@ export const ModalContainer = () => { const banOrUnbanUserModalState = useSelector(getBanOrUnbanUserModalState); const reactListModalState = useSelector(getReactListDialog); const reactClearAllModalState = useSelector(getReactClearAllDialog); + const editProfilePictureModalState = useSelector(getEditProfilePictureModalState); return ( <> @@ -79,6 +82,9 @@ export const ModalContainer = () => { {confirmModalState && <SessionConfirm {...confirmModalState} />} {reactListModalState && <ReactListModal {...reactListModalState} />} {reactClearAllModalState && <ReactClearAllModal {...reactClearAllModalState} />} + {editProfilePictureModalState && ( + <EditProfilePictureModal {...editProfilePictureModalState} /> + )} </> ); }; diff --git a/ts/components/icon/Icons.tsx b/ts/components/icon/Icons.tsx index 9be3ed3bd..2f74e9b94 100644 --- a/ts/components/icon/Icons.tsx +++ b/ts/components/icon/Icons.tsx @@ -66,6 +66,7 @@ export type SessionIconType = | 'doubleCheckCircle' | 'gallery' | 'stop' + | 'thumbnail' | 'timer00' | 'timer05' | 'timer10' @@ -83,7 +84,7 @@ export type SessionIconType = export type SessionIconSize = 'tiny' | 'small' | 'medium' | 'large' | 'huge' | 'huge2' | 'max'; -export const icons = { +export const icons: Record<string, { path: string; viewBox: string; ratio: number }> = { addUser: { path: 'M8.85,2.17c-1.73,0-3.12,1.4-3.12,3.12s1.4,3.12,3.12,3.12c1.73,0,3.13-1.4,3.13-3.12S10.58,2.17,8.85,2.17z M8.85,0.08c2.88,0,5.21,2.33,5.21,5.21s-2.33,5.21-5.21,5.21s-5.2-2.33-5.2-5.21C3.65,2.42,5.98,0.08,8.85,0.08z M20.83,5.29 c0.54,0,0.98,0.41,1.04,0.93l0.01,0.11v2.08h2.08c0.54,0,0.98,0.41,1.04,0.93v0.12c0,0.54-0.41,0.98-0.93,1.04l-0.11,0.01h-2.08 v2.08c0,0.58-0.47,1.04-1.04,1.04c-0.54,0-0.98-0.41-1.04-0.93l-0.01-0.11v-2.08h-2.08c-0.54,0-0.98-0.41-1.04-0.93l-0.01-0.11 c0-0.54,0.41-0.98,0.93-1.04l0.11-0.01h2.08V6.34C19.79,5.76,20.26,5.29,20.83,5.29z M12.5,12.58c2.8,0,5.09,2.21,5.2,4.99v0.22 v2.08c0,0.58-0.47,1.04-1.04,1.04c-0.54,0-0.98-0.41-1.04-0.93l-0.01-0.11v-2.08c0-1.67-1.3-3.03-2.95-3.12h-0.18H5.21 c-1.67,0-3.03,1.3-3.12,2.95v0.18v2.08c0,0.58-0.47,1.04-1.04,1.04c-0.54,0-0.98-0.41-1.04-0.93L0,19.88V17.8 c0-2.8,2.21-5.09,4.99-5.2h0.22h7.29V12.58z', @@ -475,6 +476,12 @@ export const icons = { viewBox: '-1 -1 35 35', ratio: 1, }, + thumbnail: { + path: + 'm34.915 23.899-8.321-6.087a.308.308 0 0 0-.385 0l-7.461 6.177a.334.334 0 0 1-.398 0l-3.75-3.057a.308.308 0 0 0-.372 0L4.07 28.15a.335.335 0 0 0-.129.257v2.337a.745.745 0 0 0 .732.732h29.638a.732.732 0 0 0 .731-.732v-6.6a.281.281 0 0 0-.128-.244Zm-23.82-5.15a2.89 2.89 0 1 0 0-5.778 2.89 2.89 0 0 0 0 5.778ZM40.36 4.624 8.193.874a3.197 3.197 0 0 0-3.57 2.838L4.469 5.1h2.568l.128-1.091a.63.63 0 0 1 .244-.424.642.642 0 0 1 .386-.141h.064L22.15 5.099h13.534a5.137 5.137 0 0 1 4.071 2.042h.308a.642.642 0 0 1 .565.706l-.09.732a5.14 5.14 0 0 1 .283 1.605v18.312L43.185 8.18a3.223 3.223 0 0 0-2.825-3.557Zm-4.675 30.678H3.3a3.21 3.21 0 0 1-3.21-3.21v-21.83a3.21 3.21 0 0 1 3.21-3.21h32.385a3.21 3.21 0 0 1 3.21 3.21v21.83a3.21 3.21 0 0 1-3.21 3.21ZM3.3 9.594a.642.642 0 0 0-.642.642v21.83a.642.642 0 0 0 .642.642h32.385a.642.642 0 0 0 .643-.642v-21.83a.642.642 0 0 0-.643-.642H3.3Z', + viewBox: '0 0 44 36', + ratio: 1, + }, timer00: { path: 'M11.428367,3.44328115 L10.5587469,3.94535651 C10.4906607,3.79477198 10.4145019,3.64614153 10.330127,3.5 C10.2457522,3.35385847 10.1551138,3.21358774 10.0587469,3.07933111 L10.928367,2.57725574 C11.0225793,2.71323387 11.1119641,2.85418158 11.1961524,3 C11.2803407,3.14581842 11.3577126,3.2937018 11.428367,3.44328115 Z M9.42274426,1.07163304 L8.92066889,1.94125309 C8.78641226,1.84488615 8.64614153,1.75424783 8.5,1.66987298 C8.35385847,1.58549813 8.20522802,1.50933927 8.05464349,1.44125309 L8.55671885,0.571633044 C8.7062982,0.642287382 8.85418158,0.719659271 9,0.803847577 C9.14581842,0.888035884 9.28676613,0.977420696 9.42274426,1.07163304 Z M11.9794631,6.5 L10.9753124,6.5 C10.9916403,6.33554688 11,6.1687497 11,6 C11,5.8312503 10.9916403,5.66445312 10.9753124,5.5 L11.9794631,5.5 C11.9930643,5.66486669 12,5.83162339 12,6 C12,6.16837661 11.9930643,6.33513331 11.9794631,6.5 Z M10.928367,9.42274426 L10.0587469,8.92066889 C10.1551138,8.78641226 10.2457522,8.64614153 10.330127,8.5 C10.4145019,8.35385847 10.4906607,8.20522802 10.5587469,8.05464349 L11.428367,8.55671885 C11.3577126,8.7062982 11.2803407,8.85418158 11.1961524,9 C11.1119641,9.14581842 11.0225793,9.28676613 10.928367,9.42274426 Z M8.55671885,11.428367 L8.05464349,10.5587469 C8.20522802,10.4906607 8.35385847,10.4145019 8.5,10.330127 C8.64614153,10.2457522 8.78641226,10.1551138 8.92066889,10.0587469 L9.42274426,10.928367 C9.28676613,11.0225793 9.14581842,11.1119641 9,11.1961524 C8.85418158,11.2803407 8.7062982,11.3577126 8.55671885,11.428367 Z M2.57725574,10.928367 L3.07933111,10.0587469 C3.21358774,10.1551138 3.35385847,10.2457522 3.5,10.330127 C3.64614153,10.4145019 3.79477198,10.4906607 3.94535651,10.5587469 L3.44328115,11.428367 C3.2937018,11.3577126 3.14581842,11.2803407 3,11.1961524 C2.85418158,11.1119641 2.71323387,11.0225793 2.57725574,10.928367 Z M5.5,11.9794631 L5.5,10.9753124 C5.66445312,10.9916403 5.8312503,11 6,11 C6.1687497,11 6.33554688,10.9916403 6.5,10.9753124 L6.5,11.9794631 C6.33513331,11.9930643 6.16837661,12 6,12 C5.83162339,12 5.66486669,11.9930643 5.5,11.9794631 Z M0.571633044,8.55671885 L1.44125309,8.05464349 C1.50933927,8.20522802 1.58549813,8.35385847 1.66987298,8.5 C1.75424783,8.64614153 1.84488615,8.78641226 1.94125309,8.92066889 L1.07163304,9.42274426 C0.977420696,9.28676613 0.888035884,9.14581842 0.803847577,9 C0.719659271,8.85418158 0.642287382,8.7062982 0.571633044,8.55671885 Z M0.0205368885,5.5 L1.02468762,5.5 C1.00835972,5.66445312 1,5.8312503 1,6 C1,6.1687497 1.00835972,6.33554688 1.02468762,6.5 L0.0205368885,6.5 C0.00693566443,6.33513331 -9.95062878e-13,6.16837661 -9.95093808e-13,6 C-9.95124738e-13,5.83162339 0.00693566443,5.66486669 0.0205368885,5.5 Z M1.07163304,2.57725574 L1.94125309,3.07933111 C1.84488615,3.21358774 1.75424783,3.35385847 1.66987298,3.5 C1.58549813,3.64614153 1.50933927,3.79477198 1.44125309,3.94535651 L0.571633044,3.44328115 C0.642287382,3.2937018 0.719659271,3.14581842 0.803847577,3 C0.888035884,2.85418158 0.977420696,2.71323387 1.07163304,2.57725574 Z M3.44328115,0.571633044 L3.94535651,1.44125309 C3.79477198,1.50933927 3.64614153,1.58549813 3.5,1.66987298 C3.35385847,1.75424783 3.21358774,1.84488615 3.07933111,1.94125309 L2.57725574,1.07163304 C2.71323387,0.977420696 2.85418158,0.888035884 3,0.803847577 C3.14581842,0.719659271 3.2937018,0.642287382 3.44328115,0.571633044 Z M6.5,0.0205368885 L6.5,7 L5.5,7 L5.5,0.0205368885 C5.66486669,0.00693566443 5.83162339,5.01e-14 6,5.01e-14 C6.16837661,5.01e-14 6.33513331,0.00693566443 6.5,0.0205368885 Z', @@ -536,10 +543,8 @@ export const icons = { ratio: 1, }, timer50: { - path: [ - 'M8.49999998,1.66987313 L8.99999998,0.80384773 C10.3298113,1.57161469 11.3667294,2.84668755 11.7955548,4.4470858 C12.6532057,7.64788242 10.7537107,10.9379042 7.5529141,11.795555 C4.35211748,12.6532059 1.06209574,10.753711 0.204444873,7.55291434 C-0.27253249,5.77281059 0.103264647,3.96510985 1.08141192,2.56355986 L1.94944135,3.0683506 L1.94144625,3.07944279 L6.25000012,5.56698754 L5.75000012,6.43301294 L1.44144624,3.9454682 C0.98322086,4.96059039 0.85962347,6.13437085 1.1703707,7.29409529 C1.88507976,9.96142581 4.62676454,11.5443383 7.29409506,10.8296292 C9.96142557,10.1149201 11.544338,7.37323536 10.829629,4.70590484 C10.4722744,3.37223964 9.60817605,2.30967894 8.49999998,1.66987313 Z', - 'M6.00250506,1.00000061 L6,1 C5.8312503,1 5.66445312,1.00835972 5.5,1.02468762 L5.5,0.0205368885 C5.66486669,0.00693566443 5.83162339,0 6,0 L6.00250686,5.12480482e-07 C9.31506271,0.0013544265 12,2.68712686 12,6 L11,6 C11,3.23941132 8.76277746,1.00135396 6.00250506,1.00000061 Z M3.44328115,0.571633044 L3.94535651,1.44125309 C3.79477198,1.50933927 3.64614153,1.58549813 3.5,1.66987298 C3.35385847,1.75424783 3.21358774,1.84488615 3.07933111,1.94125309 L2.57725574,1.07163304 C2.71323387,0.977420696 2.85418158,0.888035884 3,0.803847577 C3.14581842,0.719659271 3.2937018,0.642287382 3.44328115,0.571633044 Z', - ], + path: + 'M8.49999998,1.66987313 L8.99999998,0.80384773 C10.3298113,1.57161469 11.3667294,2.84668755 11.7955548,4.4470858 C12.6532057,7.64788242 10.7537107,10.9379042 7.5529141,11.795555 C4.35211748,12.6532059 1.06209574,10.753711 0.204444873,7.55291434 C-0.27253249,5.77281059 0.103264647,3.96510985 1.08141192,2.56355986 L1.94944135,3.0683506 L1.94144625,3.07944279 L6.25000012,5.56698754 L5.75000012,6.43301294 L1.44144624,3.9454682 C0.98322086,4.96059039 0.85962347,6.13437085 1.1703707,7.29409529 C1.88507976,9.96142581 4.62676454,11.5443383 7.29409506,10.8296292 C9.96142557,10.1149201 11.544338,7.37323536 10.829629,4.70590484 C10.4722744,3.37223964 9.60817605,2.30967894 8.49999998,1.66987313 Z M6.00250506,1.00000061 L6,1 C5.8312503,1 5.66445312,1.00835972 5.5,1.02468762 L5.5,0.0205368885 C5.66486669,0.00693566443 5.83162339,0 6,0 L6.00250686,5.12480482e-07 C9.31506271,0.0013544265 12,2.68712686 12,6 L11,6 C11,3.23941132 8.76277746,1.00135396 6.00250506,1.00000061 Z M3.44328115,0.571633044 L3.94535651,1.44125309 C3.79477198,1.50933927 3.64614153,1.58549813 3.5,1.66987298 C3.35385847,1.75424783 3.21358774,1.84488615 3.07933111,1.94125309 L2.57725574,1.07163304 C2.71323387,0.977420696 2.85418158,0.888035884 3,0.803847577 C3.14581842,0.719659271 3.2937018,0.642287382 3.44328115,0.571633044 Z', viewBox: '0 0 12 12', ratio: 1, }, diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 8ded7592b..6d4f6b60f 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -1,3 +1,4 @@ +import { isNil } from 'lodash'; import { ConversationNotificationSettingType, ConversationTypeEnum, @@ -487,6 +488,37 @@ export async function uploadOurAvatar(newAvatarDecrypted?: ArrayBuffer) { }; } +/** + * This function can be used for clearing our avatar. + */ +export async function clearOurAvatar(commit: boolean = true) { + const ourConvo = getConversationController().get(UserUtils.getOurPubKeyStrFromCache()); + if (!ourConvo) { + window.log.warn('ourConvo not found... This is not a valid case'); + return; + } + + // return early if no change are needed at all + if ( + isNil(ourConvo.get('avatarPointer')) && + isNil(ourConvo.get('avatarInProfile')) && + isNil(ourConvo.get('profileKey')) + ) { + return; + } + + ourConvo.set('avatarPointer', undefined); + ourConvo.set('avatarInProfile', undefined); + ourConvo.set('profileKey', undefined); + + await setLastProfileUpdateTimestamp(Date.now()); + + if (commit) { + await ourConvo.commit(); + await SyncUtils.forceSyncConfigurationNowIfNeeded(true); + } +} + export async function replyToMessage(messageId: string) { const quotedMessageModel = await Data.getMessageById(messageId); if (!quotedMessageModel) { diff --git a/ts/state/ducks/modalDialog.tsx b/ts/state/ducks/modalDialog.tsx index be1cb8b92..128606d87 100644 --- a/ts/state/ducks/modalDialog.tsx +++ b/ts/state/ducks/modalDialog.tsx @@ -1,6 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { SessionConfirmDialogProps } from '../../components/dialog/SessionConfirm'; import { PasswordAction } from '../../components/dialog/SessionPasswordDialog'; +import { EditProfilePictureModalProps } from '../../components/dialog/EditProfilePictureModal'; import { Noop } from '../../types/Util'; export type BanType = 'ban' | 'unban'; @@ -36,6 +37,8 @@ export type ReactModalsState = { messageId: string; } | null; +export type EditProfilePictureModalState = EditProfilePictureModalProps | null; + export type ModalState = { confirmModal: ConfirmModalState; inviteContactModal: InviteContactModalState; @@ -54,6 +57,7 @@ export type ModalState = { deleteAccountModal: DeleteAccountModalState; reactListModalState: ReactModalsState; reactClearAllModalState: ReactModalsState; + editProfilePictureModalState: EditProfilePictureModalState; }; export const initialModalState: ModalState = { @@ -74,6 +78,7 @@ export const initialModalState: ModalState = { deleteAccountModal: null, reactListModalState: null, reactClearAllModalState: null, + editProfilePictureModalState: null, }; const ModalSlice = createSlice({ @@ -131,6 +136,9 @@ const ModalSlice = createSlice({ updateReactClearAllModal(state, action: PayloadAction<ReactModalsState>) { return { ...state, reactClearAllModalState: action.payload }; }, + updateEditProfilePictureModel(state, action: PayloadAction<EditProfilePictureModalState>) { + return { ...state, editProfilePictureModalState: action.payload }; + }, }, }); @@ -153,5 +161,6 @@ export const { updateBanOrUnbanUserModal, updateReactListModal, updateReactClearAllModal, + updateEditProfilePictureModel, } = actions; export const modalReducer = reducer; diff --git a/ts/state/selectors/modal.ts b/ts/state/selectors/modal.ts index f959bc243..259aad8b5 100644 --- a/ts/state/selectors/modal.ts +++ b/ts/state/selectors/modal.ts @@ -9,6 +9,7 @@ import { ConfirmModalState, DeleteAccountModalState, EditProfileModalState, + EditProfilePictureModalState, InviteContactModalState, ModalState, OnionPathModalState, @@ -109,3 +110,8 @@ export const getReactClearAllDialog = createSelector( getModal, (state: ModalState): ReactModalsState => state.reactClearAllModalState ); + +export const getEditProfilePictureModalState = createSelector( + getModal, + (state: ModalState): EditProfilePictureModalState => state.editProfilePictureModalState +); diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 592b1f592..ed5d4ad7f 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -1,512 +1,513 @@ export type LocalizerKeys = - | 'copyErrorAndQuit' - | 'unknown' - | 'databaseError' - | 'mainMenuFile' - | 'mainMenuEdit' - | 'mainMenuView' - | 'mainMenuWindow' - | 'mainMenuHelp' + | 'ByUsingThisService...' + | 'about' + | 'accept' + | 'activeMembers' + | 'add' + | 'addACaption' + | 'addAsModerator' + | 'addModerators' + | 'addingContacts' + | 'allUsersAreRandomly...' + | 'anonymous' + | 'answeredACall' | 'appMenuHide' | 'appMenuHideOthers' - | 'appMenuUnhide' | 'appMenuQuit' - | 'editMenuUndo' - | 'editMenuRedo' - | 'editMenuCut' - | 'editMenuCopy' - | 'editMenuPaste' - | 'editMenuDeleteContact' - | 'editMenuDeleteGroup' - | 'editMenuSelectAll' - | 'windowMenuClose' - | 'windowMenuMinimize' - | 'windowMenuZoom' - | 'viewMenuResetZoom' - | 'viewMenuZoomIn' - | 'viewMenuZoomOut' - | 'viewMenuToggleFullScreen' - | 'viewMenuToggleDevTools' - | 'contextMenuNoSuggestions' - | 'openGroupInvitation' - | 'joinOpenGroupAfterInvitationConfirmationTitle' - | 'joinOpenGroupAfterInvitationConfirmationDesc' - | 'couldntFindServerMatching' - | 'enterSessionIDOrONSName' - | 'startNewConversationBy...' - | 'loading' - | 'done' - | 'youLeftTheGroup' - | 'youGotKickedFromGroup' - | 'unreadMessages' - | 'debugLogExplanation' - | 'reportIssue' - | 'markAllAsRead' - | 'incomingError' - | 'media' - | 'mediaEmptyState' - | 'document' - | 'documents' - | 'documentsEmptyState' - | 'today' - | 'yesterday' - | 'thisWeek' - | 'thisMonth' - | 'voiceMessage' - | 'stagedPreviewThumbnail' - | 'previewThumbnail' - | 'stagedImageAttachment' - | 'oneNonImageAtATimeToast' - | 'cannotMixImageAndNonImageAttachments' - | 'maximumAttachments' - | 'fileSizeWarning' - | 'unableToLoadAttachment' - | 'offline' - | 'debugLog' - | 'showDebugLog' - | 'shareBugDetails' - | 'goToReleaseNotes' - | 'goToSupportPage' - | 'about' - | 'show' - | 'sessionMessenger' - | 'noSearchResults' - | 'conversationsHeader' - | 'contactsHeader' - | 'messagesHeader' - | 'searchMessagesHeader' - | 'settingsHeader' - | 'typingAlt' - | 'contactAvatarAlt' - | 'downloadAttachment' - | 'replyToMessage' - | 'replyingToMessage' - | 'originalMessageNotFound' - | 'you' - | 'audioPermissionNeededTitle' - | 'audioPermissionNeeded' - | 'image' + | 'appMenuUnhide' + | 'appearanceSettingsTitle' + | 'areYouSureClearDevice' + | 'areYouSureDeleteDeviceOnly' + | 'areYouSureDeleteEntireAccount' | 'audio' - | 'video' - | 'photo' + | 'audioMessageAutoplayDescription' + | 'audioMessageAutoplayTitle' + | 'audioNotificationsSettingsTitle' + | 'audioPermissionNeeded' + | 'audioPermissionNeededTitle' + | 'autoUpdateDownloadButtonLabel' + | 'autoUpdateDownloadInstructions' + | 'autoUpdateDownloadedMessage' + | 'autoUpdateLaterButtonLabel' + | 'autoUpdateNewVersionInstructions' + | 'autoUpdateNewVersionMessage' + | 'autoUpdateNewVersionTitle' + | 'autoUpdateRestartButtonLabel' + | 'autoUpdateSettingDescription' + | 'autoUpdateSettingTitle' + | 'banUser' + | 'banUserAndDeleteAll' + | 'beginYourSession' + | 'blindedMsgReqsSettingDesc' + | 'blindedMsgReqsSettingTitle' + | 'block' + | 'blocked' + | 'blockedSettingsTitle' + | 'callMediaPermissionsDescription' + | 'callMediaPermissionsDialogContent' + | 'callMediaPermissionsDialogTitle' + | 'callMediaPermissionsTitle' + | 'callMissed' + | 'callMissedCausePermission' + | 'callMissedNotApproved' + | 'callMissedTitle' + | 'cameraPermissionNeeded' + | 'cameraPermissionNeededTitle' + | 'cancel' + | 'cannotMixImageAndNonImageAttachments' + | 'cannotRemoveCreatorFromGroup' + | 'cannotRemoveCreatorFromGroupDesc' | 'cannotUpdate' | 'cannotUpdateDetail' - | 'ok' - | 'cancel' + | 'changeAccountPasswordDescription' + | 'changeAccountPasswordTitle' + | 'changeNickname' + | 'changeNicknameMessage' + | 'changePassword' + | 'changePasswordInvalid' + | 'changePasswordTitle' + | 'changePasswordToastDescription' + | 'chooseAnAction' + | 'classicDarkThemeTitle' + | 'classicLightThemeTitle' + | 'clear' + | 'clearAll' + | 'clearAllConfirmationBody' + | 'clearAllConfirmationTitle' + | 'clearAllData' + | 'clearAllReactions' + | 'clearDataSettingsTitle' + | 'clearDevice' + | 'clearNickname' + | 'clickToTrustContact' | 'close' + | 'closedGroupInviteFailMessage' + | 'closedGroupInviteFailMessagePlural' + | 'closedGroupInviteFailTitle' + | 'closedGroupInviteFailTitlePlural' + | 'closedGroupInviteOkText' + | 'closedGroupInviteSuccessMessage' + | 'closedGroupInviteSuccessTitle' + | 'closedGroupInviteSuccessTitlePlural' + | 'closedGroupMaxSize' + | 'confirmNewPassword' + | 'confirmPassword' + | 'connectToServerFail' + | 'connectToServerSuccess' + | 'connectingToServer' + | 'contactAvatarAlt' + | 'contactsHeader' + | 'contextMenuNoSuggestions' | 'continue' - | 'error' + | 'continueYourSession' + | 'conversationsHeader' + | 'conversationsSettingsTitle' + | 'copiedToClipboard' + | 'copyErrorAndQuit' + | 'copyMessage' + | 'copyOpenGroupURL' + | 'copySessionID' + | 'couldntFindServerMatching' + | 'create' + | 'createAccount' + | 'createClosedGroupNamePrompt' + | 'createClosedGroupPlaceholder' + | 'createConversationNewContact' + | 'createConversationNewGroup' + | 'createGroup' + | 'createPassword' + | 'createSessionID' + | 'databaseError' + | 'debugLog' + | 'debugLogExplanation' + | 'decline' + | 'declineRequestMessage' | 'delete' - | 'messageDeletionForbidden' - | 'deleteJustForMe' + | 'deleteAccountFromLogin' + | 'deleteAccountWarning' + | 'deleteContactConfirmation' + | 'deleteConversation' + | 'deleteConversationConfirmation' | 'deleteForEveryone' - | 'deleteMessagesQuestion' + | 'deleteJustForMe' | 'deleteMessageQuestion' | 'deleteMessages' - | 'deleteConversation' + | 'deleteMessagesQuestion' | 'deleted' - | 'messageDeletedPlaceholder' - | 'from' - | 'to' - | 'sent' - | 'received' - | 'sendMessage' - | 'groupMembers' - | 'moreInformation' - | 'resend' - | 'deleteConversationConfirmation' - | 'clear' - | 'clearAllData' - | 'deleteAccountWarning' - | 'deleteAccountFromLogin' - | 'deleteContactConfirmation' - | 'quoteThumbnailAlt' - | 'imageAttachmentAlt' - | 'videoAttachmentAlt' - | 'lightboxImageAlt' - | 'imageCaptionIconAlt' - | 'addACaption' - | 'copySessionID' - | 'copyOpenGroupURL' - | 'save' - | 'saveLogToDesktop' - | 'saved' - | 'tookAScreenshot' - | 'savedTheFile' - | 'linkPreviewsTitle' - | 'linkPreviewDescription' - | 'linkPreviewsConfirmMessage' - | 'mediaPermissionsTitle' - | 'mediaPermissionsDescription' - | 'spellCheckTitle' - | 'spellCheckDescription' - | 'spellCheckDirty' - | 'readReceiptSettingDescription' - | 'readReceiptSettingTitle' - | 'typingIndicatorsSettingDescription' - | 'typingIndicatorsSettingTitle' - | 'zoomFactorSettingTitle' - | 'themesSettingTitle' - | 'primaryColor' - | 'primaryColorGreen' - | 'primaryColorBlue' - | 'primaryColorYellow' - | 'primaryColorPink' - | 'primaryColorPurple' - | 'primaryColorOrange' - | 'primaryColorRed' - | 'classicDarkThemeTitle' - | 'classicLightThemeTitle' - | 'oceanDarkThemeTitle' - | 'oceanLightThemeTitle' - | 'pruneSettingTitle' - | 'pruneSettingDescription' - | 'enable' - | 'keepDisabled' - | 'notificationSettingsDialog' - | 'nameAndMessage' - | 'noNameOrMessage' - | 'nameOnly' - | 'newMessage' - | 'createConversationNewContact' - | 'createConversationNewGroup' - | 'joinACommunity' - | 'chooseAnAction' - | 'newMessages' - | 'notificationMostRecentFrom' - | 'notificationFrom' - | 'notificationMostRecent' - | 'sendFailed' - | 'mediaMessage' - | 'messageBodyMissing' - | 'messageBody' - | 'unblockToSend' - | 'youChangedTheTimer' - | 'timerSetOnSync' - | 'theyChangedTheTimer' - | 'timerOption_0_seconds' - | 'timerOption_5_seconds' - | 'timerOption_10_seconds' - | 'timerOption_30_seconds' - | 'timerOption_1_minute' - | 'timerOption_5_minutes' - | 'timerOption_30_minutes' - | 'timerOption_1_hour' - | 'timerOption_6_hours' - | 'timerOption_12_hours' - | 'timerOption_1_day' - | 'timerOption_1_week' - | 'timerOption_2_weeks' + | 'destination' + | 'device' + | 'deviceOnly' + | 'dialogClearAllDataDeletionFailedDesc' + | 'dialogClearAllDataDeletionFailedMultiple' + | 'dialogClearAllDataDeletionFailedTitle' + | 'dialogClearAllDataDeletionFailedTitleQuestion' + | 'dialogClearAllDataDeletionQuestion' + | 'disabledDisappearingMessages' | 'disappearingMessages' - | 'changeNickname' - | 'clearNickname' - | 'nicknamePlaceholder' - | 'changeNicknameMessage' - | 'timerOption_0_seconds_abbreviated' - | 'timerOption_5_seconds_abbreviated' - | 'timerOption_10_seconds_abbreviated' - | 'timerOption_30_seconds_abbreviated' - | 'timerOption_1_minute_abbreviated' - | 'timerOption_5_minutes_abbreviated' - | 'timerOption_30_minutes_abbreviated' - | 'timerOption_1_hour_abbreviated' - | 'timerOption_6_hours_abbreviated' - | 'timerOption_12_hours_abbreviated' - | 'timerOption_1_day_abbreviated' - | 'timerOption_1_week_abbreviated' - | 'timerOption_2_weeks_abbreviated' | 'disappearingMessagesDisabled' - | 'disabledDisappearingMessages' - | 'youDisabledDisappearingMessages' - | 'timerSetTo' - | 'noteToSelf' - | 'hideMenuBarTitle' + | 'displayName' + | 'displayNameEmpty' + | 'displayNameTooLong' + | 'document' + | 'documents' + | 'documentsEmptyState' + | 'done' + | 'downloadAttachment' + | 'editGroup' + | 'editGroupName' + | 'editMenuCopy' + | 'editMenuCut' + | 'editMenuDeleteContact' + | 'editMenuDeleteGroup' + | 'editMenuPaste' + | 'editMenuRedo' + | 'editMenuSelectAll' + | 'editMenuUndo' + | 'editProfileModalTitle' + | 'emptyGroupNameError' + | 'enable' + | 'endCall' + | 'enterAnOpenGroupURL' + | 'enterDisplayName' + | 'enterNewPassword' + | 'enterPassword' + | 'enterRecoveryPhrase' + | 'enterSessionID' + | 'enterSessionIDOfRecipient' + | 'enterSessionIDOrONSName' + | 'entireAccount' + | 'error' + | 'establishingConnection' + | 'expandedReactionsText' + | 'failedResolveOns' + | 'failedToAddAsModerator' + | 'failedToRemoveFromModerator' + | 'faq' + | 'fileSizeWarning' + | 'from' + | 'getStarted' + | 'goToReleaseNotes' + | 'goToSupportPage' + | 'groupMembers' + | 'groupNamePlaceholder' + | 'helpSettingsTitle' + | 'helpUsTranslateSession' + | 'hideBanner' | 'hideMenuBarDescription' - | 'startConversation' + | 'hideMenuBarTitle' + | 'hideRequestBanner' + | 'hideRequestBannerDescription' + | 'iAmSure' + | 'image' + | 'imageAttachmentAlt' + | 'imageCaptionIconAlt' + | 'incomingCallFrom' + | 'incomingError' + | 'invalidGroupNameTooLong' + | 'invalidGroupNameTooShort' | 'invalidNumberError' - | 'failedResolveOns' - | 'autoUpdateSettingTitle' - | 'autoUpdateSettingDescription' - | 'autoUpdateNewVersionTitle' - | 'autoUpdateNewVersionMessage' - | 'autoUpdateNewVersionInstructions' - | 'autoUpdateRestartButtonLabel' - | 'autoUpdateLaterButtonLabel' - | 'autoUpdateDownloadButtonLabel' - | 'autoUpdateDownloadedMessage' - | 'autoUpdateDownloadInstructions' - | 'leftTheGroup' - | 'multipleLeftTheGroup' - | 'updatedTheGroup' - | 'titleIsNow' + | 'invalidOldPassword' + | 'invalidOpenGroupUrl' + | 'invalidPassword' + | 'invalidPubkeyFormat' + | 'invalidSessionId' + | 'inviteContacts' + | 'join' + | 'joinACommunity' + | 'joinOpenGroup' + | 'joinOpenGroupAfterInvitationConfirmationDesc' + | 'joinOpenGroupAfterInvitationConfirmationTitle' | 'joinedTheGroup' - | 'multipleJoinedTheGroup' + | 'keepDisabled' | 'kickedFromTheGroup' - | 'multipleKickedFromTheGroup' - | 'block' - | 'unblock' - | 'unblocked' - | 'blocked' - | 'blockedSettingsTitle' - | 'conversationsSettingsTitle' - | 'unbanUser' - | 'userUnbanned' - | 'userUnbanFailed' - | 'banUser' - | 'banUserAndDeleteAll' - | 'userBanned' - | 'userBanFailed' - | 'leaveGroup' + | 'learnMore' | 'leaveAndRemoveForEveryone' + | 'leaveGroup' | 'leaveGroupConfirmation' | 'leaveGroupConfirmationAdmin' - | 'cannotRemoveCreatorFromGroup' - | 'cannotRemoveCreatorFromGroupDesc' - | 'noContactsForGroup' - | 'failedToAddAsModerator' - | 'failedToRemoveFromModerator' - | 'copyMessage' - | 'selectMessage' - | 'editGroup' - | 'editGroupName' - | 'updateGroupDialogTitle' - | 'showRecoveryPhrase' - | 'yourSessionID' - | 'setAccountPasswordTitle' - | 'setAccountPasswordDescription' - | 'changeAccountPasswordTitle' - | 'changeAccountPasswordDescription' - | 'removeAccountPasswordTitle' - | 'removeAccountPasswordDescription' - | 'enterPassword' - | 'confirmPassword' - | 'enterNewPassword' - | 'confirmNewPassword' - | 'showRecoveryPhrasePasswordRequest' - | 'recoveryPhraseSavePromptMain' - | 'invalidOpenGroupUrl' - | 'copiedToClipboard' - | 'passwordViewTitle' - | 'password' - | 'setPassword' - | 'changePassword' - | 'createPassword' - | 'removePassword' + | 'leftTheGroup' + | 'lightboxImageAlt' + | 'linkDevice' + | 'linkPreviewDescription' + | 'linkPreviewsConfirmMessage' + | 'linkPreviewsTitle' + | 'linkVisitWarningMessage' + | 'linkVisitWarningTitle' + | 'loading' + | 'mainMenuEdit' + | 'mainMenuFile' + | 'mainMenuHelp' + | 'mainMenuView' + | 'mainMenuWindow' + | 'markAllAsRead' + | 'markUnread' | 'maxPasswordAttempts' - | 'typeInOldPassword' - | 'invalidOldPassword' - | 'invalidPassword' - | 'noGivenPassword' - | 'passwordsDoNotMatch' - | 'setPasswordInvalid' - | 'changePasswordInvalid' - | 'removePasswordInvalid' - | 'setPasswordTitle' - | 'changePasswordTitle' - | 'removePasswordTitle' - | 'setPasswordToastDescription' - | 'changePasswordToastDescription' - | 'removePasswordToastDescription' - | 'publicChatExists' - | 'connectToServerFail' - | 'connectingToServer' - | 'connectToServerSuccess' - | 'setPasswordFail' - | 'passwordLengthError' - | 'passwordTypeError' - | 'passwordCharacterError' - | 'remove' - | 'invalidSessionId' - | 'invalidPubkeyFormat' - | 'emptyGroupNameError' - | 'editProfileModalTitle' - | 'groupNamePlaceholder' - | 'inviteContacts' - | 'addModerators' - | 'removeModerators' - | 'addAsModerator' - | 'removeFromModerators' - | 'add' - | 'addingContacts' - | 'noContactsToAdd' - | 'noMembersInThisGroup' - | 'noModeratorsToRemove' - | 'onlyAdminCanRemoveMembers' - | 'onlyAdminCanRemoveMembersDesc' - | 'createAccount' - | 'startInTrayTitle' - | 'startInTrayDescription' - | 'yourUniqueSessionID' - | 'allUsersAreRandomly...' - | 'getStarted' - | 'createSessionID' - | 'recoveryPhrase' - | 'enterRecoveryPhrase' - | 'displayName' - | 'anonymous' - | 'removeResidueMembers' - | 'enterDisplayName' - | 'continueYourSession' - | 'linkDevice' - | 'restoreUsingRecoveryPhrase' - | 'or' - | 'ByUsingThisService...' - | 'beginYourSession' - | 'welcomeToYourSession' - | 'searchFor...' - | 'searchForContactsOnly' - | 'enterSessionID' - | 'enterSessionIDOfRecipient' - | 'message' - | 'appearanceSettingsTitle' - | 'privacySettingsTitle' - | 'notificationsSettingsTitle' - | 'audioNotificationsSettingsTitle' - | 'notificationsSettingsContent' - | 'notificationPreview' - | 'recoveryPhraseEmpty' - | 'displayNameEmpty' - | 'displayNameTooLong' + | 'maximumAttachments' + | 'media' + | 'mediaEmptyState' + | 'mediaMessage' + | 'mediaPermissionsDescription' + | 'mediaPermissionsTitle' | 'members' - | 'activeMembers' - | 'join' - | 'joinOpenGroup' - | 'createGroup' - | 'create' - | 'createClosedGroupNamePrompt' - | 'createClosedGroupPlaceholder' - | 'openGroupURL' - | 'enterAnOpenGroupURL' + | 'message' + | 'messageBody' + | 'messageBodyMissing' + | 'messageDeletedPlaceholder' + | 'messageDeletionForbidden' + | 'messageRequestAccepted' + | 'messageRequestAcceptedOurs' + | 'messageRequestAcceptedOursNoName' + | 'messageRequestPending' + | 'messageRequests' + | 'messagesHeader' + | 'moreInformation' + | 'multipleJoinedTheGroup' + | 'multipleKickedFromTheGroup' + | 'multipleLeftTheGroup' + | 'mustBeApproved' + | 'nameAndMessage' + | 'nameOnly' + | 'newMessage' + | 'newMessages' | 'next' - | 'invalidGroupNameTooShort' - | 'invalidGroupNameTooLong' - | 'pickClosedGroupMember' - | 'closedGroupMaxSize' + | 'nicknamePlaceholder' + | 'noAudioInputFound' + | 'noAudioOutputFound' | 'noBlockedContacts' - | 'userAddedToModerators' - | 'userRemovedFromModerators' - | 'orJoinOneOfThese' - | 'helpUsTranslateSession' - | 'closedGroupInviteFailTitle' - | 'closedGroupInviteFailTitlePlural' - | 'closedGroupInviteFailMessage' - | 'closedGroupInviteFailMessagePlural' - | 'closedGroupInviteOkText' - | 'closedGroupInviteSuccessTitlePlural' - | 'closedGroupInviteSuccessTitle' - | 'closedGroupInviteSuccessMessage' + | 'noCameraFound' + | 'noContactsForGroup' + | 'noContactsToAdd' + | 'noGivenPassword' + | 'noMediaUntilApproved' + | 'noMembersInThisGroup' + | 'noMessageRequestsPending' + | 'noMessagesInBlindedDisabledMsgRequests' + | 'noMessagesInEverythingElse' + | 'noMessagesInNoteToSelf' + | 'noMessagesInReadOnly' + | 'noModeratorsToRemove' + | 'noNameOrMessage' + | 'noSearchResults' + | 'noteToSelf' | 'notificationForConvo' | 'notificationForConvo_all' | 'notificationForConvo_disabled' | 'notificationForConvo_mentions_only' - | 'onionPathIndicatorTitle' + | 'notificationFrom' + | 'notificationMostRecent' + | 'notificationMostRecentFrom' + | 'notificationPreview' + | 'notificationSettingsDialog' + | 'notificationSubtitle' + | 'notificationsSettingsContent' + | 'notificationsSettingsTitle' + | 'oceanDarkThemeTitle' + | 'oceanLightThemeTitle' + | 'offline' + | 'ok' + | 'oneNonImageAtATimeToast' | 'onionPathIndicatorDescription' - | 'unknownCountry' - | 'device' - | 'destination' - | 'learnMore' - | 'linkVisitWarningTitle' - | 'linkVisitWarningMessage' + | 'onionPathIndicatorTitle' + | 'onlyAdminCanRemoveMembers' + | 'onlyAdminCanRemoveMembersDesc' | 'open' - | 'audioMessageAutoplayTitle' - | 'audioMessageAutoplayDescription' - | 'clickToTrustContact' - | 'trustThisContactDialogTitle' - | 'trustThisContactDialogDescription' + | 'openGroupInvitation' + | 'openGroupURL' + | 'openMessageRequestInbox' + | 'openMessageRequestInboxDescription' + | 'or' + | 'orJoinOneOfThese' + | 'originalMessageNotFound' + | 'otherPlural' + | 'otherSingular' + | 'password' + | 'passwordCharacterError' + | 'passwordLengthError' + | 'passwordTypeError' + | 'passwordViewTitle' + | 'passwordsDoNotMatch' + | 'permissionsSettingsTitle' + | 'photo' + | 'pickClosedGroupMember' | 'pinConversation' - | 'unpinConversation' - | 'markUnread' - | 'showUserDetails' - | 'sendRecoveryPhraseTitle' - | 'sendRecoveryPhraseMessage' - | 'dialogClearAllDataDeletionFailedTitle' - | 'dialogClearAllDataDeletionFailedDesc' - | 'dialogClearAllDataDeletionFailedTitleQuestion' - | 'dialogClearAllDataDeletionFailedMultiple' - | 'dialogClearAllDataDeletionQuestion' - | 'clearDevice' - | 'tryAgain' - | 'areYouSureClearDevice' - | 'deviceOnly' - | 'entireAccount' - | 'areYouSureDeleteDeviceOnly' - | 'areYouSureDeleteEntireAccount' - | 'iAmSure' - | 'recoveryPhraseSecureTitle' - | 'recoveryPhraseRevealMessage' + | 'pleaseWaitOpenAndOptimizeDb' + | 'previewThumbnail' + | 'primaryColor' + | 'primaryColorBlue' + | 'primaryColorGreen' + | 'primaryColorOrange' + | 'primaryColorPink' + | 'primaryColorPurple' + | 'primaryColorRed' + | 'primaryColorYellow' + | 'privacySettingsTitle' + | 'pruneSettingDescription' + | 'pruneSettingTitle' + | 'publicChatExists' + | 'quoteThumbnailAlt' + | 'rateLimitReactMessage' + | 'reactionListCountPlural' + | 'reactionListCountSingular' + | 'reactionNotification' + | 'reactionPopup' + | 'reactionPopupMany' + | 'reactionPopupOne' + | 'reactionPopupThree' + | 'reactionPopupTwo' + | 'readReceiptSettingDescription' + | 'readReceiptSettingTitle' + | 'received' + | 'recoveryPhrase' + | 'recoveryPhraseEmpty' | 'recoveryPhraseRevealButtonText' - | 'notificationSubtitle' - | 'surveyTitle' - | 'faq' - | 'support' - | 'clearAll' - | 'clearDataSettingsTitle' - | 'messageRequests' - | 'blindedMsgReqsSettingTitle' - | 'blindedMsgReqsSettingDesc' - | 'requestsSubtitle' + | 'recoveryPhraseRevealMessage' + | 'recoveryPhraseSavePromptMain' + | 'recoveryPhraseSecureTitle' + | 'remove' + | 'removeAccountPasswordDescription' + | 'removeAccountPasswordTitle' + | 'removeFromModerators' + | 'removeModerators' + | 'removePassword' + | 'removePasswordInvalid' + | 'removePasswordTitle' + | 'removePasswordToastDescription' + | 'removeResidueMembers' + | 'replyToMessage' + | 'replyingToMessage' + | 'reportIssue' | 'requestsPlaceholder' - | 'hideRequestBannerDescription' - | 'incomingCallFrom' + | 'requestsSubtitle' + | 'resend' + | 'respondingToRequestWarning' + | 'restoreUsingRecoveryPhrase' | 'ringing' - | 'establishingConnection' - | 'accept' - | 'decline' - | 'endCall' - | 'permissionsSettingsTitle' - | 'helpSettingsTitle' - | 'cameraPermissionNeededTitle' - | 'cameraPermissionNeeded' - | 'unableToCall' - | 'unableToCallTitle' - | 'callMissed' - | 'callMissedTitle' - | 'noCameraFound' - | 'noAudioInputFound' - | 'noAudioOutputFound' - | 'callMediaPermissionsTitle' - | 'callMissedCausePermission' - | 'callMissedNotApproved' - | 'callMediaPermissionsDescription' - | 'callMediaPermissionsDialogContent' - | 'callMediaPermissionsDialogTitle' + | 'save' + | 'saveLogToDesktop' + | 'saved' + | 'savedTheFile' + | 'searchFor...' + | 'searchForContactsOnly' + | 'searchMessagesHeader' + | 'selectMessage' + | 'sendFailed' + | 'sendMessage' + | 'sendRecoveryPhraseMessage' + | 'sendRecoveryPhraseTitle' + | 'sent' + | 'sessionMessenger' + | 'setAccountPasswordDescription' + | 'setAccountPasswordTitle' + | 'setDisplayPicture' + | 'setPassword' + | 'setPasswordFail' + | 'setPasswordInvalid' + | 'setPasswordTitle' + | 'setPasswordToastDescription' + | 'settingsHeader' + | 'shareBugDetails' + | 'show' + | 'showDebugLog' + | 'showRecoveryPhrase' + | 'showRecoveryPhrasePasswordRequest' + | 'showUserDetails' + | 'someOfYourDeviceUseOutdatedVersion' + | 'spellCheckDescription' + | 'spellCheckDirty' + | 'spellCheckTitle' + | 'stagedImageAttachment' + | 'stagedPreviewThumbnail' + | 'startConversation' + | 'startInTrayDescription' + | 'startInTrayTitle' + | 'startNewConversationBy...' | 'startedACall' - | 'answeredACall' + | 'support' + | 'surveyTitle' + | 'themesSettingTitle' + | 'theyChangedTheTimer' + | 'thisMonth' + | 'thisWeek' + | 'timerOption_0_seconds' + | 'timerOption_0_seconds_abbreviated' + | 'timerOption_10_seconds' + | 'timerOption_10_seconds_abbreviated' + | 'timerOption_12_hours' + | 'timerOption_12_hours_abbreviated' + | 'timerOption_1_day' + | 'timerOption_1_day_abbreviated' + | 'timerOption_1_hour' + | 'timerOption_1_hour_abbreviated' + | 'timerOption_1_minute' + | 'timerOption_1_minute_abbreviated' + | 'timerOption_1_week' + | 'timerOption_1_week_abbreviated' + | 'timerOption_2_weeks' + | 'timerOption_2_weeks_abbreviated' + | 'timerOption_30_minutes' + | 'timerOption_30_minutes_abbreviated' + | 'timerOption_30_seconds' + | 'timerOption_30_seconds_abbreviated' + | 'timerOption_5_minutes' + | 'timerOption_5_minutes_abbreviated' + | 'timerOption_5_seconds' + | 'timerOption_5_seconds_abbreviated' + | 'timerOption_6_hours' + | 'timerOption_6_hours_abbreviated' + | 'timerSetOnSync' + | 'timerSetTo' + | 'titleIsNow' + | 'to' + | 'today' + | 'tookAScreenshot' | 'trimDatabase' - | 'trimDatabaseDescription' | 'trimDatabaseConfirmationBody' - | 'pleaseWaitOpenAndOptimizeDb' - | 'messageRequestPending' - | 'messageRequestAccepted' - | 'messageRequestAcceptedOurs' - | 'messageRequestAcceptedOursNoName' - | 'declineRequestMessage' - | 'respondingToRequestWarning' - | 'hideRequestBanner' - | 'openMessageRequestInbox' - | 'noMessageRequestsPending' - | 'noMediaUntilApproved' - | 'mustBeApproved' + | 'trimDatabaseDescription' + | 'trustThisContactDialogDescription' + | 'trustThisContactDialogTitle' + | 'tryAgain' + | 'typeInOldPassword' + | 'typingAlt' + | 'typingIndicatorsSettingDescription' + | 'typingIndicatorsSettingTitle' + | 'unableToCall' + | 'unableToCallTitle' + | 'unableToLoadAttachment' + | 'unbanUser' + | 'unblock' + | 'unblockToSend' + | 'unblocked' + | 'unknown' + | 'unknownCountry' + | 'unpinConversation' + | 'unreadMessages' + | 'updateGroupDialogTitle' + | 'updatedTheGroup' + | 'userAddedToModerators' + | 'userBanFailed' + | 'userBanned' + | 'userRemovedFromModerators' + | 'userUnbanFailed' + | 'userUnbanned' + | 'video' + | 'videoAttachmentAlt' + | 'viewMenuResetZoom' + | 'viewMenuToggleDevTools' + | 'viewMenuToggleFullScreen' + | 'viewMenuZoomIn' + | 'viewMenuZoomOut' + | 'voiceMessage' + | 'welcomeToYourSession' + | 'windowMenuClose' + | 'windowMenuMinimize' + | 'windowMenuZoom' + | 'yesterday' + | 'you' + | 'youChangedTheTimer' + | 'youDisabledDisappearingMessages' + | 'youGotKickedFromGroup' | 'youHaveANewFriendRequest' - | 'clearAllConfirmationTitle' - | 'clearAllConfirmationBody' - | 'noMessagesInReadOnly' - | 'noMessagesInBlindedDisabledMsgRequests' - | 'noMessagesInNoteToSelf' - | 'noMessagesInEverythingElse' - | 'hideBanner' - | 'someOfYourDeviceUseOutdatedVersion' - | 'openMessageRequestInboxDescription' - | 'clearAllReactions' - | 'expandedReactionsText' - | 'reactionNotification' - | 'rateLimitReactMessage' - | 'otherSingular' - | 'otherPlural' - | 'reactionPopup' - | 'reactionPopupOne' - | 'reactionPopupTwo' - | 'reactionPopupThree' - | 'reactionPopupMany' - | 'reactionListCountSingular' - | 'reactionListCountPlural'; + | 'youLeftTheGroup' + | 'yourSessionID' + | 'yourUniqueSessionID' + | 'zoomFactorSettingTitle';