Skip to content

Commit

Permalink
feat(UIKIT-4240): implement basic quote reply logic (#103)
Browse files Browse the repository at this point in the history
* chore: implement basic reply logic
* chore: implement scroll to message logic
* chore: optional mark for open channel
* chore: added disabled mark to delete message action
  • Loading branch information
bang9 committed Jul 21, 2023
1 parent ec847cc commit b4add0e
Show file tree
Hide file tree
Showing 11 changed files with 302 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,13 @@ function isNotEmpty(arr?: unknown[]): arr is unknown[] {
return arr.length !== 0;
}

function shouldUseSearchLimit(startingPoint: number) {
return startingPoint < Date.now();
}

export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (sdk, channel, userId, options) => {
const initialStartingPoint = options?.startingPoint ?? Number.MAX_SAFE_INTEGER;
const initialLimit = typeof options?.startingPoint === 'number' ? MESSAGE_LIMIT.SEARCH : MESSAGE_LIMIT.DEFAULT;
const initialLimit = shouldUseSearchLimit(initialStartingPoint) ? MESSAGE_LIMIT.SEARCH : MESSAGE_LIMIT.DEFAULT;

const forceUpdate = useForceUpdate();
const collectionRef = useRef<SendbirdMessageCollection>();
Expand Down Expand Up @@ -347,9 +351,10 @@ export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (s
});
const resetWithStartingPoint: ReturnType<UseGroupChannelMessages>['resetWithStartingPoint'] = useFreshCallback(
(startingPoint, callback) => {
const limit = shouldUseSearchLimit(startingPoint) ? MESSAGE_LIMIT.SEARCH : MESSAGE_LIMIT.DEFAULT;
updateLoading(true);
updateMessages([], true, sdk.currentUser.userId);
init(startingPoint, MESSAGE_LIMIT.DEFAULT, () => {
init(startingPoint, limit, () => {
updateLoading(false);
callback?.();
});
Expand Down
151 changes: 104 additions & 47 deletions packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,20 @@ import {
} from 'react-native';

import { MentionType } from '@sendbird/chat/message';
import type { BottomSheetItem } from '@sendbird/uikit-react-native-foundation';
import {
Icon,
Image,
PressBox,
Text,
TextInput,
createStyleSheet,
useAlert,
useBottomSheet,
useToast,
useUIKitTheme,
} from '@sendbird/uikit-react-native-foundation';
import type { BottomSheetItem } from '@sendbird/uikit-react-native-foundation';
import { SendbirdChannel, isImage, shouldCompressImage, useIIFE } from '@sendbird/uikit-utils';
import { Logger, SendbirdChannel, isImage, shouldCompressImage, useIIFE } from '@sendbird/uikit-utils';

import { useLocalization, usePlatformService, useSendbirdChat } from '../../hooks/useContext';
import SBUError from '../../libs/SBUError';
Expand Down Expand Up @@ -48,6 +51,8 @@ const SendInput = forwardRef<RNTextInput, SendInputProps>(function SendInput(
inputFrozen,
inputMuted,
channel,
messageToReply,
setMessageToReply,
},
ref,
) {
Expand All @@ -57,30 +62,55 @@ const SendInput = forwardRef<RNTextInput, SendInputProps>(function SendInput(
const { openSheet } = useBottomSheet();
const toast = useToast();

const onFailureToSend = () => toast.show(STRINGS.TOAST.SEND_MSG_ERROR, 'error');
const messageReplyParams = useIIFE(() => {
const { groupChannel } = sbOptions.uikit;
if (!channel.isGroupChannel() || groupChannel.channel.replyType === 'none' || !messageToReply) return {};
return {
parentMessageId: messageToReply.messageId,
isReplyToChannel: true,
};
});

const sendUserMessage = () => {
const mentionType = MentionType.USERS;
const mentionedUserIds = mentionedUsers.map((it) => it.user.userId);
const mentionedMessageTemplate = mentionManager.textToMentionedMessageTemplate(
text,
mentionedUsers,
sbOptions.uikit.groupChannel.channel.enableMention,
);
const messageMentionParams = useIIFE(() => {
const { groupChannel } = sbOptions.uikit;
if (!channel.isGroupChannel() || !groupChannel.channel.enableMention) return {};
return {
mentionType: MentionType.USERS,
mentionedUserIds: mentionedUsers.map((it) => it.user.userId),
mentionedMessageTemplate: mentionManager.textToMentionedMessageTemplate(
text,
mentionedUsers,
groupChannel.channel.enableMention,
),
};
});

const onFailureToSend = (error: Error) => {
toast.show(STRINGS.TOAST.SEND_MSG_ERROR, 'error');
Logger.error(STRINGS.TOAST.SEND_MSG_ERROR, error);
};

const sendUserMessage = () => {
onPressSendUserMessage({
message: text,
mentionType,
mentionedUserIds,
mentionedMessageTemplate,
...messageMentionParams,
...messageReplyParams,
}).catch(onFailureToSend);

onChangeText('');
setMessageToReply?.();
};

const sheetItems = useChannelInputItems(channel, (file) => {
onPressSendFileMessage({ file }).catch(onFailureToSend);
});
const sendFileMessage = (file: FileType) => {
onPressSendFileMessage({
file,
...messageReplyParams,
}).catch(onFailureToSend);

setMessageToReply?.();
};

const sheetItems = useChannelInputItems(channel, sendFileMessage);
const onPressAttachment = () => openSheet({ sheetItems });

const getPlaceholder = () => {
Expand All @@ -92,37 +122,64 @@ const SendInput = forwardRef<RNTextInput, SendInputProps>(function SendInput(
};

return (
<View style={styles.sendInputContainer}>
{AttachmentsButton && <AttachmentsButton onPress={onPressAttachment} disabled={inputDisabled} />}
<TextInput
ref={ref}
multiline
disableFullscreenUI
onSelectionChange={onSelectionChange}
editable={!inputDisabled}
onChangeText={onChangeText}
style={styles.input}
placeholder={getPlaceholder()}
>
{mentionManager.textToMentionedComponents(
text,
mentionedUsers,
sbOptions.uikit.groupChannel.channel.enableMention,
)}
</TextInput>

{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>
{/** TODO: Reply message component */}
{messageToReply && (
<View
style={{
flexDirection: 'row',
paddingLeft: 18,
paddingRight: 16,
paddingVertical: 12,
alignItems: 'center',
borderTopWidth: 1,
borderColor: colors.onBackground04,
}}
>
<View style={{ borderWidth: 1, flex: 1, height: 32 }}>
{messageToReply.isFileMessage() ? (
<Image style={{ width: 30, height: 30 }} source={{ uri: messageToReply.url }} />
) : (
<Text>{messageToReply.message}</Text>
)}
</View>
<PressBox onPress={() => setMessageToReply?.(undefined)} style={{ borderWidth: 1, marginLeft: 16 }}>
<Icon icon={'close'} size={24} color={colors.onBackground01} />
</PressBox>
</View>
)}
<View style={styles.sendInputContainer}>
{AttachmentsButton && <AttachmentsButton onPress={onPressAttachment} disabled={inputDisabled} />}
<TextInput
ref={ref}
multiline
disableFullscreenUI
onSelectionChange={onSelectionChange}
editable={!inputDisabled}
onChangeText={onChangeText}
style={styles.input}
placeholder={getPlaceholder()}
>
{mentionManager.textToMentionedComponents(
text,
mentionedUsers,
sbOptions.uikit.groupChannel.channel.enableMention,
)}
</TextInput>

{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>
</View>
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export type ChannelInputProps = {
messageToEdit: undefined | SendbirdUserMessage | SendbirdFileMessage;
setMessageToEdit: (message?: undefined | SendbirdUserMessage | SendbirdFileMessage) => void;

// reply - only available on group channel
messageToReply?: undefined | SendbirdUserMessage | SendbirdFileMessage;
setMessageToReply?: (message?: undefined | SendbirdUserMessage | SendbirdFileMessage) => void;

// mention
SuggestedMentionList?: (props: SuggestedMentionListProps) => JSX.Element | null;

Expand Down Expand Up @@ -83,9 +87,8 @@ const ChannelInput = (props: ChannelInputProps) => {
messageToEdit,
});
const inputMode = useIIFE(() => {
if (!messageToEdit) return 'send';
if (messageToEdit.isFileMessage()) return 'send';
return 'edit';
if (messageToEdit && !messageToEdit.isFileMessage()) return 'edit';
else return 'send';
});

const mentionAvailable =
Expand Down Expand Up @@ -138,8 +141,8 @@ const ChannelInput = (props: ChannelInputProps) => {
onChangeText={onChangeText}
autoFocus={AUTO_FOCUS}
onSelectionChange={onSelectionChange}
messageToEdit={messageToEdit}
mentionedUsers={mentionedUsers}
messageToEdit={messageToEdit}
setMessageToEdit={setMessageToEdit}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ export type ChannelMessageListProps<T extends SendbirdGroupChannel | SendbirdOpe
onPressScrollToBottomButton: (animated?: boolean) => void;

onEditMessage: (message: HandleableMessage) => void;
onReplyMessage?: (message: HandleableMessage) => void; // only available on group channel
onDeleteMessage: (message: HandleableMessage) => Promise<void>;
onResendFailedMessage: (failedMessage: HandleableMessage) => Promise<void>;
onPressParentMessage?: (parentMessage: SendbirdMessage) => void;
onPressMediaMessage?: (message: SendbirdFileMessage, deleteMessage: () => Promise<void>, uri: string) => void;

renderMessage: (props: {
Expand All @@ -66,6 +68,7 @@ export type ChannelMessageListProps<T extends SendbirdGroupChannel | SendbirdOpe
nextMessage?: SendbirdMessage;
onPress?: () => void;
onLongPress?: () => void;
onPressParentMessage?: ChannelMessageListProps<T>['onPressParentMessage'];
onShowUserProfile?: UserProfileContextType['show'];
channel: T;
currentUserId?: ChannelMessageListProps<T>['currentUserId'];
Expand All @@ -91,9 +94,11 @@ const ChannelMessageList = <T extends SendbirdGroupChannel | SendbirdOpenChannel
hasNext,
channel,
onEditMessage,
onReplyMessage,
onDeleteMessage,
onResendFailedMessage,
onPressMediaMessage,
onPressParentMessage,
currentUserId,
renderNewMessagesButton,
renderScrollToBottomButton,
Expand All @@ -119,6 +124,7 @@ const ChannelMessageList = <T extends SendbirdGroupChannel | SendbirdOpenChannel
channel,
currentUserId,
onEditMessage,
onReplyMessage,
onDeleteMessage,
onResendFailedMessage,
onPressMediaMessage,
Expand All @@ -134,6 +140,7 @@ const ChannelMessageList = <T extends SendbirdGroupChannel | SendbirdOpenChannel
nextMessage: messages[index - 1],
onPress,
onLongPress,
onPressParentMessage,
onShowUserProfile: show,
enableMessageGrouping,
channel,
Expand Down Expand Up @@ -188,11 +195,18 @@ const useGetMessagePressActions = <T extends SendbirdGroupChannel | SendbirdOpen
currentUserId,
onResendFailedMessage,
onEditMessage,
onReplyMessage,
onDeleteMessage,
onPressMediaMessage,
}: Pick<
ChannelMessageListProps<T>,
'channel' | 'currentUserId' | 'onEditMessage' | 'onDeleteMessage' | 'onResendFailedMessage' | 'onPressMediaMessage'
| 'channel'
| 'currentUserId'
| 'onEditMessage'
| 'onReplyMessage'
| 'onDeleteMessage'
| 'onResendFailedMessage'
| 'onPressMediaMessage'
>) => {
const { colors } = useUIKitTheme();
const { STRINGS } = useLocalization();
Expand Down Expand Up @@ -267,12 +281,26 @@ const useGetMessagePressActions = <T extends SendbirdGroupChannel | SendbirdOpen
onPress: () => onEditMessage(msg),
},
{
// TODO: disabled if message has a parentMessageId
// disabled: Boolean(msg.parentMessageId),
icon: 'delete',
title: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE,
onPress: () => confirmDelete(msg),
},
);
}

if (channel.isGroupChannel() && sbOptions.uikit.groupChannel.channel.replyType === 'quote_reply') {
const disabled = Boolean(msg.parentMessageId);
sheetItems.push({
// TODO: Implement disabled to bottom sheet
// disabled,
icon: 'reply',
// TODO: Add reply label
title: disabled ? 'Reply(disabled)' : 'Reply', //'STRINGS.LABELS.CHANNEL_MESSAGE_REPLY',
onPress: () => onReplyMessage?.(msg),
});
}
}
}

Expand Down Expand Up @@ -301,11 +329,25 @@ const useGetMessagePressActions = <T extends SendbirdGroupChannel | SendbirdOpen
if (!channel.isEphemeral) {
if (isMyMessage(msg, currentUserId) && msg.sendingStatus === 'succeeded') {
sheetItems.push({
// TODO: disabled if message has a parentMessageId
// disabled: Boolean(msg.parentMessageId),
icon: 'delete',
title: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE,
onPress: () => confirmDelete(msg),
});
}

if (channel.isGroupChannel() && sbOptions.uikit.groupChannel.channel.replyType === 'quote_reply') {
const disabled = Boolean(msg.parentMessageId);
sheetItems.push({
// TODO: Implement disabled to bottom sheet
// disabled,
icon: 'reply',
// TODO: Add reply label
title: disabled ? 'Reply(disabled)' : 'Reply', //'STRINGS.LABELS.CHANNEL_MESSAGE_REPLY',
onPress: () => onReplyMessage?.(msg),
});
}
}

const fileType = getFileType(msg.type || getFileExtension(msg.name));
Expand Down
Loading

0 comments on commit b4add0e

Please sign in to comment.