Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from 'react';
import { I18nManager, Text as RNText, TextProps as RNTextProps, StyleSheet, TextStyle } from 'react-native';

import { isStartsWithRTL } from '@sendbird/uikit-utils';

import useUIKitTheme from '../../theme/useUIKitTheme';
import type { TypoName, UIKitTheme } from '../../types';
import { isStartsWithRTL } from './isStartsWithRTL';

export interface RTLTextAlignSupportProps {
/**
Expand Down Expand Up @@ -33,6 +34,10 @@ const Text = ({ children, color, style, supportRTLAlign = true, originalText, ..
]) as TextStyle;

const textAlign = (() => {
if (textStyle.textAlign && textStyle.textAlign !== 'left' && textStyle.textAlign !== 'right') {
return textStyle.textAlign;
}

if (I18nManager.isRTL && supportRTLAlign) {
if (
(originalText && isStartsWithRTL(originalText)) ||
Expand All @@ -43,6 +48,7 @@ const Text = ({ children, color, style, supportRTLAlign = true, originalText, ..
return I18nManager.doLeftAndRightSwapInRTL ? 'right' : 'left';
}
}

if (textStyle.textAlign) return textStyle.textAlign;
return undefined;
})();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from 'react';
import { I18nManager, TextInput as RNTextInput, StyleSheet, TextInputProps, TextStyle } from 'react-native';

import { isStartsWithRTL } from '@sendbird/uikit-utils';

import createStyleSheet from '../../styles/createStyleSheet';
import useUIKitTheme from '../../theme/useUIKitTheme';
import type { UIKitTheme } from '../../types';
import { RTLTextAlignSupportProps } from '../Text';
import { isStartsWithRTL } from '../Text/isStartsWithRTL';

type Props = {
variant?: keyof UIKitTheme['colors']['ui']['input'];
Expand Down Expand Up @@ -34,6 +35,10 @@ const TextInput = React.forwardRef<RNTextInput, Props>(function TextInput(
]) as TextStyle;

const textAlign = (() => {
if (textStyle.textAlign && textStyle.textAlign !== 'left' && textStyle.textAlign !== 'right') {
return textStyle.textAlign;
}

if (I18nManager.isRTL && supportRTLAlign) {
const text = originalText || props.value || props.placeholder;
// Note: TextInput is not affected by doLeftAndRightSwapInRTL
Expand All @@ -44,7 +49,7 @@ const TextInput = React.forwardRef<RNTextInput, Props>(function TextInput(
}
}

if (textStyle.textAlign) textStyle.textAlign;
if (textStyle.textAlign) return textStyle.textAlign;
return undefined;
})();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage'
let seekFinished = !shouldSeekToTime;

const forPlayback = playerService.addPlaybackListener(({ stopped, currentTime, duration }) => {
voiceMessageStatusManager.setCurrentTime(message.channelUrl, message.messageId, currentTime);
voiceMessageStatusManager.setCurrentTime(message.channelUrl, message.messageId, stopped ? 0 : currentTime);
if (seekFinished) {
setState((prevState) => ({ ...prevState, currentTime: stopped ? 0 : currentTime, duration }));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type VoiceFileMessageState = {
type Props = ThreadParentMessageRendererProps<{
durationMetaArrayKey?: string;
onUnmount: () => void;
initialCurrentTime?: number;
}>;

const ThreadParentMessageFileVoice = (props: Props) => {
Expand All @@ -25,6 +26,7 @@ const ThreadParentMessageFileVoice = (props: Props) => {
parentMessage,
durationMetaArrayKey = 'KEY_VOICE_MESSAGE_DURATION',
onUnmount,
initialCurrentTime,
} = props;

const fileMessage: SendbirdFileMessage = parentMessage as SendbirdFileMessage;
Expand All @@ -38,7 +40,7 @@ const ThreadParentMessageFileVoice = (props: Props) => {
const initialDuration = value ? parseInt(value, 10) : 0;
return {
status: 'paused',
currentTime: 0,
currentTime: initialCurrentTime || 0,
duration: initialDuration,
};
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export type ThreadParentMessageRendererProps<AdditionalProps = unknown> = {
const ThreadParentMessageRenderer = (props: ThreadParentMessageRendererProps) => {
const handlers = useSBUHandlers();
const playerUnsubscribes = useRef<(() => void)[]>([]);
const { sbOptions, currentUser, mentionManager } = useSendbirdChat();
const { sbOptions, currentUser, mentionManager, voiceMessageStatusManager } = useSendbirdChat();
const { palette } = useUIKitTheme();
const { mediaService, playerService } = usePlatformService();
const parentMessage = props.parentMessage;
Expand Down Expand Up @@ -68,6 +68,11 @@ const ThreadParentMessageRenderer = (props: ThreadParentMessageRendererProps) =>
let seekFinished = !shouldSeekToTime;

const forPlayback = playerService.addPlaybackListener(({ stopped, currentTime, duration }) => {
voiceMessageStatusManager.setCurrentTime(
parentMessage.channelUrl,
parentMessage.messageId,
stopped ? 0 : currentTime,
);
if (seekFinished) {
setState((prevState) => ({ ...prevState, currentTime: stopped ? 0 : currentTime, duration }));
}
Expand Down Expand Up @@ -177,6 +182,10 @@ const ThreadParentMessageRenderer = (props: ThreadParentMessageRendererProps) =>
return (
<ThreadParentMessageFileVoice
durationMetaArrayKey={VOICE_MESSAGE_META_ARRAY_DURATION_KEY}
initialCurrentTime={voiceMessageStatusManager.getCurrentTime(
parentMessage.channelUrl,
parentMessage.messageId,
)}
onUnmount={() => {
if (isVoiceMessage(parentMessage) && playerService.uri === parentMessage.url) {
resetPlayer().catch((_) => {});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,6 @@ const useConfigInstance = ({ imageCompression, userMention, voiceMessage }: Send
debounceMills: userMention?.debounceMills ?? MentionConfig.DEFAULT.DEBOUNCE_MILLS,
delimiter: MentionConfig.DEFAULT.DELIMITER,
trigger: MentionConfig.DEFAULT.TRIGGER,
forceTriggerLeftInRTL: MentionConfig.DEFAULT.FORCE_TRIGGER_LEFT_IN_RTL,
});
}, [userMention?.mentionLimit, userMention?.suggestionLimit, userMention?.debounceMills]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): G
await recorderService.reset().catch(() => {});
};
const _onPressHeaderLeft = useFreshCallback(async () => {
voiceMessageStatusManager.clear();
await onBlurFragment();
onPressHeaderLeft();
});
Expand All @@ -144,6 +143,7 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): G

useEffect(() => {
return () => {
voiceMessageStatusManager.clear();
onBlurFragment();
};
}, []);
Expand Down
12 changes: 0 additions & 12 deletions packages/uikit-react-native/src/libs/MentionConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,6 @@ export interface MentionConfigInterface {
debounceMills: number;
delimiter: string;
trigger: string;
/**
* This configuration keeps the trigger positioned to the left in RTL mode, instead of being placed after `username@`.
* @example
* RTL: `@username`
* LTR: `@username`
*/
forceTriggerLeftInRTL: boolean;
}

