Skip to content

Commit d2aa9ba

Browse files
committed
feat: add support for draft message
1 parent 73821ca commit d2aa9ba

File tree

6 files changed

+118
-37
lines changed

6 files changed

+118
-37
lines changed

package/src/components/ChannelPreview/ChannelPreviewMessage.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const ChannelPreviewMessage = (props: ChannelPreviewMessageProps) => {
2626
const {
2727
theme: {
2828
channelPreview: { message },
29-
colors: { grey },
29+
colors: { accent_blue, grey },
3030
},
3131
} = useTheme();
3232

@@ -37,7 +37,11 @@ export const ChannelPreviewMessage = (props: ChannelPreviewMessageProps) => {
3737
preview.text && (
3838
<Text
3939
key={`${preview.text}_${index}`}
40-
style={[{ color: grey }, preview.bold ? styles.bold : {}, message]}
40+
style={[
41+
{ color: preview?.draft ? accent_blue : grey },
42+
preview.bold ? styles.bold : {},
43+
message,
44+
]}
4145
>
4246
{preview.text}
4347
</Text>

package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ export const useChannelPreviewData = (
2323
const { forceUpdate: contextForceUpdate } = useChannelsContext();
2424
const channelListForceUpdate = forceUpdateOverride ?? contextForceUpdate;
2525

26+
useEffect(() => {
27+
const unsubscribe = channel.messageComposer.registerSubscriptions();
28+
return () => {
29+
unsubscribe();
30+
};
31+
}, [channel.messageComposer]);
32+
2633
const channelLastMessage = channel.lastMessage();
2734
const channelLastMessageString = `${channelLastMessage?.id}${channelLastMessage?.updated_at}`;
2835

package/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import { useMemo } from 'react';
22

33
import { TFunction } from 'i18next';
44
import type {
5+
AttachmentManagerState,
56
Channel,
67
ChannelState,
78
MessageResponse,
89
PollState,
910
PollVote,
1011
StreamChat,
12+
TextComposerState,
1113
UserResponse,
1214
} from 'stream-chat';
1315

@@ -26,6 +28,7 @@ export type LatestMessagePreview = {
2628
previews: {
2729
bold: boolean;
2830
text: string;
31+
draft?: boolean;
2932
}[];
3033
status: number;
3134
created_at?: string | Date;
@@ -82,6 +85,8 @@ const getMentionUsers = (mentionedUser: UserResponse[] | undefined) => {
8285
const getLatestMessageDisplayText = (
8386
channel: Channel,
8487
client: StreamChat,
88+
draftText: string | undefined,
89+
draftAttachments: boolean | undefined,
8590
message: LatestMessage | undefined,
8691
t: (key: string) => string,
8792
pollState: LatestMessagePreviewSelectorReturnType | undefined,
@@ -101,6 +106,18 @@ const getLatestMessageDisplayText = (
101106
? `${messageSender === t('You') ? '' : '@'}${messageSender}: `
102107
: '';
103108
const boldOwner = messageSenderText.includes('@');
109+
if (draftText) {
110+
return [
111+
{ bold: true, draft: true, text: 'Draft:' },
112+
{ bold: false, text: draftText },
113+
];
114+
}
115+
if (draftAttachments) {
116+
return [
117+
{ bold: true, draft: true, text: 'Draft:' },
118+
{ bold: false, text: t('🏙 Attachment...') },
119+
];
120+
}
104121
if (message.text) {
105122
// rough guess optimization to limit string preview to max 100 characters
106123
const shortenedText = message.text.substring(0, 100).replace(/\n/g, ' ');
@@ -201,12 +218,15 @@ const getLatestMessageReadStatus = (
201218
const getLatestMessagePreview = (params: {
202219
channel: Channel;
203220
client: StreamChat;
221+
draftText?: string;
222+
draftAttachments?: boolean;
204223
pollState: LatestMessagePreviewSelectorReturnType | undefined;
205224
readEvents: boolean;
206225
t: TFunction;
207226
lastMessage?: ReturnType<ChannelState['formatMessage']> | MessageResponse;
208227
}) => {
209-
const { channel, client, lastMessage, pollState, readEvents, t } = params;
228+
const { channel, client, draftText, draftAttachments, lastMessage, pollState, readEvents, t } =
229+
params;
210230

211231
const messages = channel.state.messages;
212232

@@ -231,11 +251,27 @@ const getLatestMessagePreview = (params: {
231251
return {
232252
created_at: message?.created_at,
233253
messageObject: message,
234-
previews: getLatestMessageDisplayText(channel, client, message, t, pollState),
254+
previews: getLatestMessageDisplayText(
255+
channel,
256+
client,
257+
draftText,
258+
draftAttachments,
259+
message,
260+
t,
261+
pollState,
262+
),
235263
status: getLatestMessageReadStatus(channel, client, message, readEvents),
236264
};
237265
};
238266

267+
const textComposerStateSelector = (state: TextComposerState) => ({
268+
text: state.text,
269+
});
270+
271+
const stateSelector = (state: AttachmentManagerState) => ({
272+
attachments: state.attachments,
273+
});
274+
239275
/**
240276
* Hook to set the display preview for latest message on channel.
241277
*
@@ -251,6 +287,18 @@ export const useLatestMessagePreview = (
251287
const { client } = useChatContext();
252288
const { t } = useTranslationContext();
253289

290+
const { text: draftText } = useStateStore(
291+
channel.messageComposer.textComposer.state,
292+
textComposerStateSelector,
293+
);
294+
295+
const { attachments } = useStateStore(
296+
channel.messageComposer.attachmentManager.state,
297+
stateSelector,
298+
);
299+
300+
const draftAttachments = attachments.length > 0;
301+
254302
const channelConfigExists = typeof channel?.getConfig === 'function';
255303

256304
const translatedLastMessage = useTranslatedMessage(lastMessage);
@@ -282,6 +330,8 @@ export const useLatestMessagePreview = (
282330
return getLatestMessagePreview({
283331
channel,
284332
client,
333+
draftAttachments,
334+
draftText,
285335
lastMessage: translatedLastMessage,
286336
pollState,
287337
readEvents,
@@ -290,6 +340,8 @@ export const useLatestMessagePreview = (
290340
// eslint-disable-next-line react-hooks/exhaustive-deps
291341
}, [
292342
channelLastMessageString,
343+
draftText,
344+
draftAttachments,
293345
forceUpdate,
294346
readEvents,
295347
readStatus,

package/src/components/MessageInput/MessageInput.tsx

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import Animated, {
1919
import {
2020
FileReference,
2121
isLocalImageAttachment,
22+
MessageComposerConfig,
2223
type MessageComposerState,
2324
type TextComposerState,
2425
type UserResponse,
@@ -164,6 +165,10 @@ const messageComposerStateStoreSelector = (state: MessageComposerState) => ({
164165
quotedMessage: state.quotedMessage,
165166
});
166167

168+
const configStateSelector = (state: MessageComposerConfig) => ({
169+
draftsEnabled: state.drafts.enabled,
170+
});
171+
167172
const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
168173
const {
169174
additionalTextInputProps,
@@ -215,13 +220,12 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
215220

216221
const messageComposer = useMessageComposer();
217222
const { attachmentManager, textComposer } = messageComposer;
218-
const { command, mentionedUsers, text } = useStateStore(
219-
textComposer.state,
220-
textComposerStateSelector,
221-
);
223+
const { command, text } = useStateStore(textComposer.state, textComposerStateSelector);
224+
222225
const { quotedMessage } = useStateStore(messageComposer.state, messageComposerStateStoreSelector);
223-
const { attachments, availableUploadSlots } = useAttachmentManagerState();
226+
const { attachments } = useAttachmentManagerState();
224227
const hasSendableData = useMessageComposerHasSendableData();
228+
const { draftsEnabled } = useStateStore(messageComposer.configState, configStateSelector);
225229

226230
const imageUploads = attachments.filter((attachment) => isLocalImageAttachment(attachment));
227231
const fileUploads = attachments.filter((attachment) => !isLocalImageAttachment(attachment));
@@ -303,31 +307,36 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
303307
// eslint-disable-next-line react-hooks/exhaustive-deps
304308
}, []);
305309

306-
const editingExists = !!editing;
307-
310+
// Effect to ensure we focus the input box when editing a message.
308311
useEffect(() => {
309-
if (editing && inputBoxRef.current) {
312+
const { editedMessage } = messageComposer;
313+
if (editedMessage && inputBoxRef.current) {
310314
inputBoxRef.current.focus();
311315
}
316+
}, [messageComposer, inputBoxRef]);
312317

313-
/**
314-
* Make sure to test `initialValue` functionality, if you are modifying following condition.
315-
*
316-
* We have the following condition, to make sure - when user comes out of "editing message" state,
317-
* we wipe out all the state around message input such as text, mentioned users, image uploads etc.
318-
* But it also means, this condition will be fired up on first render, which may result in clearing
319-
* the initial value set on input box, through the prop - `initialValue`.
320-
* This prop generally gets used for the case of draft message functionality.
321-
*/
322-
if (
323-
!editing &&
324-
(command || attachments.length > 0 || mentionedUsers.length > 0 || availableUploadSlots) &&
325-
resetInput
326-
) {
327-
resetInput();
328-
}
329-
// eslint-disable-next-line react-hooks/exhaustive-deps
330-
}, [editingExists]);
318+
// Effect to create draft whenever we un-mount the component.
319+
useEffect(() => {
320+
return () => {
321+
if (draftsEnabled) {
322+
messageComposer.createDraft();
323+
}
324+
};
325+
}, [draftsEnabled, messageComposer, resetInput]);
326+
327+
/**
328+
* Effect to get the draft data for legacy thread composer and set it to message composer.
329+
* TODO: This can be removed once we remove legacy thread composer.
330+
*/
331+
useEffect(() => {
332+
const threadId = messageComposer.threadId;
333+
if (!threadId || !messageComposer.channel || !messageComposer.compositionIsEmpty) return;
334+
messageComposer.channel.getDraft({ parent_id: threadId }).then(({ draft }) => {
335+
if (draft) {
336+
messageComposer.initState({ composition: draft });
337+
}
338+
});
339+
}, [messageComposer]);
331340

332341
const uploadImagesHandler = async () => {
333342
const imageToUpload = selectedImages.find((selectedImage) => {

package/src/components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback } from 'react';
1+
import React, { useCallback, useState } from 'react';
22

33
import { Image, StyleSheet, View } from 'react-native';
44

@@ -23,11 +23,11 @@ export const ImageAttachmentUploadPreview = ({
2323
handleRetry,
2424
removeAttachments,
2525
}: ImageAttachmentUploadPreviewProps) => {
26+
const [loading, setLoading] = useState(false);
2627
const { enableOfflineSupport } = useChatContext();
27-
const indicatorType = getIndicatorTypeForFileState(
28-
attachment.localMetadata.uploadState,
29-
enableOfflineSupport,
30-
);
28+
const indicatorType = loading
29+
? ProgressIndicatorTypes.IN_PROGRESS
30+
: getIndicatorTypeForFileState(attachment.localMetadata.uploadState, enableOfflineSupport);
3131

3232
const {
3333
theme: {
@@ -45,6 +45,14 @@ export const ImageAttachmentUploadPreview = ({
4545
removeAttachments([attachment.localMetadata.id]);
4646
}, [attachment, removeAttachments]);
4747

48+
const onLoadEndHandler = useCallback(() => {
49+
setLoading(false);
50+
}, []);
51+
52+
const onLoadStartHandler = useCallback(() => {
53+
setLoading(true);
54+
}, []);
55+
4856
return (
4957
<View style={[styles.itemContainer, itemContainer]}>
5058
<AttachmentUploadProgressIndicator
@@ -53,6 +61,8 @@ export const ImageAttachmentUploadPreview = ({
5361
type={indicatorType}
5462
>
5563
<Image
64+
onLoadEnd={onLoadEndHandler}
65+
onLoadStart={onLoadStartHandler}
5666
resizeMode='cover'
5767
source={{ uri: attachment.localMetadata.previewUri ?? attachment.image_url }}
5868
style={[styles.upload, upload]}

package/src/contexts/messageInputContext/hooks/useMessageComposer.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,7 @@ export const useMessageComposer = () => {
6565
} else {
6666
return channel.messageComposer;
6767
}
68-
// eslint-disable-next-line react-hooks/exhaustive-deps
69-
}, [cachedEditedMessage, cachedParentMessage, channel, threadInstance]);
68+
}, [cachedEditedMessage, cachedParentMessage, channel, client, editedMessage, threadInstance]);
7069

7170
if (
7271
(['legacy_thread', 'message'] as MessageComposer['contextType'][]).includes(

0 commit comments

Comments
 (0)