Skip to content

Commit f43f795

Browse files
authored
[CLNP-4596] feat: add forceLeftToRightMessageLayout to enable LTR message layout display in RTL mode (#1184)
Addresses https://sendbird.atlassian.net/browse/CLNP-4596 This PR addresses the layout issues and CSS [specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity) improvements for right-to-left (RTL) text direction in the Sendbird UIKit React components. The background for adding the `forceLeftToRightMessageLayout` flag is that some of our users in the middle-east want to set `htmlTextDirection='rtl'` while keeping the message layout in left-to-right (LTR) format (outgoing messages on the right, incoming messages on the left). ### Todo - [x] The postcssRtl plugin options were modified. - [x] Layout adjustments were made to ensure proper alignment and behavior.
1 parent 07915a5 commit f43f795

File tree

18 files changed

+174
-9
lines changed

18 files changed

+174
-9
lines changed

.storybook/main.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { mergeConfig } from 'vite';
22
import svgr from 'vite-plugin-svgr';
33
import { dirname, join } from 'path';
4+
import postcssRTLOptions from "../postcssRtlOptions.mjs";
45

56
/**
67
* This function is used to resolve the absolute path of a package.
@@ -22,9 +23,7 @@ export default {
2223
css: {
2324
postcss: {
2425
plugins: [
25-
require('postcss-rtlcss')({
26-
mode: 'override',
27-
}),
26+
require('postcss-rtlcss')(postcssRTLOptions),
2827
],
2928
},
3029
},

apps/testing/src/utils/paramsBuilder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const useConfigParams = (initParams: InitialParams): ParamsAsProps => {
3030
isMultipleFilesMessageEnabled: parseValue(searchParams.get('enableMultipleFilesMessage')) ?? true,
3131
enableLegacyChannelModules: parseValue(searchParams.get('enableLegacyChannelModules')) ?? false,
3232
htmlTextDirection: parseValue(searchParams.get('htmlTextDirection')) ?? 'ltr',
33+
forceLeftToRightMessageLayout: parseValue(searchParams.get('forceLeftToRightMessageLayout')) ?? false,
3334
uikitOptions: {},
3435
} as ParamsAsProps;
3536

apps/testing/vite.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import { defineConfig } from 'vite';
22
import react from '@vitejs/plugin-react';
33
import vitePluginSvgr from 'vite-plugin-svgr';
44
import postcssRtl from "postcss-rtlcss";
5+
// @ts-ignore
6+
import postcssRtlOptions from '../../postcssRtlOptions.mjs';
57

68
// https://vitejs.dev/config/
79
export default defineConfig({
810
plugins: [react(), vitePluginSvgr({ include: '**/*.svg' })],
911
css: {
1012
postcss: {
11-
plugins: [postcssRtl({ mode: 'override' })],
13+
plugins: [postcssRtl(postcssRtlOptions)],
1214
},
1315
},
1416
});

