Merge branch 'unstable' into userconfig_disappearingmessage

pull/2971/head
William Grant 9 months ago
parent 9d8cca1970
commit 85f270b67a

@ -3,8 +3,9 @@ module.exports = {
settings: {
'import/core-modules': ['electron'],
'import/resolver': {
typescript: true,
node: true,
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
react: {
version: 'detect',

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit "$1"

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

@ -0,0 +1,29 @@
const ignoredFiles = [
'package.json',
'yarn.lock',
'tsconfig.json',
'.lintstagedrc.js',
'.eslintrc.js',
];
const path = require('path');
const buildFormatCommand = filenames => {
const results = filenames
.map(f => path.relative(process.cwd(), f))
.filter(f => !ignoredFiles.includes(f));
return results.length ? `prettier --list-different --write ${results.join(' ')}` : '';
};
const buildLintCommand = filenames => {
const results = filenames
.map(f => path.relative(process.cwd(), f))
.filter(f => !ignoredFiles.includes(f));
return results.length ? `eslint --cache ${results.join(' ')}` : '';
};
module.exports = {
'*.{js,ts,tsx}': [buildFormatCommand, buildLintCommand],
};

@ -37,3 +37,7 @@ Gruntfile.js
!istanbul-reports/lib/html/assets
!istanbul-api/node_modules/istanbul-reports/lib/html/assets
# lint-staged fix
# https://github.com/eemeli/yaml/issues/384#issuecomment-1368496106
!**/yaml/dist/**/doc

@ -10,7 +10,7 @@ It's a good idea to gauge interest in your intended work by finding the current
for it or creating a new one yourself. Use Github issues as a place to signal
your intentions and get feedback from the users most likely to appreciate your changes.
You're most likely to have your pull request accepted if it addresses an existing Github issue marked with the [good-first-issue](https://github.com/oxen-io/session-desktop/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) tag, these issues are specifically tagged, because they are generally features/bug fixes which can be cleanly merged on a single platform without requiring cross platform work, are generally of lower complexity than larger features and are non contentious, meaning that the core team doesn't need to try and assess the community desire for such a feature before merging.
You're most likely to have your pull request accepted if it addresses an existing Github issue marked with the [good-first-issue](https://github.com/oxen-io/session-desktop/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) tag, these issues are specifically tagged, because they are generally features/bug fixes which can be cleanly merged on a single platform without requiring cross platform work, are generally of lower complexity than larger features and are non contentious, meaning that the core team doesn't need to try and assess the community desire for such a feature before merging.
Of course we encourage community developers to work on ANY issue filed on our Github regardless of how its tagged, however if you pick up or create an issue without the “Good first issue” tag it would be best if you leave a comment on the issue so that the core team can give you any guidance required, especially around UI heavy features or issues which require cross platform integration.
@ -45,10 +45,10 @@ Building on Windows versions 8+ is supported out of the box
1. Install `make`
1. Depending on your distro, you might need to install `hunspell` and `hunspell-<lan>` (e.g. `hunspell-en-au`)
If you are using a Debian based Linux distribution gcc, g++ and make can be installed as part of the `build-essential` package using
If you are using a Debian based Linux distribution gcc, g++ and make can be installed as part of the `build-essential` package using
```
apt install build-essential
```
apt install build-essential
```
### All platforms
@ -129,6 +129,16 @@ Please write tests! Our testing framework is
The easiest way to run all tests at once is `yarn test`.
## Committing your changes
Before a commit is accepted the staged changes will be formatted using [prettier](https://prettier.io/) and linted using [eslint](https://eslint.org/). The commit will be reverted if files are formatted or lint errors are returned.
### Commit Message Convention
This project follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
Commit messages will be checked using [husky](https://typicode.github.io/husky/#/) and [commitlint](https://commitlint.js.org/).
## Pull requests
So you wanna make a pull request? Please observe the following guidelines.

@ -525,6 +525,7 @@
"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",
"setDisplayPicture": "Set Display Picture",
"settingAppliesToEveryone": "This setting applies to everyone in this conversation.",
"onlyGroupAdminsCanChange": "Only group admins can change this setting."
}

@ -0,0 +1 @@
module.exports = { extends: ['@commitlint/config-conventional'] };

@ -64,7 +64,8 @@
"postinstall": "yarn patch-package && yarn electron-builder install-app-deps",
"update-git-info": "node ./build/updateLocalConfig.js",
"worker:utils": "webpack --config=./utils.worker.config.js",
"worker:libsession": "rimraf 'ts/webworker/workers/node/libsession/*.node' && webpack --config=./libsession.worker.config.js"
"worker:libsession": "rimraf 'ts/webworker/workers/node/libsession/*.node' && webpack --config=./libsession.worker.config.js",
"prepare": "husky install"
},
"dependencies": {
"@emoji-mart/data": "^1.1.2",
@ -132,6 +133,9 @@
"webrtc-adapter": "^4.1.1"
},
"devDependencies": {
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"@commitlint/types": "^17.4.4",
"@electron/notarize": "^2.1.0",
"@types/backbone": "1.4.2",
"@types/blueimp-load-image": "5.14.4",
@ -183,8 +187,10 @@
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-react-hooks": "^4.6.0",
"events": "^3.3.0",
"husky": "^8.0.0",
"jsdom": "^22.1.0",
"jsdom-global": "^3.0.2",
"lint-staged": "^14.0.1",
"mini-css-extract-plugin": "^2.7.5",
"mocha": "10.0.0",
"node-loader": "^2.0.0",

@ -145,13 +145,6 @@
position: absolute;
bottom: 0px;
}
.session-icon-button {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0px;
}
}
&-terms-conditions-agreement {

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

@ -5,12 +5,14 @@ import { getFocusedSettingsSection } from '../state/selectors/section';
import { SmartSessionConversation } from '../state/smart/SessionConversation';
import { SessionSettingsView } from './settings/SessionSettings';
import { useHTMLDirection } from '../util/i18n';
const FilteredSettingsView = SessionSettingsView as any;
export const SessionMainPanel = () => {
const focusedSettingsSection = useSelector(getFocusedSettingsSection);
const isSettingsView = focusedSettingsSection !== undefined;
const htmlDirection = useHTMLDirection();
// even if it looks like this does nothing, this does update the redux store.
useAppIsFocused();
@ -20,7 +22,7 @@ export const SessionMainPanel = () => {
}
return (
<div className="session-conversation">
<SmartSessionConversation />
<SmartSessionConversation htmlDirection={htmlDirection} />
</div>
);
};

@ -1,4 +1,5 @@
import styled from 'styled-components';
import { HTMLDirection } from '../../util/i18n';
export interface FlexProps {
children?: any;
@ -6,7 +7,7 @@ export interface FlexProps {
container?: boolean;
dataTestId?: string;
// Container Props
flexDirection?: 'row' | 'column';
flexDirection?: 'row' | 'row-reverse' | 'column' | 'column-reverse';
justifyContent?:
| 'flex-start'
| 'flex-end'
@ -36,6 +37,8 @@ export interface FlexProps {
maxWidth?: string;
minWidth?: string;
maxHeight?: string;
// RTL support
dir?: HTMLDirection;
}
export const Flex = styled.div<FlexProps>`
@ -53,4 +56,5 @@ export const Flex = styled.div<FlexProps>`
height: ${props => props.height || 'auto'};
max-width: ${props => props.maxWidth || 'none'};
min-width: ${props => props.minWidth || 'none'};
direction: ${props => props.dir || undefined};
`;

@ -3,6 +3,7 @@ import React, { useState } from 'react';
import classNames from 'classnames';
import { SessionIconButton } from '../icon';
import { Noop } from '../../types/Util';
import { useHTMLDirection } from '../../util/i18n';
type Props = {
label?: string;
@ -46,7 +47,17 @@ const ErrorItem = (props: { error: string | undefined }) => {
};
const ShowHideButton = (props: { toggleForceShow: Noop }) => {
return <SessionIconButton iconType="eye" iconSize="medium" onClick={props.toggleForceShow} />;
const htmlDirection = useHTMLDirection();
const position = htmlDirection === 'ltr' ? { right: '0px' } : { left: '0px' };
return (
<SessionIconButton
iconType="eye"
iconSize="medium"
onClick={props.toggleForceShow}
style={{ position: 'absolute', top: '50%', transform: 'translateY(-50%)', ...position }}
/>
);
};
export const SessionInput = (props: Props) => {

@ -52,6 +52,7 @@ import { NoMessageInConversation } from './SubtleNotification';
import { ConversationHeaderWithDetails } from './header/ConversationHeader';
import { MessageDetail } from './message/message-item/MessageDetail';
import { HTMLDirection } from '../../util/i18n';
import { NoticeBanner } from '../NoticeBanner';
import { SessionSpinner } from '../basic/SessionSpinner';
import { RightPanel } from './right-panel/RightPanel';
@ -76,6 +77,7 @@ interface Props {
showMessageDetails: boolean;
isRightPanelShowing: boolean;
hasOngoingCallWithFocusedConvo: boolean;
htmlDirection: HTMLDirection;
// lightbox options
lightBoxOptions?: LightBoxOptions;
@ -304,6 +306,7 @@ export class SessionConversation extends React.Component<Props, State> {
stagedAttachments={this.props.stagedAttachments}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onChoseAttachments={this.onChoseAttachments}
htmlDirection={this.props.htmlDirection}
/>
</div>
<div

@ -69,7 +69,7 @@ export const StyledEmojiPanel = styled.div<{
content: '';
position: absolute;
top: calc(100% - 40px);
left: calc(100% - 79px);
left: calc(100% - 106px);
width: 22px;
height: 22px;
transform: rotate(45deg);
@ -78,6 +78,10 @@ export const StyledEmojiPanel = styled.div<{
border: 0.7px solid var(--border-color);
clip-path: polygon(100% 100%, 7.2px 100%, 100% 7.2px);
${props.panelBackgroundRGB && `background-color: rgb(${props.panelBackgroundRGB})`};
[dir='rtl'] & {
left: 75px;
}
}
`};
}

@ -36,16 +36,17 @@ import {
StagedAttachmentImportedType,
StagedPreviewImportedType,
} from '../../../util/attachmentsUtil';
import { HTMLDirection } from '../../../util/i18n';
import { LinkPreviews } from '../../../util/linkPreviews';
import { Flex } from '../../basic/Flex';
import { CaptionEditor } from '../../CaptionEditor';
import { Flex } from '../../basic/Flex';
import { getMediaPermissionsSettings } from '../../settings/SessionSettings';
import { getDraftForConversation, updateDraftForConversation } from '../SessionConversationDrafts';
import { SessionQuotedMessageComposition } from '../SessionQuotedMessageComposition';
import {
getPreview,
LINK_PREVIEW_TIMEOUT,
SessionStagedLinkPreview,
getPreview,
} from '../SessionStagedLinkPreview';
import { StagedAttachmentList } from '../StagedAttachmentList';
import {
@ -108,6 +109,7 @@ interface Props {
quotedMessageProps?: ReplyingToMessageProps;
stagedAttachments: Array<StagedAttachmentType>;
onChoseAttachments: (newAttachments: Array<File>) => void;
htmlDirection: HTMLDirection;
}
interface State {
@ -119,26 +121,28 @@ interface State {
showCaptionEditor?: AttachmentType;
}
const sendMessageStyle = {
control: {
wordBreak: 'break-all',
},
input: {
overflow: 'auto',
maxHeight: '50vh',
wordBreak: 'break-word',
padding: '0px',
margin: '0px',
},
highlighter: {
boxSizing: 'border-box',
overflow: 'hidden',
maxHeight: '50vh',
},
flexGrow: 1,
minHeight: '24px',
width: '100%',
...styleForCompositionBoxSuggestions,
const sendMessageStyle = (dir?: HTMLDirection) => {
return {
control: {
wordBreak: 'break-all',
},
input: {
overflow: 'auto',
maxHeight: '50vh',
wordBreak: 'break-word',
padding: '0px',
margin: '0px',
},
highlighter: {
boxSizing: 'border-box',
overflow: 'hidden',
maxHeight: '50vh',
},
flexGrow: 1,
minHeight: '24px',
width: '100%',
...styleForCompositionBoxSuggestions(dir),
};
};
const getDefaultState = (newConvoId?: string) => {
@ -209,21 +213,23 @@ const getSelectionBasedOnMentions = (draft: string, index: number) => {
return Number.MAX_SAFE_INTEGER;
};
const StyledEmojiPanelContainer = styled.div`
const StyledEmojiPanelContainer = styled.div<{ dir?: HTMLDirection }>`
${StyledEmojiPanel} {
position: absolute;
bottom: 68px;
right: 0px;
${props => (props.dir === 'rtl' ? 'left: 0px' : 'right: 0px;')}
}
`;
const StyledSendMessageInput = styled.div`
const StyledSendMessageInput = styled.div<{ dir?: HTMLDirection }>`
position: relative;
cursor: text;
display: flex;
align-items: center;
flex-grow: 1;
min-height: var(--composition-container-height);
padding: var(--margins-xs) 0;
${props => props.dir === 'rtl' && 'margin-inline-start: var(--margins-sm);'}
z-index: 1;
background-color: inherit;
@ -235,7 +241,7 @@ const StyledSendMessageInput = styled.div`
textarea {
font-family: var(--font-default);
min-height: calc(var(--composition-container-height) / 3);
max-height: 3 * var(--composition-container-height);
max-height: calc(3 * var(--composition-container-height));
margin-right: var(--margins-md);
color: var(--text-color-primary);
@ -417,7 +423,13 @@ class CompositionBoxInner extends React.Component<Props, State> {
/* eslint-disable @typescript-eslint/no-misused-promises */
return (
<>
<Flex
dir={this.props.htmlDirection}
container={true}
flexDirection={'row'}
alignItems={'center'}
width={'100%'}
>
{typingEnabled && <AddStagedAttachmentButton onClick={this.onChooseAttachment} />}
<input
className="hidden"
@ -430,6 +442,7 @@ class CompositionBoxInner extends React.Component<Props, State> {
{typingEnabled && <StartRecordingButton onClick={this.onLoadVoiceNoteView} />}
<StyledSendMessageInput
role="main"
dir={this.props.htmlDirection}
onClick={this.focusCompositionBox} // used to focus on the textarea when clicking in its container
ref={el => {
this.container = el;
@ -443,7 +456,7 @@ class CompositionBoxInner extends React.Component<Props, State> {
)}
{typingEnabled && <SendMessageButton onClick={this.onSendMessage} />}
{typingEnabled && showEmojiPanel && (
<StyledEmojiPanelContainer role="button">
<StyledEmojiPanelContainer role="button" dir={this.props.htmlDirection}>
<SessionEmojiPanel
ref={this.emojiPanel}
show={showEmojiPanel}
@ -452,7 +465,7 @@ class CompositionBoxInner extends React.Component<Props, State> {
/>
</StyledEmojiPanelContainer>
)}
</>
</Flex>
);
}
/* eslint-enable @typescript-eslint/no-misused-promises */
@ -460,6 +473,7 @@ class CompositionBoxInner extends React.Component<Props, State> {
private renderTextArea() {
const { i18n } = window;
const { draft } = this.state;
const { htmlDirection } = this.props;
if (!this.props.selectedConversation) {
return null;
@ -483,6 +497,8 @@ class CompositionBoxInner extends React.Component<Props, State> {
const { typingEnabled } = this.props;
const neverMatchingRegex = /($a)/;
const style = sendMessageStyle(htmlDirection);
return (
<MentionsInput
value={draft}
@ -493,11 +509,12 @@ class CompositionBoxInner extends React.Component<Props, State> {
onKeyUp={this.onKeyUp}
placeholder={messagePlaceHolder}
spellCheck={true}
dir={htmlDirection}
inputRef={this.textarea}
disabled={!typingEnabled}
rows={1}
data-testid="message-input-text-area"
style={sendMessageStyle}
style={style}
suggestionsPortalHost={this.container as any}
forceSuggestionsAboveCursor={true} // force mentions to be rendered on top of the cursor, this is working with a fork of react-mentions for now
>
@ -507,7 +524,9 @@ class CompositionBoxInner extends React.Component<Props, State> {
markup="@ᅭ__id__ᅲ__display__ᅭ" // ᅭ = \uFFD2 is one of the forbidden char for a display name (check displayNameRegex)
trigger="@"
// this is only for the composition box visible content. The real stuff on the backend box is the @markup
displayTransform={(_id, display) => `@${display}`}
displayTransform={(_id, display) =>
htmlDirection === 'rtl' ? `${display}@` : `@${display}`
}
data={this.fetchUsersForGroup}
renderSuggestion={renderUserMentionRow}
/>

@ -8,6 +8,7 @@ import { searchSync } from '../../../util/emoji.js';
const EmojiQuickResult = styled.span`
display: flex;
align-items: center;
min-width: 250px;
width: 100%;
padding-inline-end: 20px;
padding-inline-start: 10px;

@ -1,28 +1,40 @@
import React from 'react';
import { SuggestionDataItem } from 'react-mentions';
import { MemberListItem } from '../../MemberListItem';
import { HTMLDirection } from '../../../util/i18n';
export const styleForCompositionBoxSuggestions = {
suggestions: {
list: {
fontSize: 14,
boxShadow: 'var(--suggestions-shadow)',
backgroundColor: 'var(--suggestions-background-color)',
color: 'var(--suggestions-text-color)',
},
item: {
height: '100%',
paddingTop: '5px',
paddingBottom: '5px',
backgroundColor: 'var(--suggestions-background-color)',
color: 'var(--suggestions-text-color)',
transition: '0.25s',
'&focused': {
backgroundColor: 'var(--suggestions-background-hover-color)',
const listRTLStyle = { position: 'absolute', bottom: '0px', right: '100%' };
export const styleForCompositionBoxSuggestions = (dir: HTMLDirection = 'ltr') => {
const styles = {
suggestions: {
list: {
fontSize: 14,
boxShadow: 'var(--suggestions-shadow)',
backgroundColor: 'var(--suggestions-background-color)',
color: 'var(--suggestions-text-color)',
dir,
},
item: {
height: '100%',
paddingTop: '5px',
paddingBottom: '5px',
backgroundColor: 'var(--suggestions-background-color)',
color: 'var(--suggestions-text-color)',
transition: '0.25s',
'&focused': {
backgroundColor: 'var(--suggestions-background-hover-color)',
},
},
},
},
};
if (dir === 'rtl') {
styles.suggestions.list = { ...styles.suggestions.list, ...listRTLStyle };
}
return styles;
};
export const renderUserMentionRow = (suggestion: SuggestionDataItem) => {

@ -86,7 +86,7 @@ const StyledMessageOpaqueContent = styled(StyledMessageHighlighter)<{
align-self: ${props => (props.messageDirection === 'incoming' ? 'flex-start' : 'flex-end')};
padding: var(--padding-message-content);
border-radius: var(--border-radius-message-box);
max-width: 100%;
width: 100%;
`;
export const IsMessageVisibleContext = createContext(false);

@ -24,18 +24,6 @@ import {
showMessageDetailsView,
toggleSelectedMessageId,
} from '../../../../state/ducks/conversations';
import {
useSelectedConversationKey,
useSelectedIsBlocked,
useSelectedIsPublic,
useSelectedWeAreAdmin,
useSelectedWeAreModerator,
} from '../../../../state/selectors/selectedConversation';
import { saveAttachmentToDisk } from '../../../../util/attachmentsUtil';
import { Reactions } from '../../../../util/reactions';
import { SessionContextMenuContainer } from '../../../SessionContextMenuContainer';
import { SessionEmojiPanel, StyledEmojiPanel } from '../../SessionEmojiPanel';
import { MessageReactBar } from './MessageReactBar';
import {
useMessageAttachments,
useMessageBody,
@ -48,7 +36,18 @@ import {
useMessageStatus,
useMessageTimestamp,
} from '../../../../state/selectors';
import { useIsPublic } from '../../../../hooks/useParamSelector';
import {
useSelectedConversationKey,
useSelectedIsBlocked,
useSelectedIsPublic,
useSelectedWeAreAdmin,
useSelectedWeAreModerator,
} from '../../../../state/selectors/selectedConversation';
import { saveAttachmentToDisk } from '../../../../util/attachmentsUtil';
import { Reactions } from '../../../../util/reactions';
import { SessionContextMenuContainer } from '../../../SessionContextMenuContainer';
import { SessionEmojiPanel, StyledEmojiPanel } from '../../SessionEmojiPanel';
import { MessageReactBar } from './MessageReactBar';
export type MessageContextMenuSelectorProps = Pick<
MessageRenderingProps,
@ -149,10 +148,11 @@ const SaveAttachment = ({ messageId }: MessageId) => {
const AdminActionItems = ({ messageId }: MessageId) => {
const convoId = useSelectedConversationKey();
const isPublic = useIsPublic();
const isPublic = useSelectedIsPublic();
const weAreModerator = useSelectedWeAreModerator();
const weAreAdmin = useSelectedWeAreAdmin();
const showAdminActions = (weAreAdmin || weAreModerator) && isPublic;
const sender = useMessageSender(messageId);
const isSenderAdmin = useMessageSenderIsAdmin(messageId);

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

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

@ -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} />
)}
</>
);
};

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

@ -86,7 +86,7 @@ export const OverlayMessage = () => {
const pubkeyorOnsTrimmed = pubkeyOrOns.trim();
if (!PubKey.validateWithErrorNoBlinding(pubkeyorOnsTrimmed)) {
await openConvoOnceResolved(pubkeyOrOns);
await openConvoOnceResolved(pubkeyorOnsTrimmed);
return;
}

@ -366,12 +366,6 @@ export const MarkAllReadMenuItem = (): JSX.Element | null => {
return null;
};
export function isRtlBody(): boolean {
const body = document.getElementsByTagName('body').item(0);
return body?.classList.contains('rtl') || false;
}
export const BlockMenuItem = (): JSX.Element | null => {
const convoId = useConvoIdFromContext();
const isMe = useIsMe(convoId);
@ -577,7 +571,7 @@ export const NotificationForConvoMenuItem = (): JSX.Element | null => {
return null;
}
// const isRtlMode = isRtlBody();'
// const isRtlMode = isRtlBody();
// exclude mentions_only settings for private chats as this does not make much sense
const notificationForConvoOptions = ConversationNotificationSetting.filter(n =>

@ -1,3 +1,4 @@
import { isNil } from 'lodash';
import {
ConversationNotificationSettingType,
ConversationTypeEnum,
@ -502,6 +503,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) {

@ -750,8 +750,9 @@ app.on('ready', async () => {
assertLogger().info('app ready');
assertLogger().info(`starting version ${packageJson.version}`);
if (!locale) {
const appLocale = app.getLocale() || 'en';
const appLocale = process.env.LANGUAGE || app.getLocale() || 'en';
locale = loadLocale({ appLocale, logger });
assertLogger().info(`locale is ${appLocale}`);
}
const key = getDefaultSQLKey();

@ -267,13 +267,17 @@ async function start() {
await connect();
});
function openInbox() {
function switchBodyToRtlIfNeeded() {
const rtlLocales = ['fa', 'ar', 'he'];
const loc = (window.i18n as any).getLocale();
if (rtlLocales.includes(loc) && !document.getElementById('body')?.classList.contains('rtl')) {
document.getElementById('body')?.classList.add('rtl');
}
}
function openInbox() {
switchBodyToRtlIfNeeded();
const hideMenuBar = Storage.get('hide-menu-bar', true) as boolean;
window.setAutoHideMenuBar(hideMenuBar);
window.setMenuBarVisibility(!hideMenuBar);
@ -287,6 +291,7 @@ async function start() {
function showRegistrationView() {
ReactDOM.render(<SessionRegistrationView />, document.getElementById('root'));
switchBodyToRtlIfNeeded();
}
ExpirationTimerOptions.initExpiringMessageListener();

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

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

@ -16,8 +16,13 @@ import { getSelectedConversationKey } from '../selectors/selectedConversation';
import { getStagedAttachmentsForCurrentConversation } from '../selectors/stagedAttachments';
import { getTheme } from '../selectors/theme';
import { getOurNumber } from '../selectors/user';
import { HTMLDirection } from '../../util/i18n';
const mapStateToProps = (state: StateType) => {
type SmartSessionConversationOwnProps = {
htmlDirection: HTMLDirection;
};
const mapStateToProps = (state: StateType, ownProps: SmartSessionConversationOwnProps) => {
return {
selectedConversation: getSelectedConversation(state),
selectedConversationKey: getSelectedConversationKey(state),
@ -31,6 +36,7 @@ const mapStateToProps = (state: StateType) => {
stagedAttachments: getStagedAttachmentsForCurrentConversation(state),
hasOngoingCallWithFocusedConvo: getHasOngoingCallWithFocusedConvo(state),
isSelectedConvoInitialLoadingInProgress: getIsSelectedConvoInitialLoadingInProgress(state),
htmlDirection: ownProps.htmlDirection,
};
};

File diff suppressed because it is too large Load Diff

@ -67,3 +67,15 @@ export const loadEmojiPanelI18n = async () => {
}
return undefined;
};
// RTL Support
export type HTMLDirection = 'ltr' | 'rtl';
export function isRtlBody(): boolean {
const body = document.getElementsByTagName('body').item(0);
return body?.classList.contains('rtl') || false;
}
export const useHTMLDirection = (): HTMLDirection => (isRtlBody() ? 'rtl' : 'ltr');

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save