Merge remote-tracking branch 'yougotwill/feature/ses-379/composition-rtl-support' into feature/ses-379/composition-rtl-support
This commit is contained in:
commit
bee00157ef
|
@ -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};
|
||||
`;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
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) => {
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Reference in New Issue