From 18d53ebc67012fac4b4c889655bd32029080861e Mon Sep 17 00:00:00 2001 From: OnestarLee Date: Tue, 31 Mar 2026 15:03:45 +0900 Subject: [PATCH 1/3] feat: add customizable placeholder text for ChannelInput component --- .../src/components/ChannelInput/SendInput.tsx | 3 +++ .../src/components/ChannelInput/index.tsx | 3 +++ .../src/domain/groupChannel/types.ts | 4 +++- .../src/domain/groupChannelThread/types.ts | 4 +++- .../src/domain/openChannel/types.ts | 4 +++- .../fragments/createGroupChannelFragment.tsx | 2 ++ .../createGroupChannelThreadFragment.tsx | 2 ++ .../src/fragments/createOpenChannelFragment.tsx | 2 ++ .../src/hooks/useMentionTextInput.ts | 17 +++++++++++++++-- packages/uikit-react-native/src/index.ts | 4 ++++ 10 files changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx b/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx index 98abbe665..8c434422e 100644 --- a/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx +++ b/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx @@ -56,6 +56,7 @@ const SendInput = forwardRef(function SendInput( setMessageToReply, messageForThread, partialTextInputProps, + placeholder, }, ref, ) { @@ -174,6 +175,8 @@ const SendInput = forwardRef(function SendInput( const sheetItems = useChannelInputItems(channel, sendFileMessage); const getPlaceholder = () => { + if (placeholder) return placeholder; + if (inputMuted) return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_MUTED; if (inputFrozen) return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_DISABLED; if (inputDisabled) return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_DISABLED; diff --git a/packages/uikit-react-native/src/components/ChannelInput/index.tsx b/packages/uikit-react-native/src/components/ChannelInput/index.tsx index c92a41e37..b5b4d65ce 100644 --- a/packages/uikit-react-native/src/components/ChannelInput/index.tsx +++ b/packages/uikit-react-native/src/components/ChannelInput/index.tsx @@ -91,6 +91,9 @@ export type ChannelInputProps = { // TextInput props - only safe properties that don't interfere with UIKit functionality partialTextInputProps?: Partial>; + + /** Custom placeholder text that overrides all default localization strings including muted/frozen/disabled states. */ + placeholder?: string; }; const AUTO_FOCUS = Platform.select({ ios: false, android: true, default: false }); diff --git a/packages/uikit-react-native/src/domain/groupChannel/types.ts b/packages/uikit-react-native/src/domain/groupChannel/types.ts index a1ba976c7..67d1d08c5 100644 --- a/packages/uikit-react-native/src/domain/groupChannel/types.ts +++ b/packages/uikit-react-native/src/domain/groupChannel/types.ts @@ -54,6 +54,7 @@ export interface GroupChannelProps { searchItem?: GroupChannelProps['MessageList']['searchItem']; partialTextInputProps?: GroupChannelProps['Input']['partialTextInputProps']; + placeholder?: GroupChannelProps['Input']['placeholder']; /** * @description You can specify the query parameters for the message list. @@ -114,7 +115,8 @@ export interface GroupChannelProps { | 'onPressUpdateFileMessage' | 'SuggestedMentionList' | 'AttachmentsButton' - | 'partialTextInputProps', + | 'partialTextInputProps' + | 'placeholder', 'inputDisabled' >; diff --git a/packages/uikit-react-native/src/domain/groupChannelThread/types.ts b/packages/uikit-react-native/src/domain/groupChannelThread/types.ts index 61f247d4e..50a903bd5 100644 --- a/packages/uikit-react-native/src/domain/groupChannelThread/types.ts +++ b/packages/uikit-react-native/src/domain/groupChannelThread/types.ts @@ -43,6 +43,7 @@ export interface GroupChannelThreadProps { keyboardAvoidOffset?: GroupChannelThreadProps['Provider']['keyboardAvoidOffset']; flatListProps?: GroupChannelThreadProps['MessageList']['flatListProps']; sortComparator?: UseGroupChannelMessagesOptions['sortComparator']; + placeholder?: GroupChannelThreadProps['Input']['placeholder']; }; Header: { onPressLeft: () => void; @@ -85,7 +86,8 @@ export interface GroupChannelThreadProps { | 'onPressUpdateUserMessage' | 'onPressUpdateFileMessage' | 'SuggestedMentionList' - | 'AttachmentsButton', + | 'AttachmentsButton' + | 'placeholder', 'inputDisabled' >; diff --git a/packages/uikit-react-native/src/domain/openChannel/types.ts b/packages/uikit-react-native/src/domain/openChannel/types.ts index 1a160aa41..eb00ccb02 100644 --- a/packages/uikit-react-native/src/domain/openChannel/types.ts +++ b/packages/uikit-react-native/src/domain/openChannel/types.ts @@ -44,6 +44,7 @@ export type OpenChannelProps = { flatListProps?: OpenChannelProps['MessageList']['flatListProps']; sortComparator?: UseOpenChannelMessagesOptions['sortComparator']; queryCreator?: UseOpenChannelMessagesOptions['queryCreator']; + placeholder?: OpenChannelProps['Input']['placeholder']; }; Header: { rightIconName: keyof typeof Icon.Assets; @@ -78,7 +79,8 @@ export type OpenChannelProps = { | 'onPressSendFileMessage' | 'onPressUpdateUserMessage' | 'onPressUpdateFileMessage' - | 'AttachmentsButton', + | 'AttachmentsButton' + | 'placeholder', 'inputDisabled' >; diff --git a/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx b/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx index 88bdce944..8ad3ec4f7 100644 --- a/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx +++ b/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx @@ -71,6 +71,7 @@ const createGroupChannelFragment = (initModule?: Partial): G flatListProps, messageListQueryParams, partialTextInputProps, + placeholder, collectionCreator, }) => { const { playerService, recorderService } = usePlatformService(); @@ -343,6 +344,7 @@ const createGroupChannelFragment = (initModule?: Partial): G onPressUpdateUserMessage={onPressUpdateUserMessage} onPressUpdateFileMessage={onPressUpdateFileMessage} partialTextInputProps={partialTextInputProps} + placeholder={placeholder} /> diff --git a/packages/uikit-react-native/src/fragments/createGroupChannelThreadFragment.tsx b/packages/uikit-react-native/src/fragments/createGroupChannelThreadFragment.tsx index f091fed00..0015c2c51 100644 --- a/packages/uikit-react-native/src/fragments/createGroupChannelThreadFragment.tsx +++ b/packages/uikit-react-native/src/fragments/createGroupChannelThreadFragment.tsx @@ -53,6 +53,7 @@ const createGroupChannelThreadFragment = ( keyboardAvoidOffset, sortComparator = threadMessageComparator, flatListProps, + placeholder, }) => { const { playerService, recorderService } = usePlatformService(); const { sdk, currentUser, sbOptions, voiceMessageStatusManager, groupChannelFragmentOptions } = useSendbirdChat(); @@ -259,6 +260,7 @@ const createGroupChannelThreadFragment = ( onPressSendFileMessage={onPressSendFileMessage} onPressUpdateUserMessage={onPressUpdateUserMessage} onPressUpdateFileMessage={onPressUpdateFileMessage} + placeholder={placeholder} /> diff --git a/packages/uikit-react-native/src/fragments/createOpenChannelFragment.tsx b/packages/uikit-react-native/src/fragments/createOpenChannelFragment.tsx index 0fe012c47..6d1fdb954 100644 --- a/packages/uikit-react-native/src/fragments/createOpenChannelFragment.tsx +++ b/packages/uikit-react-native/src/fragments/createOpenChannelFragment.tsx @@ -52,6 +52,7 @@ const createOpenChannelFragment = (initModule?: Partial): Ope flatListProps, queryCreator, sortComparator = messageComparator, + placeholder, }) => { const { sdk, currentUser } = useSendbirdChat(); @@ -182,6 +183,7 @@ const createOpenChannelFragment = (initModule?: Partial): Ope onPressSendFileMessage={onPressSendFileMessage} onPressUpdateUserMessage={onPressUpdateUserMessage} onPressUpdateFileMessage={onPressUpdateFileMessage} + placeholder={placeholder} /> diff --git a/packages/uikit-react-native/src/hooks/useMentionTextInput.ts b/packages/uikit-react-native/src/hooks/useMentionTextInput.ts index 48eaf9562..0e8d73c48 100644 --- a/packages/uikit-react-native/src/hooks/useMentionTextInput.ts +++ b/packages/uikit-react-native/src/hooks/useMentionTextInput.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import type { NativeSyntheticEvent, TextInput, TextInputSelectionChangeEventData } from 'react-native'; import { Platform } from 'react-native'; @@ -7,8 +7,21 @@ import { SendbirdFileMessage, SendbirdUserMessage, replace, useFreshCallback } f import type { MentionedUser } from '../types'; import { useSendbirdChat } from './useContext'; +export interface UseMentionTextInputParams { + messageToEdit?: SendbirdUserMessage | SendbirdFileMessage; +} + +export interface UseMentionTextInputReturn { + textInputRef: React.RefObject; + selection: { start: number; end: number }; + onSelectionChange: (e: NativeSyntheticEvent) => void; + text: string; + onChangeText: (text: string, addedMentionedUser?: MentionedUser) => void; + mentionedUsers: MentionedUser[]; +} + // Note: The selection change with the keyboard cursor might not work properly with RTL languages -const useMentionTextInput = (params: { messageToEdit?: SendbirdUserMessage | SendbirdFileMessage }) => { +const useMentionTextInput = (params: UseMentionTextInputParams): UseMentionTextInputReturn => { const { mentionManager, sbOptions } = useSendbirdChat(); const mentionedUsersRef = useRef([]); diff --git a/packages/uikit-react-native/src/index.ts b/packages/uikit-react-native/src/index.ts index 51053f8ca..d596a3cc4 100644 --- a/packages/uikit-react-native/src/index.ts +++ b/packages/uikit-react-native/src/index.ts @@ -6,6 +6,8 @@ import { PromisePolyfill } from './utils/promise'; /** Components **/ export { default as ChannelInput } from './components/ChannelInput'; +export type { ChannelInputProps } from './components/ChannelInput'; +export { default as EditInput } from './components/ChannelInput/EditInput'; export { default as ChannelMessageList } from './components/ChannelMessageList'; export { default as GroupChannelMessageRenderer } from './components/GroupChannelMessageRenderer'; export { default as OpenChannelMessageRenderer } from './components/OpenChannelMessageRenderer'; @@ -61,6 +63,8 @@ export { LocalizationContext, LocalizationProvider } from './contexts/Localizati export { default as useConnection } from './hooks/useConnection'; export { default as usePushTokenRegistration } from './hooks/usePushTokenRegistration'; export * from './hooks/useContext'; +export { default as useMentionTextInput } from './hooks/useMentionTextInput'; +export type { UseMentionTextInputParams, UseMentionTextInputReturn } from './hooks/useMentionTextInput'; /** Localization **/ export { createBaseStringSet } from './localization/createBaseStringSet'; From c81c277d2b961c991cba68c94e8395162dc25933 Mon Sep 17 00:00:00 2001 From: OnestarLee Date: Tue, 31 Mar 2026 16:35:59 +0900 Subject: [PATCH 2/3] chore: enhance placeholder functionality in SendInput component --- .../src/components/ChannelInput/SendInput.tsx | 2 +- .../uikit-react-native/src/components/ChannelInput/index.tsx | 4 +++- packages/uikit-react-native/src/hooks/useMentionTextInput.ts | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx b/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx index 8c434422e..73ee0dc76 100644 --- a/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx +++ b/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx @@ -175,7 +175,7 @@ const SendInput = forwardRef(function SendInput( const sheetItems = useChannelInputItems(channel, sendFileMessage); const getPlaceholder = () => { - if (placeholder) return placeholder; + if (placeholder != null) return placeholder; if (inputMuted) return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_MUTED; if (inputFrozen) return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_DISABLED; diff --git a/packages/uikit-react-native/src/components/ChannelInput/index.tsx b/packages/uikit-react-native/src/components/ChannelInput/index.tsx index b5b4d65ce..af428fa52 100644 --- a/packages/uikit-react-native/src/components/ChannelInput/index.tsx +++ b/packages/uikit-react-native/src/components/ChannelInput/index.tsx @@ -92,7 +92,9 @@ export type ChannelInputProps = { // TextInput props - only safe properties that don't interfere with UIKit functionality partialTextInputProps?: Partial>; - /** Custom placeholder text that overrides all default localization strings including muted/frozen/disabled states. */ + /** Custom placeholder text for the send input. + * Overrides all default localization strings (including muted/frozen/disabled states) for send mode only. + * Edit mode continues to use the default localized placeholder. */ placeholder?: string; }; diff --git a/packages/uikit-react-native/src/hooks/useMentionTextInput.ts b/packages/uikit-react-native/src/hooks/useMentionTextInput.ts index 0e8d73c48..cfc274117 100644 --- a/packages/uikit-react-native/src/hooks/useMentionTextInput.ts +++ b/packages/uikit-react-native/src/hooks/useMentionTextInput.ts @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import { type RefObject, useEffect, useRef, useState } from 'react'; import type { NativeSyntheticEvent, TextInput, TextInputSelectionChangeEventData } from 'react-native'; import { Platform } from 'react-native'; @@ -12,7 +12,7 @@ export interface UseMentionTextInputParams { } export interface UseMentionTextInputReturn { - textInputRef: React.RefObject; + textInputRef: RefObject; selection: { start: number; end: number }; onSelectionChange: (e: NativeSyntheticEvent) => void; text: string; From 1d43cfbea60031510945c4c9393586304271a0bd Mon Sep 17 00:00:00 2001 From: OnestarLee Date: Wed, 1 Apr 2026 11:10:50 +0900 Subject: [PATCH 3/3] chore: fix textInputRef type to use RefObject for React compatibility Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/ChannelInput/index.tsx | 6 +++--- .../uikit-react-native/src/hooks/useMentionTextInput.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/uikit-react-native/src/components/ChannelInput/index.tsx b/packages/uikit-react-native/src/components/ChannelInput/index.tsx index af428fa52..38812b3d9 100644 --- a/packages/uikit-react-native/src/components/ChannelInput/index.tsx +++ b/packages/uikit-react-native/src/components/ChannelInput/index.tsx @@ -172,7 +172,7 @@ const ChannelInput = (props: ChannelInputProps) => { { void, chatDisabled: bo }; const useAutoFocusOnEditMode = ( - textInputRef: React.MutableRefObject, + textInputRef: React.RefObject, messageToEdit?: SendbirdBaseMessage, ) => { useEffect(() => { diff --git a/packages/uikit-react-native/src/hooks/useMentionTextInput.ts b/packages/uikit-react-native/src/hooks/useMentionTextInput.ts index cfc274117..9c8342b72 100644 --- a/packages/uikit-react-native/src/hooks/useMentionTextInput.ts +++ b/packages/uikit-react-native/src/hooks/useMentionTextInput.ts @@ -12,7 +12,7 @@ export interface UseMentionTextInputParams { } export interface UseMentionTextInputReturn { - textInputRef: RefObject; + textInputRef: RefObject; selection: { start: number; end: number }; onSelectionChange: (e: NativeSyntheticEvent) => void; text: string; @@ -25,7 +25,7 @@ const useMentionTextInput = (params: UseMentionTextInputParams): UseMentionTextI const { mentionManager, sbOptions } = useSendbirdChat(); const mentionedUsersRef = useRef([]); - const textInputRef = useRef(undefined); + const textInputRef = useRef(null); const [text, setText] = useState(''); const [selection, setSelection] = useState({ start: 0, end: 0 });