class MentionConfig {
Expand All @@ -20,7 +13,6 @@ class MentionConfig {
DEBOUNCE_MILLS: 300,
DELIMITER: ' ',
TRIGGER: '@',
FORCE_TRIGGER_LEFT_IN_RTL: true,
};
constructor(private _config: MentionConfigInterface) {}

Expand All @@ -43,10 +35,6 @@ class MentionConfig {
get trigger() {
return this._config.trigger;
}

get forceTriggerLeftInRTL() {
return this._config.forceTriggerLeftInRTL;
}
}

export default MentionConfig;
25 changes: 15 additions & 10 deletions packages/uikit-react-native/src/libs/MentionManager.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React from 'react';
import { I18nManager } from 'react-native';

import { Text, createStyleSheet } from '@sendbird/uikit-react-native-foundation';
import type { SendbirdFileMessage, SendbirdUser, SendbirdUserMessage } from '@sendbird/uikit-utils';
import { createMentionTemplateRegex, replaceWithRegex } from '@sendbird/uikit-utils';
import { createMentionTemplateRegex, isEndsWithRTL, replaceWithRegex } from '@sendbird/uikit-utils';

import type { MentionedUser, Range } from '../types';
import type { MentionConfigInterface } from './MentionConfig';
Expand All @@ -22,12 +21,18 @@ class MentionManager {
this._templateRegex = createMentionTemplateRegex(this.config.trigger);
}

get triggerDirPrefixForDisplay() {
if (this.config.forceTriggerLeftInRTL) {
return SPAN_DIRECTION.LRM;
}
return I18nManager.isRTL ? SPAN_DIRECTION.RLM : SPAN_DIRECTION.LRM;
}
// Note: When the input starts in LTR and the mentioned user's name is in RTL, it appears as "Hello @{cibarA}."
// If typing continues in RTL, the mention is rendered as: "Hello @{txeTlanoitiddA}{cibarA}."
//
// Conversely, if the input starts in RTL and the mentioned user's name is in LTR, it appears as "{Eng}@ cibarA."
// If typing continues, it is rendered as: "{Eng}{AdditionalText}@ cibarA."
//
// While this follows the natural text direction, it can make mentions harder to distinguish.
// To address this, we use the RLM or LRM Unicode characters to reset subsequent spans based on the last text string of the user's name.
// By applying this trick, the result will be displayed as "Hello @{cibarA} {txeTlanoitiddA}" or "{AdditionalText} {Eng}@ cibarA," ensuring the mention block remains clearly distinguishable.
getDirectionOfNextSpan = (name: string) => {
return isEndsWithRTL(name) ? SPAN_DIRECTION.LRM : SPAN_DIRECTION.RLM;
};

public rangeHelpers = {
inRangeUnderOver(start: number, num: number, end: number) {
Expand Down Expand Up @@ -131,9 +136,9 @@ class MentionManager {
* @description User to @user.nickname text format
* */
public asMentionedMessageText = (user: SendbirdUser, delimiter = false) => {
const prefix = this.triggerDirPrefixForDisplay;
const prefix = '';
const content = `${this.config.trigger}${user.nickname}`;
const postfix = delimiter ? this.config.delimiter : '';
const postfix = this.getDirectionOfNextSpan(user.nickname) + (delimiter ? this.config.delimiter : '');

return prefix + content + postfix;
};
Expand Down
1 change: 1 addition & 0 deletions packages/uikit-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { default as arrayToMap, arrayToMapWithGetter } from './shared/arrayToMap
export * from './shared/regex';
export * from './shared/bufferedRequest';
export * from './shared/file';
export * from './shared/rtl';
export * from './shared';

export * from './hooks';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const isStartsWithRTL = (str?: string): boolean => {
const isRTLString = (dir: 'start' | 'end', str?: string) => {
if (!str || str.length === 0) {
return false;
}
Expand All @@ -8,15 +8,23 @@ export const isStartsWithRTL = (str?: string): boolean => {
return false;
}

const firstChar = Array.from(trimmedStr)[0];
const point = firstChar.codePointAt(0);
const char = dir === 'start' ? Array.from(trimmedStr)[0] : Array.from(trimmedStr).pop();
const point = char?.codePointAt(0);
if (point === undefined) {
return false;
}

return isRTLCodePoint(point);
};

export const isStartsWithRTL = (str?: string): boolean => {
return isRTLString('start', str);
};

export const isEndsWithRTL = (str?: string): boolean => {
return isRTLString('end', str);
};

const isRTLCodePoint = (codePoint: number) => {
for (const [start, end] of rtlCodePointRanges) {
if (codePoint >= start && codePoint <= end) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const OpenChannelListFragment = createOpenChannelListFragment({
List: (props) => {
const { colors } = useUIKitTheme();
return (
<Box flex={1}>
<Box flex={1} alignItems={'flex-start'}>
<Text body2 color={colors.onBackground02} style={{ paddingHorizontal: 16, paddingTop: 16, paddingBottom: 4 }}>
{'Preset channels developed by UIKit'}
</Text>
Expand Down
Loading