postcssRtlOptions.mjs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
const CLASS_NAMES_TO_CHECK = [
2+
// Normal message list
3+
'sendbird-message-content',
4+
// Thread message list
5+
'sendbird-thread-list-item-content',
6+
'sendbird-parent-message-info'
7+
];
8+
9+
export default {
10+
prefixSelectorTransformer: (prefix, selector) => {
11+
// To increase specificity https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity
12+
// for certain classnames within .sendbird-conversation__messages context.
13+
// This only applies when the root dir is set as 'rtl' but forceLeftToRightMessageLayout is true.
14+
if (CLASS_NAMES_TO_CHECK.some(cls => selector.includes(cls))) {
15+
return `.sendbird-conversation__messages${prefix} ${selector}`;
16+
}
17+
return `${prefix} ${selector}`;
18+
},
19+
}
20+
21+
// Why we're doing this:
22+
// Let's say a root div element has `dir="rtl"` (with `htmlTextDirection={'rtl'}` prop setting).
23+
24+
/*
25+
<div class="sendbird-root" dir="rtl">
26+
...
27+
// Message list
28+
<div class="sendbird-conversation__messages" dir="ltr">
29+
<div class="emoji-container">
30+
<span class="emoji-inner">😀</span>
31+
</div>
32+
</div>
33+
</div>
34+
*/
35+
36+
// Even though the message list element has dir="ltr" attribute,
37+
// some children elements of the message list would have their styles overridden by the root one with the CSS settings below.
38+
// Why? postcss-rtlcss plugin generates the CSS settings in order of rtl -> ltr.
39+
40+
/*
41+
[dir="rtl"] .sendbird-message-content .emoji-container .emoji-inner {
42+
right: -84px; // Specificity (0.6.0)
43+
}
44+
[dir="ltr"] .sendbird-message-content .emoji-container .emoji-inner {
45+
left: -84px; // Specificity (0.6.0)
46+
}
47+
*/
48+
49+
// If both CSS settings have the same specificity, the one generated first (rtl) will be applied,
50+
// which is not the desired result since we want the `.emoji-inner` element to have the ltr setting.
51+
52+
// To increase the specificity of the ltr setting,
53+
// we can directly connect the classname of the element which has `dir='ltr'` to the children's selector in this way:
54+
55+
/*
56+
.sendbird-conversation__messages[dir="ltr"] .sendbird-message-content .emoji-container .emoji-inner {
57+
left: -84px; // Specificity (0.7.0), will be applied
58+
}
59+
60+
[dir="rtl"] .sendbird-message-content .emoji-container .emoji-inner {
61+
right: -84px; // Specificity (0.6.0), will be ignored
62+
}
63+
[dir="ltr"] .sendbird-message-content .emoji-container .emoji-inner {
64+
left: -84px; // Specificity (0.6.0), will be ignored
65+
}
66+
*/

rollup.config.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import ts2 from "rollup-plugin-typescript2"
1616
import pkg from "./package.json" assert {type: "json"};
1717
import inputs from "./rollup.module-exports.mjs";
1818
import { readFileSync, writeFileSync } from 'fs';
19+
import postcssRTLOptions from "./postcssRtlOptions.mjs";
1920

2021
const APP_VERSION_STRING = "__react_dev_mode__";
2122

@@ -59,7 +60,7 @@ export default {
5960
const result = scss.renderSync({ file: id });
6061
resolvecss({ code: result.css.toString() });
6162
}),
62-
plugins: [autoprefixer, postcssRtl({ mode: 'override' })],
63+
plugins: [autoprefixer, postcssRtl(postcssRTLOptions)],
6364
sourceMap: false,
6465
extract: "dist/index.css",
6566
extensions: [".sass", ".scss", ".css"],

src/hooks/useHTMLTextDirection.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,18 @@ const useHTMLTextDirection = (direction: HTMLTextDirection) => {
1515
}, [direction]);
1616
};
1717

18+
export const useMessageLayoutDirection = (direction: HTMLTextDirection, forceLeftToRightMessageLayout: boolean, loading: boolean) => {
19+
useEffect(() => {
20+
if (loading) return;
21+
const messageListElements = document.getElementsByClassName('sendbird-conversation__messages');
22+
if (messageListElements.length > 0) {
23+
Array.from(messageListElements).forEach((elem: HTMLElement) => {
24+
elem.dir = forceLeftToRightMessageLayout
25+
? 'ltr'
26+
: direction;
27+
});
28+
}
29+
}, [direction, forceLeftToRightMessageLayout, loading]);
30+
};
31+
1832
export default useHTMLTextDirection;

