So, we want to customize GroupChannelModule component Input, and the steps we followed to achieve our use-case are as follows:
We named Customized input component as CustomInput and replaced the module default Input by CustomInput.
const CustomGroupChannelModule = createGroupChannelModule({
Input: CustomInput,
});
Our CustomInput code is as follows:
const CustomInput = ({
SuggestedMentionList,
shouldRenderInput,
onPressSendUserMessage,
onPressUpdateUserMessage,
onPressUpdateFileMessage,
}) => {
const {
channel,
messageToEdit,
setMessageToEdit,
keyboardAvoidOffset = 0,
} = useContext(GroupChannelContexts.Fragment);
const chatAvailableState = getGroupChannelChatAvailableState(channel);
return (
<CustomChannelInput
channel={channel}
messageToEdit={messageToEdit}
setMessageToEdit={setMessageToEdit}
inputMuted={chatAvailableState.muted}
inputFrozen={chatAvailableState.frozen}
inputDisabled={chatAvailableState.disabled}
keyboardAvoidOffset={keyboardAvoidOffset}
shouldRenderInput={shouldRenderInput}
onPressSendUserMessage={onPressSendUserMessage}
onPressSendFileMessage={onPressSendFileMessage}
onPressUpdateUserMessage={onPressUpdateUserMessage}
onPressUpdateFileMessage={onPressUpdateFileMessage}
SuggestedMentionList={SuggestedMentionList}
/>
);
};
Our CustomChannelInput component is below:
const CustomChannelInput = ({
channel,
messageToEdit,
setMessageToEdit,
inputMuted,
inputFrozen,
inputDisabled,
keyboardAvoidOffset,
shouldRenderInput,
onPressSendUserMessage,
onPressSendFileMessage,
onPressUpdateUserMessage,
onPressUpdateFileMessage,
SuggestedMentionList,
}) => {
const GET_INPUT_KEY = (shouldReset) =>
shouldReset ? "uikit-input-clear" : "uikit-input";
const AUTO_FOCUS = Platform.select({
ios: false,
android: true,
default: false,
});
const KEYBOARD_AVOID_VIEW_BEHAVIOR = Platform.select({
ios: "padding",
default: undefined,
});
const { top, left, right, bottom } = useSafeAreaInsets();
const { features, mentionManager } = useSendbirdChat();
const {
selection,
onSelectionChange,
textInputRef,
text,
onChangeText,
mentionedUsers,
} = useMentionTextInput({
messageToEdit,
});
const inputMode = useIIFE(() => {
if (!messageToEdit) return "send";
if (messageToEdit.isFileMessage()) return "send";
return "edit";
});
const mentionAvailable =
features.userMentionEnabled &&
channel.isGroupChannel() &&
!channel.isBroadcast;
const inputKeyToRemount = GET_INPUT_KEY(
mentionAvailable ? mentionedUsers.length === 0 : false
);
const [inputHeight, setInputHeight] = useState(
styles.inputDefault.height
);
const useTypingTrigger = (text, channel) => {
if (channel.isGroupChannel()) {
useEffect(() => {
if (text.length === 0) channel.endTyping();
else channel.startTyping();
}, [text]);
}
};
const useTextPersistenceOnDisabled = (text, setText, chatDisabled) => {
const textTmpRef = useRef("");
useEffect(() => {
if (chatDisabled) {
textTmpRef.current = text;
setText("");
} else {
setText(textTmpRef.current);
}
}, [chatDisabled]);
};
const useAutoFocusOnEditMode = (textInputRef, messageToEdit) => {
useEffect(() => {
if (messageToEdit?.isUserMessage()) {
if (!AUTO_FOCUS)
setTimeout(() => textInputRef.current?.focus(), 500);
}
}, [messageToEdit]);
};
useTypingTrigger(text, channel);
useTextPersistenceOnDisabled(text, onChangeText, inputDisabled);
useAutoFocusOnEditMode(textInputRef, messageToEdit);
const onPressToMention = (user, searchStringRange) => {
const mentionedMessageText = mentionManager.asMentionedMessageText(
user,
true
);
const range = {
start: searchStringRange.start,
end: searchStringRange.start + mentionedMessageText.length - 1,
};
onChangeText(
replace(
text,
searchStringRange.start,
searchStringRange.end,
mentionedMessageText
),
{ user, range }
);
};
const SafeAreaBottom = ({ height }) => {
return <View style={{ height }} />;
};
if (!shouldRenderInput) {
return <SafeAreaBottom height={bottom} />;
}
const props = {
channel,
messageToEdit,
setMessageToEdit,
inputMuted,
inputFrozen,
inputDisabled,
keyboardAvoidOffset,
shouldRenderInput,
onPressSendUserMessage,
onPressSendFileMessage,
onPressUpdateUserMessage,
onPressUpdateFileMessage,
SuggestedMentionList,
};
return (
<>
<KeyboardAvoidingView
keyboardVerticalOffset={-bottom + keyboardAvoidOffset}
behavior={KEYBOARD_AVOID_VIEW_BEHAVIOR}
>
<View
style={{
paddingLeft: left,
paddingRight: right,
backgroundColor: colors.background,
}}
>
<View
onLayout={(e) => setInputHeight(e.nativeEvent.layout.height)}
style={styles.inputContainer}
>
{inputMode === "send" && (
<CustomSendInput
{...props}
key={inputKeyToRemount}
ref={textInputRef}
text={text}
onChangeText={onChangeText}
onSelectionChange={onSelectionChange}
mentionedUsers={mentionedUsers}
variant="underline"
/>
)}
</View>
<SafeAreaBottom height={bottom} />
</View>
</KeyboardAvoidingView>
{mentionAvailable && SuggestedMentionList && (
<SuggestedMentionList
text={text}
selection={selection}
inputHeight={inputHeight}
topInset={top}
bottomInset={bottom}
onPressToMention={onPressToMention}
mentionedUsers={mentionedUsers}
/>
)}
</>
);
};
And, Our CustomSendInput code is as follows:
const CustomSendInput = forwardRef(function CustomSendInput(
{
onPressSendUserMessage,
onPressSendFileMessage,
text,
onChangeText,
onSelectionChange,
mentionedUsers,
inputDisabled,
inputFrozen,
inputMuted,
},
ref
) {
const { mentionManager, imageCompressionConfig, features } =
useSendbirdChat();
const { fileService, mediaService } = usePlatformService();
const { STRINGS } = useLocalization();
const { openSheet } = useBottomSheet();
const { alert } = useAlert();
const toast = useToast();
const onFailureToSend = () =>
toast.show(STRINGS.TOAST.SEND_MSG_ERROR, "error");
const sendUserMessage = () => {
const mentionType = MentionType.USERS;
const mentionedUserIds = mentionedUsers.map((it) => it.user.userId);
const mentionedMessageTemplate =
mentionManager.textToMentionedMessageTemplate(
text,
mentionedUsers
);
if (onPressSendUserMessage) {
onPressSendUserMessage({
message: text,
mentionType,
mentionedUserIds,
mentionedMessageTemplate,
}).catch(onFailureToSend);
}
onChangeText("");
};
const sendFileMessage = (file) => {
if (onPressSendFileMessage) {
onPressSendFileMessage({ file }).catch(onFailureToSend);
}
};
const onPressAttachment = () => {
openSheet({
sheetItems: [
{
title: STRINGS.LABELS.CHANNEL_INPUT_ATTACHMENT_CAMERA,
icon: "camera",
iconColor: inputDisabled
? colors.ui.input.default.disabled.highlight
: colors.ui.input.default.active.highlight,
onPress: async () => {
const mediaFile = await fileService.openCamera({
mediaType: "all",
onOpenFailure: (error) => {
if (
error.code === SBUError.CODE.ERR_PERMISSIONS_DENIED
) {
alert({
title: STRINGS.DIALOG.ALERT_PERMISSIONS_TITLE,
message: STRINGS.DIALOG.ALERT_PERMISSIONS_MESSAGE(
STRINGS.LABELS.PERMISSION_CAMERA,
STRINGS.LABELS.PERMISSION_APP_NAME
),
buttons: [
{
text: STRINGS.DIALOG.ALERT_PERMISSIONS_OK,
onPress: () => SBUUtils.openSettings(),
},
],
});
} else {
toast.show(STRINGS.TOAST.OPEN_CAMERA_ERROR, "error");
}
},
});
if (mediaFile) {
// Image compression
if (
isImage(mediaFile.uri, mediaFile.type) &&
shouldCompressImage(
mediaFile.type,
features.imageCompressionEnabled
)
) {
await SBUUtils.safeRun(async () => {
const compressed = await mediaService.compressImage({
uri: mediaFile.uri,
maxWidth: imageCompressionConfig.width,
maxHeight: imageCompressionConfig.height,
compressionRate:
imageCompressionConfig.compressionRate,
});
if (compressed) {
mediaFile.uri = compressed.uri;
mediaFile.size = compressed.size;
}
});
}
sendFileMessage(mediaFile);
}
},
},
{
title: STRINGS.LABELS.CHANNEL_INPUT_ATTACHMENT_PHOTO_LIBRARY,
icon: "photo",
iconColor: inputDisabled
? colors.ui.input.default.disabled.highlight
: colors.ui.input.default.active.highlight,
onPress: async () => {
const mediaFiles = await fileService.openMediaLibrary({
selectionLimit: 1,
mediaType: "all",
onOpenFailure: (error) => {
if (
error.code === SBUError.CODE.ERR_PERMISSIONS_DENIED
) {
alert({
title: STRINGS.DIALOG.ALERT_PERMISSIONS_TITLE,
message: STRINGS.DIALOG.ALERT_PERMISSIONS_MESSAGE(
STRINGS.LABELS.PERMISSION_DEVICE_STORAGE,
STRINGS.LABELS.PERMISSION_APP_NAME
),
buttons: [
{
text: STRINGS.DIALOG.ALERT_PERMISSIONS_OK,
onPress: () => SBUUtils.openSettings(),
},
],
});
} else {
toast.show(
STRINGS.TOAST.OPEN_PHOTO_LIBRARY_ERROR,
"error"
);
}
},
});
if (mediaFiles && mediaFiles[0]) {
const mediaFile = mediaFiles[0];
// Image compression
if (
isImage(mediaFile.uri, mediaFile.type) &&
shouldCompressImage(
mediaFile.type,
features.imageCompressionEnabled
)
) {
await SBUUtils.safeRun(async () => {
const compressed = await mediaService.compressImage({
uri: mediaFile.uri,
maxWidth: imageCompressionConfig.width,
maxHeight: imageCompressionConfig.height,
compressionRate:
imageCompressionConfig.compressionRate,
});
if (compressed) {
mediaFile.uri = compressed.uri;
mediaFile.size = compressed.size;
}
});
}
sendFileMessage(mediaFile);
}
},
},
{
title: STRINGS.LABELS.CHANNEL_INPUT_ATTACHMENT_FILES,
icon: "document",
iconColor: inputDisabled
? colors.ui.input.default.disabled.highlight
: colors.ui.input.default.active.highlight,
onPress: async () => {
const documentFile = await fileService.openDocument({
onOpenFailure: () =>
toast.show(STRINGS.TOAST.OPEN_FILES_ERROR, "error"),
});
if (documentFile) {
// Image compression
if (
isImage(documentFile.uri, documentFile.type) &&
shouldCompressImage(
documentFile.type,
features.imageCompressionEnabled
)
) {
await SBUUtils.safeRun(async () => {
const compressed = await mediaService.compressImage({
uri: documentFile.uri,
maxWidth: imageCompressionConfig.width,
maxHeight: imageCompressionConfig.height,
compressionRate:
imageCompressionConfig.compressionRate,
});
if (compressed) {
documentFile.uri = compressed.uri;
documentFile.size = compressed.size;
}
});
}
sendFileMessage(documentFile);
}
},
},
],
});
};
const getPlaceholder = () => {
if (!inputDisabled)
return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_ACTIVE;
if (inputFrozen)
return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_DISABLED;
if (inputMuted)
return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_MUTED;
return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_DISABLED;
};
return (
<View style={styles.sendInputContainer}>
<TouchableOpacity
onPress={onPressAttachment}
disabled={inputDisabled}
>
<Image
source={require("../../../assets/AddAttachment.png")}
style={[
styles.iconAttach,
{
resizeMode: "contain",
height: 24,
width: 24,
tintColor: inputDisabled
? colors.ui.input.default.disabled.highlight
: colors.ui.input.default.active.highlight,
},
]}
/>
</TouchableOpacity>
<CustomTextInput
ref={ref}
multiline
disableFullscreenUI
onSelectionChange={onSelectionChange}
editable={!inputDisabled}
onChangeText={onChangeText}
style={[
styles.input,
{
borderWidth: text.length === 0 ? 0 : 1,
borderColor:
text.length === 0
? colors?.onBackground01
: "rgba(110, 112, 155, 0.75)",
},
]}
placeholder={getPlaceholder()}
text={text}
>
{mentionManager.textToMentionedComponents(text, mentionedUsers)}
</CustomTextInput>
{Boolean(text.trim()) && (
<TouchableOpacity
onPress={sendUserMessage}
disabled={inputDisabled}
>
<Icon
color={
inputDisabled
? colors.ui.input.default.disabled.highlight
: colors.ui.input.default.active.highlight
}
icon={"send"}
size={24}
containerStyle={styles.iconSend}
/>
</TouchableOpacity>
)}
</View>
);
});
Here’s our CustomTextInput component:
const CustomTextInput = forwardRef(function CustomTextInput(
{
children,
style,
variant = "default",
editable = true,
text,
...props
},
ref
) {
const { typography, colors, palette } = useUIKitTheme();
const variantStyle = colors["ui"]["input"][variant];
const inputStyle = editable
? variantStyle.active
: variantStyle.disabled;
const underlineStyle = variant === "underline" && {
borderBottomWidth: 2,
borderBottomColor: inputStyle.highlight,
};
return (
<TextInput
ref={ref}
editable={editable}
selectionColor={palette?.primary500}
placeholderTextColor={inputStyle.placeholder}
style={[
typography.body3,
{ paddingVertical: 8, paddingHorizontal: 16 },
{
color: inputStyle.text,
backgroundColor:
text.length === 0 ? "#E1E1F5" : inputStyle.background,
},
underlineStyle,
style,
]}
{...props}
>
{children}
</TextInput>
);
});
What’s the reason behind this weird behaviour? We are following the same steps for the functionality as used in default module input component.
Here’s a video depicting it more clearly, what our issue issue.
https://drive.google.com/file/d/1-4VXFY-cjJvX9_gaK0LLGgc02rHu7bfW/view?usp=sharing