Skip to content

Commit e0382af

Browse files
HoonBaekchohongm
andauthored
feature: Add an interface for confirming file download (#1020)
[CLNP-2565](https://sendbird.atlassian.net/browse/CLNP-2565) ### Original Reason * There was no interface available to decide whether to proceed with the file download or not ### ChangeLog & Feature * Added `onBeforeDownloadFileMessage` to the `<GroupChannel />` and `<Thread />` modules * Added `onDownloadClick` to the `FileViewer`, `FileViewerView`, `MobileBottomSheet`, `MobileContextMenu`, and `MobileMenu` ### How to use ```tsx const ONE_MB = 1024 * 1024; /** * Use this list to check if it's displayed as a ThumbnailMessage. * (https://github.com/sendbird/sendbird-uikit-react/blob/main/src/utils/index.ts) */ const ThumbnailMessageTypes = [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp', // not supported in IE 'video/mpeg', 'video/ogg', 'video/webm', 'video/mp4', ]; <GroupChannel // or Thread onBeforeDownloadFileMessage={async ({ message, index = null }) => { if (message.isFileMessage()) { const confirmed = window.confirm(`The file size is ${(message.size / ONE_MB).toFixed(2)}MB. Would you like to continue downloading?`); return confirmed; } if (message.isMultipleFilesMessage()) { const confirmed = window.confirm(`The file size is ${(message.fileInfoList[index].fileSize / ONE_MB).toFixed(2)}MB. Would you like to continue downloading?`); return confirmed; } return true; }} /> ``` --------- Co-authored-by: Liam Hongman Cho <[email protected]>
1 parent d51fd2d commit e0382af

File tree

21 files changed

+241
-17
lines changed

21 files changed

+241
-17
lines changed

src/modules/GroupChannel/components/FileViewer/FileViewerView.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import './index.scss';
2-
import React from 'react';
2+
import React, { MouseEvent } from 'react';
33
import { createPortal } from 'react-dom';
44

55
import { FileViewerProps } from '.';
@@ -15,12 +15,14 @@ import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext';
1515
type DeleteMessageTypeLegacy = (message: CoreMessageType) => Promise<void>;
1616
export interface FileViewerViewProps extends FileViewerProps {
1717
deleteMessage: ((message: SendableMessageType) => Promise<void>) | DeleteMessageTypeLegacy;
18+
onDownloadClick?: (e: MouseEvent) => Promise<void>;
1819
}
1920

2021
export const FileViewerView = ({
2122
message,
2223
onCancel,
2324
deleteMessage,
25+
onDownloadClick,
2426
}: FileViewerViewProps) => {
2527
const { sender, type, url, name = '', threadInfo } = message;
2628
const { profileUrl, nickname, userId } = sender;
@@ -38,6 +40,7 @@ export const FileViewerView = ({
3840
onDelete={() => deleteMessage(message).then(() => onCancel())}
3941
isByMe={config.userId === userId}
4042
disableDelete={threadInfo?.replyCount > 0}
43+
onDownloadClick={onDownloadClick}
4144
/>,
4245
document.getElementById(MODAL_ROOT),
4346
);
@@ -56,6 +59,7 @@ export interface FileViewerUIProps {
5659
onCancel: () => void;
5760
onDelete: () => void;
5861
disableDelete: boolean;
62+
onDownloadClick?: (e: MouseEvent) => Promise<void>;
5963
}
6064

6165
export const FileViewerComponent = ({
@@ -71,6 +75,7 @@ export const FileViewerComponent = ({
7175
onCancel,
7276
onDelete,
7377
disableDelete,
78+
onDownloadClick,
7479
}: FileViewerUIProps) => (
7580
<div className="sendbird-fileviewer">
7681
<div className="sendbird-fileviewer__header">
@@ -88,7 +93,13 @@ export const FileViewerComponent = ({
8893
<div className="sendbird-fileviewer__header__right">
8994
{isSupportedFileView(type) && (
9095
<div className="sendbird-fileviewer__header__right__actions">
91-
<a className="sendbird-fileviewer__header__right__actions__download" rel="noopener noreferrer" href={url} target="_blank">
96+
<a
97+
className="sendbird-fileviewer__header__right__actions__download"
98+
rel="noopener noreferrer"
99+
href={url}
100+
target="_blank"
101+
onClick={onDownloadClick}
102+
>
92103
<Icon type={IconTypes.DOWNLOAD} fillColor={IconColors.ON_BACKGROUND_1} height="24px" width="24px" />
93104
</a>
94105
{onDelete && isByMe && (

src/modules/GroupChannel/components/FileViewer/index.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,37 @@ import type { FileMessage } from '@sendbird/chat/message';
33

44
import { FileViewerView } from './FileViewerView';
55
import { useGroupChannelContext } from '../../context/GroupChannelProvider';
6+
import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext';
67

78
export interface FileViewerProps {
89
onCancel: () => void;
910
message: FileMessage;
1011
}
1112

1213
export const FileViewer = (props: FileViewerProps) => {
13-
const { deleteMessage } = useGroupChannelContext();
14-
return <FileViewerView {...props} deleteMessage={deleteMessage} />;
14+
const { deleteMessage, onBeforeDownloadFileMessage } = useGroupChannelContext();
15+
const { config } = useSendbirdStateContext();
16+
const { logger } = config;
17+
return (
18+
<FileViewerView
19+
{...props}
20+
deleteMessage={deleteMessage}
21+
onDownloadClick={async (e) => {
22+
if (!onBeforeDownloadFileMessage) {
23+
return null;
24+
}
25+
try {
26+
const allowDownload = await onBeforeDownloadFileMessage({ message: props.message });
27+
if (!allowDownload) {
28+
e.preventDefault();
29+
logger.info?.('FileViewer: Not allowed to download.');
30+
}
31+
} catch (err) {
32+
logger.error?.('FileViewer: Error occurred while determining download continuation:', err);
33+
}
34+
}}
35+
/>
36+
);
1537
};
1638

1739
export default FileViewer;

src/modules/GroupChannel/components/Message/MessageView.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ import MessageContent, { MessageContentProps } from '../../../../ui/MessageConte
2020

2121
import SuggestedReplies, { SuggestedRepliesProps } from '../SuggestedReplies';
2222
import SuggestedMentionListView from '../SuggestedMentionList/SuggestedMentionListView';
23+
import type { OnBeforeDownloadFileMessageType } from '../../context/GroupChannelProvider';
2324

2425
export interface MessageProps {
2526
message: EveryMessage;
2627
hasSeparator?: boolean;
2728
chainTop?: boolean;
2829
chainBottom?: boolean;
2930
handleScroll?: (isBottomMessageAffected?: boolean) => void;
30-
3131
/**
3232
* Customizes all child components of the message.
3333
* */
@@ -80,6 +80,11 @@ export interface MessageViewProps extends MessageProps {
8080

8181
renderFileViewer: (props: { message: FileMessage; onCancel: () => void }) => React.ReactElement;
8282
renderRemoveMessageModal?: (props: { message: EveryMessage; onCancel: () => void }) => React.ReactElement;
83+
/**
84+
* You can't use this prop in the Channel component (legacy).
85+
* Accepting this prop only for the GroupChannel.
86+
*/
87+
onBeforeDownloadFileMessage?: OnBeforeDownloadFileMessageType;
8388

8489
animatedMessageId: number;
8590
setAnimatedMessageId: React.Dispatch<React.SetStateAction<number>>;
@@ -127,6 +132,7 @@ const MessageView = (props: MessageViewProps) => {
127132
setQuoteMessage,
128133
onQuoteMessageClick,
129134
onReplyInThreadClick,
135+
onBeforeDownloadFileMessage,
130136

131137
sendUserMessage,
132138
updateUserMessage,
@@ -261,6 +267,7 @@ const MessageView = (props: MessageViewProps) => {
261267
onReplyInThread: onReplyInThreadClick,
262268
onQuoteMessageClick: onQuoteMessageClick,
263269
onMessageHeightChange: handleScroll,
270+
onBeforeDownloadFileMessage,
264271
})}
265272
{ /* Suggested Replies */ }
266273
{

src/modules/GroupChannel/components/Message/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const Message = (props: MessageProps): React.ReactElement => {
2727
onQuoteMessageClick,
2828
onReplyInThreadClick,
2929
onMessageAnimated,
30+
onBeforeDownloadFileMessage,
3031
messages,
3132
updateUserMessage,
3233
sendUserMessage,
@@ -82,6 +83,7 @@ export const Message = (props: MessageProps): React.ReactElement => {
8283
renderFileViewer={(props) => <FileViewer {...props} />}
8384
renderRemoveMessageModal={(props) => <RemoveMessageModal {...props} />}
8485
usedInLegacy={false}
86+
onBeforeDownloadFileMessage={onBeforeDownloadFileMessage}
8587
/>
8688
);
8789
};

src/modules/GroupChannel/context/GroupChannelProvider.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
ReplyType as ChatReplyType,
77
UserMessageCreateParams,
88
UserMessageUpdateParams,
9+
type FileMessage,
10+
type MultipleFilesMessage,
911
} from '@sendbird/chat/message';
1012
import type { GroupChannel, MessageCollectionParams, MessageFilterParams } from '@sendbird/chat/groupChannel';
1113
import { MessageFilter } from '@sendbird/chat/groupChannel';
@@ -30,6 +32,7 @@ type OnBeforeHandler<T> = (params: T) => T | Promise<T>;
3032
type MessageListQueryParamsType = Omit<MessageCollectionParams, 'filter'> & MessageFilterParams;
3133
type MessageActions = ReturnType<typeof useMessageActions>;
3234
type MessageListDataSourceWithoutActions = Omit<ReturnType<typeof useGroupChannelMessages>, keyof MessageActions | `_dangerous_${string}`>;
35+
export type OnBeforeDownloadFileMessageType = (params: { message: FileMessage | MultipleFilesMessage, index?: number }) => Promise<boolean>;
3336

3437
interface ContextBaseType {
3538
// Required
@@ -61,6 +64,7 @@ interface ContextBaseType {
6164
onBeforeSendVoiceMessage?: OnBeforeHandler<FileMessageCreateParams>;
6265
onBeforeSendMultipleFilesMessage?: OnBeforeHandler<MultipleFilesMessageCreateParams>;
6366
onBeforeUpdateUserMessage?: OnBeforeHandler<UserMessageUpdateParams>;
67+
onBeforeDownloadFileMessage?: OnBeforeDownloadFileMessageType;
6468

6569
// Click
6670
onBackClick?(): void;
@@ -123,6 +127,7 @@ export const GroupChannelProvider = (props: GroupChannelProviderProps) => {
123127
onBeforeSendVoiceMessage,
124128
onBeforeSendMultipleFilesMessage,
125129
onBeforeUpdateUserMessage,
130+
onBeforeDownloadFileMessage,
126131
onMessageAnimated,
127132
onBackClick,
128133
onChatHeaderActionClick,
@@ -389,6 +394,7 @@ export const GroupChannelProvider = (props: GroupChannelProviderProps) => {
389394
onBeforeSendVoiceMessage,
390395
onBeforeSendMultipleFilesMessage,
391396
onBeforeUpdateUserMessage,
397+
onBeforeDownloadFileMessage,
392398
// ## Focusing
393399
onMessageAnimated,
394400
// ## Click

src/modules/GroupChannel/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from 'react';
33
import { GroupChannelProvider, GroupChannelProviderProps } from './context/GroupChannelProvider';
44
import GroupChannelUI, { GroupChannelUIProps } from './components/GroupChannelUI';
55

6-
export interface GroupChannelProps extends GroupChannelProviderProps, GroupChannelUIProps {}
6+
export interface GroupChannelProps extends GroupChannelProviderProps, GroupChannelUIProps { }
77
export const GroupChannel = (props: GroupChannelProps) => {
88
return (
99
<GroupChannelProvider {...props}>

src/modules/Thread/components/ParentMessageInfo/ParentMessageInfoItem.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,23 @@ import { useMediaQueryContext } from '../../../../lib/MediaQueryContext';
3636
import { useThreadMessageKindKeySelector } from '../../../Channel/context/hooks/useThreadMessageKindKeySelector';
3737
import { useFileInfoListWithUploaded } from '../../../Channel/context/hooks/useFileInfoListWithUploaded';
3838
import { Colors } from '../../../../utils/color';
39+
import type { OnBeforeDownloadFileMessageType } from '../../../GroupChannel/context/GroupChannelProvider';
3940

4041
export interface ParentMessageInfoItemProps {
4142
className?: string;
4243
message: SendableMessageType;
4344
showFileViewer?: (bool: boolean) => void;
45+
onBeforeDownloadFileMessage?: OnBeforeDownloadFileMessageType;
4446
}
4547

4648
export default function ParentMessageInfoItem({
4749
className,
4850
message,
4951
showFileViewer,
52+
onBeforeDownloadFileMessage = null,
5053
}: ParentMessageInfoItemProps): ReactElement {
5154
const { stores, config, eventHandlers } = useSendbirdStateContext?.() || {};
55+
const { logger } = config;
5256
const onPressUserProfileHandler = eventHandlers?.reaction?.onPressUserProfile;
5357
const {
5458
replyType,
@@ -93,6 +97,29 @@ export default function ParentMessageInfoItem({
9397
});
9498
}, [message?.updatedAt, (message as UserMessage)?.message]);
9599

100+
// Only for the FileMessageItemBody
101+
const downloadFileWithUrl = () => {
102+
if (message.messageType === 'file') {
103+
window.open((message as FileMessage)?.url);
104+
}
105+
};
106+
const handleOnClickTextButton = onBeforeDownloadFileMessage
107+
? async () => {
108+
if (message.messageType === 'file') {
109+
try {
110+
const allowDownload = await onBeforeDownloadFileMessage({ message: message as FileMessage });
111+
if (allowDownload) {
112+
downloadFileWithUrl();
113+
} else {
114+
logger?.info?.('ParentMessageInfoItem: Not allowed to download.');
115+
}
116+
} catch (err) {
117+
logger?.error?.('ParentMessageInfoItem: Error occurred while determining download continuation:', err);
118+
}
119+
}
120+
}
121+
: downloadFileWithUrl;
122+
96123
// Thumbnail mesage
97124
const [isImageRendered, setImageRendered] = useState(false);
98125
const thumbnailUrl: string = (message as FileMessage)?.thumbnails?.length > 0
@@ -174,6 +201,7 @@ export default function ParentMessageInfoItem({
174201
</div>
175202
)} */}
176203
{
204+
// Instead of the FileMessageItemBody component
177205
(getUIKitMessageType((message as FileMessage)) === getUIKitMessageTypes().FILE) && (
178206
<div className="sendbird-parent-message-info-item__file-message">
179207
<div className="sendbird-parent-message-info-item__file-message__file-icon">
@@ -193,7 +221,7 @@ export default function ParentMessageInfoItem({
193221
</div>
194222
<TextButton
195223
className="sendbird-parent-message-info-item__file-message__file-name"
196-
onClick={() => { window.open((message as FileMessage)?.url); }}
224+
onClick={handleOnClickTextButton}
197225
color={Colors.ONBACKGROUND_1}
198226
>
199227
<Label
@@ -216,6 +244,7 @@ export default function ParentMessageInfoItem({
216244
isReactionEnabled={isReactionEnabled}
217245
threadMessageKindKey={threadMessageKindKey}
218246
statefulFileInfoList={statefulFileInfoList}
247+
onBeforeDownloadFileMessage={onBeforeDownloadFileMessage}
219248
/>
220249
)
221250
}

src/modules/Thread/components/ParentMessageInfo/index.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export default function ParentMessageInfo({
5959
onHeaderActionClick,
6060
isMuted,
6161
isChannelFrozen,
62+
onBeforeDownloadFileMessage,
6263
} = useThreadContext();
6364
const { isMobile } = useMediaQueryContext();
6465

@@ -118,6 +119,21 @@ export default function ParentMessageInfo({
118119
}));
119120
}, [mentionedUserIds]);
120121

122+
const handleOnDownloadClick = async (e) => {
123+
if (!onBeforeDownloadFileMessage) {
124+
return null;
125+
}
126+
try {
127+
const allowDownload = await onBeforeDownloadFileMessage({ message: parentMessage as FileMessage });
128+
if (!allowDownload) {
129+
e.preventDefault();
130+
logger?.info?.('ParentMessageInfo: Not allowed to download.');
131+
}
132+
} catch (err) {
133+
logger?.error?.('ParentMessageInfo: Error occurred while determining download continuation:', err);
134+
}
135+
};
136+
121137
// User Profile
122138
const avatarRef = useRef(null);
123139
const { disableUserProfile, renderUserProfile } = useContext(UserProfileContext);
@@ -280,6 +296,7 @@ export default function ParentMessageInfo({
280296
<ParentMessageInfoItem
281297
message={parentMessage}
282298
showFileViewer={setShowFileViewer}
299+
onBeforeDownloadFileMessage={onBeforeDownloadFileMessage}
283300
/>
284301
</div>
285302
{/* context menu */}
@@ -329,6 +346,7 @@ export default function ParentMessageInfo({
329346
setShowFileViewer(false);
330347
});
331348
}}
349+
onDownloadClick={handleOnDownloadClick}
332350
/>
333351
)}
334352
{showMobileMenu && (
@@ -354,6 +372,7 @@ export default function ParentMessageInfo({
354372
showRemove={setShowRemove}
355373
toggleReaction={toggleReaction}
356374
isOpenedFromThread
375+
onDownloadClick={handleOnDownloadClick}
357376
/>
358377
)}
359378
</div>

src/modules/Thread/components/ThreadList/ThreadListItem.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useMemo, useState, useRef, useEffect, useLayoutEffect } from 'react';
22
import format from 'date-fns/format';
3-
import { FileMessage } from '@sendbird/chat/message';
3+
import type { FileMessage, MultipleFilesMessage } from '@sendbird/chat/message';
44

55
import { useLocalization } from '../../../../lib/LocalizationContext';
66
import DateSeparator from '../../../../ui/DateSeparator';
@@ -61,6 +61,7 @@ export default function ThreadListItem({
6161
deleteMessage,
6262
isMuted,
6363
isChannelFrozen,
64+
onBeforeDownloadFileMessage,
6465
} = threadContext;
6566
const openingMessage = threadContext?.message;
6667

@@ -258,13 +259,27 @@ export default function ThreadListItem({
258259
)}
259260
{showFileViewer && (
260261
<FileViewer
261-
message={message as FileMessage}
262+
message={message as FileMessage | MultipleFilesMessage}
262263
isByMe={message?.sender?.userId === userId}
263264
onClose={() => setShowFileViewer(false)}
264265
onDelete={() => {
265266
deleteMessage(message);
266267
setShowFileViewer(false);
267268
}}
269+
onDownloadClick={async (e) => {
270+
if (!onBeforeDownloadFileMessage) {
271+
return null;
272+
}
273+
try {
274+
const allowDownload = await onBeforeDownloadFileMessage({ message: message as FileMessage | MultipleFilesMessage });
275+
if (!allowDownload) {
276+
e.preventDefault();
277+
logger.info?.('ThreadListItem: Not allowed to download.');
278+
}
279+
} catch (err) {
280+
logger.error?.('ThreadListItem: Error occurred while determining download continuation:', err);
281+
}
282+
}}
268283
/>
269284
)}
270285
</div>

0 commit comments

Comments
 (0)