src/lib/Sendbird.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export interface SendbirdProviderProps extends CommonUIKitConfigProps, React.Pro
103103
disableMarkAsDelivered?: boolean;
104104
breakpoint?: string | boolean;
105105
htmlTextDirection?: HTMLTextDirection;
106+
forceLeftToRightMessageLayout?: boolean;
106107
renderUserProfile?: (props: RenderUserProfileProps) => React.ReactElement;
107108
onUserProfileMessage?: (channel: GroupChannel) => void;
108109
uikitOptions?: UIKitOptions;
@@ -178,6 +179,7 @@ const SendbirdSDK = ({
178179
isMultipleFilesMessageEnabled = false,
179180
eventHandlers,
180181
htmlTextDirection = 'ltr',
182+
forceLeftToRightMessageLayout = false,
181183
}: SendbirdProviderProps): React.ReactElement => {
182184
const { logLevel = '', userMention = {}, isREMUnitEnabled = false, pubSub: customPubSub } = config;
183185
const { isMobile } = useMediaQueryContext();
@@ -400,6 +402,7 @@ const SendbirdSDK = ({
400402
isMessageReceiptStatusEnabledOnChannelList: configs.groupChannel.channelList.enableMessageReceiptStatus,
401403
showSearchIcon: sdkInitialized && configsWithAppAttr(sdk).groupChannel.setting.enableMessageSearch,
402404
htmlTextDirection,
405+
forceLeftToRightMessageLayout,
403406
},
404407
eventHandlers,
405408
emojiManager,

src/lib/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
@import '../styles/light-theme';
44
@import '../styles/dark-theme';
55
@import '../styles/misc-colors';
6+
@import '../styles/postcss-rtl';

src/lib/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export interface SendBirdStateConfig {
7373
accessToken?: string;
7474
theme: string;
7575
htmlTextDirection: HTMLTextDirection;
76+
forceLeftToRightMessageLayout: boolean;
7677
pubSub: SBUGlobalPubSub;
7778
logger: Logger;
7879
setCurrentTheme: (theme: 'light' | 'dark') => void;

src/modules/App/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export interface AppProps {
4545
disableAutoSelect?: AppLayoutProps['disableAutoSelect'];
4646
onProfileEditSuccess?: AppLayoutProps['onProfileEditSuccess'];
4747
htmlTextDirection?: AppLayoutProps['htmlTextDirection'];
48+
forceLeftToRightMessageLayout?: AppLayoutProps['forceLeftToRightMessageLayout'];
4849

4950
/**
5051
* The default value is false.
@@ -102,6 +103,7 @@ export default function App(props: AppProps) {
102103
enableLegacyChannelModules = false,
103104
uikitOptions,
104105
htmlTextDirection = 'ltr',
106+
forceLeftToRightMessageLayout = false,
105107
// The below configs are duplicates of the Dashboard UIKit Configs.
106108
// Since their default values will be set in the Sendbird component,
107109
// we don't need to set them here.
@@ -158,6 +160,7 @@ export default function App(props: AppProps) {
158160
isMentionEnabled={isMentionEnabled}
159161
isVoiceMessageEnabled={isVoiceMessageEnabled}
160162
htmlTextDirection={htmlTextDirection}
163+
forceLeftToRightMessageLayout={forceLeftToRightMessageLayout}
161164
>
162165
<AppLayout
163166
isMessageGroupingEnabled={isMessageGroupingEnabled}

src/modules/App/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface AppLayoutProps {
1515
isReactionEnabled?: boolean;
1616
replyType?: 'NONE' | 'QUOTE_REPLY' | 'THREAD';
1717
htmlTextDirection?: HTMLTextDirection;
18+
forceLeftToRightMessageLayout?: boolean;
1819
isMessageGroupingEnabled?: boolean;
1920
isMultipleFilesMessageEnabled?: boolean;
2021
allowProfileEdit?: boolean;

src/modules/Channel/context/ChannelProvider.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { useSendMultipleFilesMessage } from './hooks/useSendMultipleFilesMessage
5151
import { useHandleChannelPubsubEvents } from './hooks/useHandleChannelPubsubEvents';
5252
import { PublishingModuleType } from '../../internalInterfaces';
5353
import { ChannelActionTypes } from './dux/actionTypes';
54+
import { useMessageLayoutDirection } from '../../../hooks/useHTMLTextDirection';
5455

5556
export interface MessageListParams extends Partial<SDKMessageListParams> { // make `prevResultSize` and `nextResultSize` to optional
5657
/** @deprecated It won't work even if you activate this props */
@@ -211,6 +212,8 @@ const ChannelProvider = (props: ChannelContextProps) => {
211212
onUserProfileMessage,
212213
markAsReadScheduler,
213214
groupChannel,
215+
htmlTextDirection,
216+
forceLeftToRightMessageLayout,
214217
} = config;
215218
const sdk = globalStore?.stores?.sdkStore?.sdk;
216219
const sdkInit = globalStore?.stores?.sdkStore?.initialized;
@@ -378,6 +381,12 @@ const ChannelProvider = (props: ChannelContextProps) => {
378381
markAsReadScheduler,
379382
});
380383

384+
useMessageLayoutDirection(
385+
htmlTextDirection,
386+
forceLeftToRightMessageLayout,
387+
loading,
388+
);
389+
381390
// callbacks for Message CURD actions
382391
const deleteMessage = useDeleteMessageCallback(
383392
{ currentGroupChannel, messagesDispatcher },

src/modules/GroupChannel/context/GroupChannelProvider.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import PUBSUB_TOPICS, { PubSubSendMessagePayload } from '../../../lib/pubSub/top
2626
import { PubSubTypes } from '../../../lib/pubSub';
2727
import { useMessageActions } from './hooks/useMessageActions';
2828
import { getIsReactionEnabled } from '../../../utils/getIsReactionEnabled';
29+
import { useMessageLayoutDirection } from '../../../hooks/useHTMLTextDirection';
2930

3031
type OnBeforeHandler<T> = (params: T) => T | Promise<T>;
3132
type MessageListQueryParamsType = Omit<MessageCollectionParams, 'filter'> & MessageFilterParams;
@@ -47,6 +48,7 @@ interface ContextBaseType {
4748
disableUserProfile?: boolean;
4849
disableMarkAsRead?: boolean;
4950
scrollBehavior?: 'smooth' | 'auto';
51+
forceLeftToRightMessageLayout?: boolean;
5052

5153
startingPoint?: number;
5254

@@ -141,7 +143,7 @@ export const GroupChannelProvider = (props: GroupChannelProviderProps) => {
141143
const { config, stores } = useSendbirdStateContext();
142144

143145
const { sdkStore } = stores;
144-
const { markAsReadScheduler, logger } = config;
146+
const { markAsReadScheduler, logger, htmlTextDirection, forceLeftToRightMessageLayout } = config;
145147

146148
// State
147149
const [quoteMessage, setQuoteMessage] = useState<SendableMessageType | null>(null);
@@ -253,6 +255,12 @@ export const GroupChannelProvider = (props: GroupChannelProviderProps) => {
253255
if (_animatedMessageId) setAnimatedMessageId(_animatedMessageId);
254256
}, [_animatedMessageId]);
255257

258+
useMessageLayoutDirection(
259+
htmlTextDirection,
260+
forceLeftToRightMessageLayout,
261+
messageDataSource.loading,
262+
);
263+
256264
const scrollToBottom = usePreservedCallback(async (animated?: boolean) => {
257265
if (!scrollRef.current) return;
258266

src/modules/Thread/components/ThreadUI/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import Label, { LabelTypography, LabelColors } from '../../../../ui/Label';
1818
import { isAboutSame } from '../../context/utils';
1919
import { MessageProvider } from '../../../Message/context/MessageProvider';
2020
import { SendableMessageType } from '../../../../utils';
21+
import { classnames } from '../../../../utils/utils';
2122

2223
export interface ThreadUIProps {
2324
renderHeader?: () => React.ReactElement;
@@ -148,7 +149,7 @@ const ThreadUI: React.FC<ThreadUIProps> = ({
148149
)
149150
}
150151
<div
151-
className="sendbird-thread-ui--scroll"
152+
className={classnames('sendbird-thread-ui--scroll, sendbird-conversation__messages')}
152153
ref={scrollRef}
153154
onScroll={onScroll}
154155
>

src/modules/Thread/context/ThreadProvider.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { PublishingModuleType, useSendMultipleFilesMessage } from './hooks/useSe
3333
import { SendableMessageType } from '../../../utils';
3434
import { useThreadFetchers } from './hooks/useThreadFetchers';
3535
import type { OnBeforeDownloadFileMessageType } from '../../GroupChannel/context/GroupChannelProvider';
36+
import { useMessageLayoutDirection } from '../../../hooks/useHTMLTextDirection';
3637

3738
export type ThreadProviderProps = {
3839
children?: React.ReactElement;
@@ -93,7 +94,7 @@ export const ThreadProvider = (props: ThreadProviderProps) => {
9394
const { user } = userStore;
9495
const sdkInit = sdkStore?.initialized;
9596
// // config
96-
const { logger, pubSub, onUserProfileMessage } = config;
97+
const { logger, pubSub, onUserProfileMessage, htmlTextDirection, forceLeftToRightMessageLayout } = config;
9798

9899
const isMentionEnabled = config.groupChannel.enableMention;
99100
const isReactionEnabled = config.groupChannel.enableReactions;
@@ -167,6 +168,13 @@ export const ThreadProvider = (props: ThreadProviderProps) => {
167168
}
168169
}, [stores.sdkStore.initialized, config.isOnline, initialize]);
169170

171+
useMessageLayoutDirection(
172+
htmlTextDirection,
173+
forceLeftToRightMessageLayout,
174+
// we're assuming that if the thread message list is empty, it's in the loading state
175+
allThreadMessages.length === 0,
176+
);
177+
170178
const toggleReaction = useToggleReactionCallback({ currentChannel }, { logger });
171179

172180
// Send Message Hooks

src/stories/apps/GroupChannelApp.stories.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const meta: Meta<typeof App> = {
3030
'isMessageGroupingEnabled',
3131
'disableAutoSelect',
3232
'htmlTextDirection',
33+
'forceLeftToRightMessageLayout',
3334
],
3435
},
3536
},
@@ -142,6 +143,12 @@ const meta: Meta<typeof App> = {
142143
description: 'A property that sets the text direction of the HTML. `ltr` is for left-to-right, and `rtl` is for right-to-left.',
143144
control: 'radio',
144145
options: ['ltr', 'rtl'],
146+
},
147+
forceLeftToRightMessageLayout: {
148+
type: 'boolean',
149+
description:
150+
'A property that forces the layout of the message to be left-to-right. This would be only useful when the htmlTextDirection is set to right-to-left, but you want to display the message layout in left-to-right.',
151+
control: 'boolean',
145152
}
146153
},
147154
};
@@ -177,4 +184,5 @@ Default.args = {
177184
isMessageGroupingEnabled: true,
178185
disableAutoSelect: false,
179186
htmlTextDirection: 'ltr',
187+
forceLeftToRightMessageLayout: false,
180188
};

src/styles/_postcss-rtl.scss

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// This mixin adjusts CSS properties to handle RTL (right-to-left) layout fixes especially when
2+
// - htmlTextDirection='rtl'
3+
// - forceLeftToRightMessageLayout=true
4+
// for specific components to increase specificity and ensure proper alignment.
5+
@mixin layout-adjustments() {
6+
.sendbird-avatar-img {
7+
left: 50%;
8+
top: 50%;
9+
transform: translate(-50%, -50%);
10+
}
11+
.sendbird-voice-message-item-body__playback-time {
12+
position: absolute;
13+
right: 12px;
14+
left: unset;
15+
top: 15px;
16+
}
17+
.sendbird-reaction-badge__inner {
18+
padding-left: 20px;
19+
padding-right: 4px;
20+
.sendbird-reaction-badge__inner__icon {
21+
left: 4px;
22+
right: unset;
23+
}
24+
}
25+
}
26+
27+
.sendbird-message-content {
28+
@include layout-adjustments();
29+
}
30+
31+
.sendbird-thread-list-item-content {
32+
@include layout-adjustments();
33+
}
34+
35+
.sendbird-parent-message-info {
36+
@include layout-adjustments();
37+
}

0 commit comments

Comments
 (0)