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 $emoji$ to this message",
- "reactionListCountPlural": "And $otherPlural$ have reacted $emoji$ to this message"
+ "reactionListCountPlural": "And $otherPlural$ have reacted $emoji$ to this message",
+ "setDisplayPicture": "Set Display Picture"
}
diff --git a/stylesheets/_session_signin.scss b/stylesheets/_session_signin.scss
index 46fe3ff0a..8a7872f54 100644
--- a/stylesheets/_session_signin.scss
+++ b/stylesheets/_session_signin.scss
@@ -145,13 +145,6 @@
position: absolute;
bottom: 0px;
}
-
- .session-icon-button {
- position: absolute;
- top: 50%;
- transform: translateY(-50%);
- right: 0px;
- }
}
&-terms-conditions-agreement {
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/SessionMainPanel.tsx b/ts/components/SessionMainPanel.tsx
index 035275712..d5d383331 100644
--- a/ts/components/SessionMainPanel.tsx
+++ b/ts/components/SessionMainPanel.tsx
@@ -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 (
-
+
);
};
diff --git a/ts/components/basic/Flex.tsx b/ts/components/basic/Flex.tsx
index f1c50ca47..650e66982 100644
--- a/ts/components/basic/Flex.tsx
+++ b/ts/components/basic/Flex.tsx
@@ -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`
@@ -53,4 +56,5 @@ export const Flex = styled.div`
height: ${props => props.height || 'auto'};
max-width: ${props => props.maxWidth || 'none'};
min-width: ${props => props.minWidth || 'none'};
+ direction: ${props => props.dir || undefined};
`;
diff --git a/ts/components/basic/SessionInput.tsx b/ts/components/basic/SessionInput.tsx
index 2143b7413..0c33a22b0 100644
--- a/ts/components/basic/SessionInput.tsx
+++ b/ts/components/basic/SessionInput.tsx
@@ -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 ;
+ const htmlDirection = useHTMLDirection();
+ const position = htmlDirection === 'ltr' ? { right: '0px' } : { left: '0px' };
+
+ return (
+
+ );
};
export const SessionInput = (props: Props) => {
diff --git a/ts/components/conversation/SessionConversation.tsx b/ts/components/conversation/SessionConversation.tsx
index 3f3d0446b..d90a97193 100644
--- a/ts/components/conversation/SessionConversation.tsx
+++ b/ts/components/conversation/SessionConversation.tsx
@@ -53,6 +53,7 @@ import { SessionRightPanelWithDetails } from './SessionRightPanel';
import { NoMessageInConversation } from './SubtleNotification';
import { MessageDetail } from './message/message-item/MessageDetail';
+import { HTMLDirection } from '../../util/i18n';
import { SessionSpinner } from '../basic/SessionSpinner';
const DEFAULT_JPEG_QUALITY = 0.85;
@@ -74,6 +75,7 @@ interface Props {
showMessageDetails: boolean;
isRightPanelShowing: boolean;
hasOngoingCallWithFocusedConvo: boolean;
+ htmlDirection: HTMLDirection;
// lightbox options
lightBoxOptions?: LightBoxOptions;
@@ -289,6 +291,7 @@ export class SessionConversation extends React.Component {
stagedAttachments={this.props.stagedAttachments}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onChoseAttachments={this.onChoseAttachments}
+ htmlDirection={this.props.htmlDirection}
/>
;
onChoseAttachments: (newAttachments: Array
) => 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 {
/* eslint-disable @typescript-eslint/no-misused-promises */
return (
- <>
+
{typingEnabled && }
{
{typingEnabled && }
{
this.container = el;
@@ -443,7 +456,7 @@ class CompositionBoxInner extends React.Component {
)}
{typingEnabled && }
{typingEnabled && showEmojiPanel && (
-
+
{
/>
)}
- >
+
);
}
/* eslint-enable @typescript-eslint/no-misused-promises */
@@ -460,6 +473,7 @@ class CompositionBoxInner extends React.Component {
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 {
const { typingEnabled } = this.props;
const neverMatchingRegex = /($a)/;
+ const style = sendMessageStyle(htmlDirection);
+
return (
{
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 {
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}
/>
diff --git a/ts/components/conversation/composition/EmojiQuickResult.tsx b/ts/components/conversation/composition/EmojiQuickResult.tsx
index 2e8c400f1..2d0526026 100644
--- a/ts/components/conversation/composition/EmojiQuickResult.tsx
+++ b/ts/components/conversation/composition/EmojiQuickResult.tsx
@@ -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;
diff --git a/ts/components/conversation/composition/UserMentions.tsx b/ts/components/conversation/composition/UserMentions.tsx
index 09f18ab0a..e5c7aeef5 100644
--- a/ts/components/conversation/composition/UserMentions.tsx
+++ b/ts/components/conversation/composition/UserMentions.tsx
@@ -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) => {
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