Merge remote-tracking branch 'yougotwill/feature/ses-379/composition-rtl-support' into feature/ses-379/composition-rtl-support

This commit is contained in:
Audric Ackermann 2023-08-18 15:27:11 +10:00
commit bee00157ef
11 changed files with 119 additions and 61 deletions

View File

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

View File

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

View File

@ -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<Props, State> {
stagedAttachments={this.props.stagedAttachments}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onChoseAttachments={this.onChoseAttachments}
htmlDirection={this.props.htmlDirection}
/>
</div>
<div

View File

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

View File

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

View File

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

View File

@ -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',
const listRTLStyle = { position: 'absolute', bottom: '0px', right: '100%' };
'&focused': {
backgroundColor: 'var(--suggestions-background-hover-color)',
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) => {

View File

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

View File

@ -751,8 +751,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();

View File

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

View File

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