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

View File

@ -1,4 +1,5 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { HTMLDirection } from '../../util/i18n';
export interface FlexProps { export interface FlexProps {
children?: any; children?: any;
@ -6,7 +7,7 @@ export interface FlexProps {
container?: boolean; container?: boolean;
dataTestId?: string; dataTestId?: string;
// Container Props // Container Props
flexDirection?: 'row' | 'column'; flexDirection?: 'row' | 'row-reverse' | 'column' | 'column-reverse';
justifyContent?: justifyContent?:
| 'flex-start' | 'flex-start'
| 'flex-end' | 'flex-end'
@ -36,6 +37,8 @@ export interface FlexProps {
maxWidth?: string; maxWidth?: string;
minWidth?: string; minWidth?: string;
maxHeight?: string; maxHeight?: string;
// RTL support
dir?: HTMLDirection;
} }
export const Flex = styled.div<FlexProps>` export const Flex = styled.div<FlexProps>`
@ -53,4 +56,5 @@ export const Flex = styled.div<FlexProps>`
height: ${props => props.height || 'auto'}; height: ${props => props.height || 'auto'};
max-width: ${props => props.maxWidth || 'none'}; max-width: ${props => props.maxWidth || 'none'};
min-width: ${props => props.minWidth || '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 { NoMessageInConversation } from './SubtleNotification';
import { MessageDetail } from './message/message-item/MessageDetail'; import { MessageDetail } from './message/message-item/MessageDetail';
import { HTMLDirection } from '../../util/i18n';
import { SessionSpinner } from '../basic/SessionSpinner'; import { SessionSpinner } from '../basic/SessionSpinner';
const DEFAULT_JPEG_QUALITY = 0.85; const DEFAULT_JPEG_QUALITY = 0.85;
@ -74,6 +75,7 @@ interface Props {
showMessageDetails: boolean; showMessageDetails: boolean;
isRightPanelShowing: boolean; isRightPanelShowing: boolean;
hasOngoingCallWithFocusedConvo: boolean; hasOngoingCallWithFocusedConvo: boolean;
htmlDirection: HTMLDirection;
// lightbox options // lightbox options
lightBoxOptions?: LightBoxOptions; lightBoxOptions?: LightBoxOptions;
@ -289,6 +291,7 @@ export class SessionConversation extends React.Component<Props, State> {
stagedAttachments={this.props.stagedAttachments} stagedAttachments={this.props.stagedAttachments}
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
onChoseAttachments={this.onChoseAttachments} onChoseAttachments={this.onChoseAttachments}
htmlDirection={this.props.htmlDirection}
/> />
</div> </div>
<div <div

View File

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

View File

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

View File

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

View File

@ -1,28 +1,40 @@
import React from 'react'; import React from 'react';
import { SuggestionDataItem } from 'react-mentions'; import { SuggestionDataItem } from 'react-mentions';
import { MemberListItem } from '../../MemberListItem'; import { MemberListItem } from '../../MemberListItem';
import { HTMLDirection } from '../../../util/i18n';
export const styleForCompositionBoxSuggestions = { const listRTLStyle = { position: 'absolute', bottom: '0px', right: '100%' };
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': { export const styleForCompositionBoxSuggestions = (dir: HTMLDirection = 'ltr') => {
backgroundColor: 'var(--suggestions-background-hover-color)', 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) => { export const renderUserMentionRow = (suggestion: SuggestionDataItem) => {

View File

@ -366,12 +366,6 @@ export const MarkAllReadMenuItem = (): JSX.Element | null => {
return 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 => { export const BlockMenuItem = (): JSX.Element | null => {
const convoId = useConvoIdFromContext(); const convoId = useConvoIdFromContext();
const isMe = useIsMe(convoId); const isMe = useIsMe(convoId);
@ -577,7 +571,7 @@ export const NotificationForConvoMenuItem = (): JSX.Element | null => {
return null; return null;
} }
// const isRtlMode = isRtlBody();' // const isRtlMode = isRtlBody();
// exclude mentions_only settings for private chats as this does not make much sense // exclude mentions_only settings for private chats as this does not make much sense
const notificationForConvoOptions = ConversationNotificationSetting.filter(n => const notificationForConvoOptions = ConversationNotificationSetting.filter(n =>

View File

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

View File

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

View File

@ -67,3 +67,15 @@ export const loadEmojiPanelI18n = async () => {
} }
return undefined; 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');