diff --git a/package.json b/package.json index ad0aac643..aee005eed 100644 --- a/package.json +++ b/package.json @@ -82,5 +82,8 @@ "workspaces": [ "packages/*" ], - "packageManager": "yarn@4.2.2" + "packageManager": "yarn@4.2.2", + "resolutions": { + "@sendbird/chat": "file:./sendbird-chat-4.14.2.tgz" + } } diff --git a/packages/uikit b/packages/uikit index acc071328..68281969a 160000 --- a/packages/uikit +++ b/packages/uikit @@ -1 +1 @@ -Subproject commit acc071328e687dc2fa69c5061699f3d3548fafcf +Subproject commit 68281969aa4df9bfb0d574184898e920f72caf88 diff --git a/sendbird-chat-4.14.2.tgz b/sendbird-chat-4.14.2.tgz new file mode 100644 index 000000000..490543793 Binary files /dev/null and b/sendbird-chat-4.14.2.tgz differ diff --git a/src/App.tsx b/src/App.tsx index 5b80bfb74..4a7abe0e4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,15 @@ import ChatAiWidget, { ChatAiWidgetProps } from './components/widget/ChatAiWidget'; const App = (props: ChatAiWidgetProps) => { - return ; + return ( + + ); }; export default App; diff --git a/src/components/BotMessageFeedback.tsx b/src/components/BotMessageFeedback.tsx index 074ea0d02..4582e5a04 100644 --- a/src/components/BotMessageFeedback.tsx +++ b/src/components/BotMessageFeedback.tsx @@ -2,10 +2,10 @@ import { BaseMessage, Feedback, FeedbackRating } from '@sendbird/chat/message'; import { useReducer } from 'react'; import FeedbackIconButton from '@uikit/ui/FeedbackIconButton'; -import MessageFeedbackFailedModal from '@uikit/ui/MessageFeedbackFailedModal'; import MessageFeedbackModal from '@uikit/ui/MessageFeedbackModal'; import MobileFeedbackMenu from '@uikit/ui/MobileFeedbackMenu'; +import { AlertModal } from './ui/AlertModal'; import { elementIds } from '../const'; import { useConstantState } from '../context/ConstantContext'; import { Icon } from '../foundation/components/Icon'; @@ -137,13 +137,7 @@ function BotMessageFeedback({ message }: { message: BaseMessage }) { } { // error modal - !!state.errorText && ( - setState({ errorText: '' })} - /> - ) + !!state.errorText && setState({ errorText: '' })} /> } ); diff --git a/src/components/BotMessageWithBodyInput.tsx b/src/components/BotMessageWithBodyInput.tsx index e8cd6b90f..6b72aa6a3 100644 --- a/src/components/BotMessageWithBodyInput.tsx +++ b/src/components/BotMessageWithBodyInput.tsx @@ -63,15 +63,14 @@ export default function BotMessageWithBodyInput(props: Props) { const nonChainedMessage = chainTop == null && chainBottom == null; const displaySender = nonChainedMessage || chainTop; const displayProfileImage = nonChainedMessage || chainBottom; - const { profileUrl, nickname } = botStudioEditProps?.botInfo ?? {}; - const botProfileUrl = profileUrl ?? botUser?.profileUrl; + const { nickname } = botStudioEditProps?.botInfo ?? {}; const botNickname = nickname ?? botUser?.nickname; return ( {displayProfileImage ? (
- +
) : ( diff --git a/src/components/BotProfileImage.tsx b/src/components/BotProfileImage.tsx index 7f8a282cf..8eb53ca42 100644 --- a/src/components/BotProfileImage.tsx +++ b/src/components/BotProfileImage.tsx @@ -1,7 +1,9 @@ import { styled } from '@linaria/react'; import { useTheme } from 'styled-components'; +import { useChatContext } from './chat/context/ChatProvider'; import { getColorBasedOnSaturation } from '../colors'; +import { useConstantState } from '../context/ConstantContext'; import BotProfileIcon from '../icons/bot-profile-image-small.svg'; const Container = styled.span<{ backgroundColor: string; size: number }>` @@ -23,13 +25,17 @@ const Icon = styled(BotProfileIcon)<{ fill: string }>` `; type Props = { - profileUrl?: string; size: number; }; -function BotProfileImage({ size, profileUrl }: Props) { +function BotProfileImage({ size }: Props) { const theme = useTheme(); + const { botStudioEditProps } = useConstantState(); + const { botUser } = useChatContext(); + const { botInfo } = botStudioEditProps ?? {}; + const profileUrl = botInfo?.profileUrl ?? botUser?.profileUrl; + if (profileUrl) { return {'bot; } diff --git a/src/components/CustomMessage.tsx b/src/components/CustomMessage.tsx index 205b12884..3094a6414 100644 --- a/src/components/CustomMessage.tsx +++ b/src/components/CustomMessage.tsx @@ -12,6 +12,7 @@ import CustomTypingIndicatorBubble from './CustomTypingIndicatorBubble'; import FileMessage from './FileMessage'; import { CarouselMessage } from './messages/CarouselMessage'; import FormMessage from './messages/FormMessage'; +import { OutgoingFileMessage } from './messages/OutgoingFileMessage'; import ParsedBotMessageBody from './ParsedBotMessageBody'; import UserMessageWithBodyInput from './UserMessageWithBodyInput'; import { useConstantState } from '../context/ConstantContext'; @@ -32,13 +33,11 @@ type Props = { export default function CustomMessage(props: Props) { const { botUser } = useChatContext(); const { message, activeSpinnerId } = props; - const { replacementTextList, enableEmojiFeedback, botStudioEditProps = {} } = useConstantState(); + const { replacementTextList, enableEmojiFeedback } = useConstantState(); const { userId: currentUserId } = useWidgetSession(); - const { botInfo } = botStudioEditProps; const getCarouselItems = useCarouselItems(message); const botUserId = botUser?.userId; - const botProfileUrl = botInfo?.profileUrl ?? botUser?.profileUrl ?? ''; const isWaitingForBotReply = activeSpinnerId === message.messageId && !!botUser; const shouldRenderFeedback = () => { @@ -75,6 +74,15 @@ export default function CustomMessage(props: Props) { ); } + + if (message.isFileMessage()) { + return ( +
+ + {isWaitingForBotReply && } +
+ ); + } } // Sent by bot user @@ -95,7 +103,7 @@ export default function CustomMessage(props: Props) { } + bodyComponent={} createdAt={message.createdAt} messageFeedback={renderFeedbackButtons()} /> diff --git a/src/components/CustomTypingIndicatorBubble.tsx b/src/components/CustomTypingIndicatorBubble.tsx index c3c36e74f..ca910cf58 100644 --- a/src/components/CustomTypingIndicatorBubble.tsx +++ b/src/components/CustomTypingIndicatorBubble.tsx @@ -1,18 +1,10 @@ import BotProfileImage from './BotProfileImage'; -import { useChatContext } from './chat/context/ChatProvider'; -import { useConstantState } from '../context/ConstantContext'; import { TypingBubble } from '../foundation/components/TypingBubble'; function CustomTypingIndicatorBubble() { - const { botStudioEditProps = {} } = useConstantState(); - const { botUser } = useChatContext(); - - const botInfo = botStudioEditProps.botInfo; - const botProfileUrl = botInfo?.profileUrl ?? botUser?.profileUrl; - return (
- +
); diff --git a/src/components/FileMessage.tsx b/src/components/FileMessage.tsx index 85be3fc12..bb7457e95 100644 --- a/src/components/FileMessage.tsx +++ b/src/components/FileMessage.tsx @@ -2,19 +2,17 @@ import '../css/index.css'; import { FileMessage as ChatFileMessage } from '@sendbird/chat/message'; import { useState } from 'react'; -import FileViewer from '@uikit/modules/GroupChannel/components/FileViewer'; import { isImageMessage, isVideoMessage } from '@uikit/utils'; -import BotProfileImage from './BotProfileImage'; import { useChatContext } from './chat/context/ChatProvider'; +import { FileViewer } from './ui/FileViewer'; type Props = { message: ChatFileMessage; - profileUrl: string; }; export default function FileMessage(props: Props) { - const { message, profileUrl } = props; + const { message } = props; const { scrollSource } = useChatContext(); const [showFileViewer, setShowFileViewer] = useState(false); @@ -42,13 +40,7 @@ export default function FileMessage(props: Props) { onClick={() => setShowFileViewer(true)} /> )} - {showFileViewer && ( - setShowFileViewer(false)} - profile={} - /> - )} + {showFileViewer && setShowFileViewer(false)} />} ); } diff --git a/src/components/FormInput.tsx b/src/components/FormInput.tsx index f2aec4fb9..8d82a6a87 100644 --- a/src/components/FormInput.tsx +++ b/src/components/FormInput.tsx @@ -113,6 +113,11 @@ const Chip = styled.div` outline: 'none', 'box-shadow': `0 0 0 1px ${theme.borderColor.formChip.focus}`, }, + '&:active': { + color: theme.textColor.formChip.selected, + backgroundColor: theme.bgColor.formChip.selected, + border: `1px solid ${theme.borderColor.formChip.selected}`, + }, }; } case 'selected': { @@ -133,6 +138,11 @@ const Chip = styled.div` outline: 'none', 'box-shadow': `0 0 0 1px ${theme.borderColor.formChip.focus}`, }, + '&:active': { + color: theme.textColor.formChip.selected, + backgroundColor: theme.bgColor.formChip.selected, + border: `1px solid ${theme.borderColor.formChip.selected}`, + }, }; } case 'submittedDefault': { diff --git a/src/components/chat/hooks/useWidgetChatHandlers.ts b/src/components/chat/hooks/useWidgetChatHandlers.ts index 3eac67062..00bc59af7 100644 --- a/src/components/chat/hooks/useWidgetChatHandlers.ts +++ b/src/components/chat/hooks/useWidgetChatHandlers.ts @@ -2,9 +2,10 @@ import type { FileMessageCreateParams, UserMessageCreateParams } from '@sendbird import { useRef } from 'react'; import { useConstantState } from '../../../context/ConstantContext'; +import { getImageAspectRatioMetaArray } from '../../../utils/getImageAspectRatio'; export interface WidgetChatHandlers { - onBeforeSendMessage: (params: T) => T; + onBeforeSendMessage: (params: T) => Promise; onAfterSendMessage: () => void; } @@ -14,11 +15,19 @@ export const useWidgetChatHandlers = (params: { onScrollToBottom: () => void }) aiAttributesRef.current = botStudioEditProps?.aiAttributes; return { - onBeforeSendMessage: (params: T) => { + onBeforeSendMessage: async (params: T) => { + const metaArray = await getImageAspectRatioMetaArray(params); if (aiAttributesRef.current) { - return { ...params, data: JSON.stringify({ ai_attrs: aiAttributesRef.current }) }; + return { + ...params, + metaArrays: metaArray ? [metaArray] : undefined, + data: JSON.stringify({ ai_attrs: aiAttributesRef.current }), + }; } else { - return params; + return { + ...params, + metaArrays: metaArray ? [metaArray] : undefined, + }; } }, onAfterSendMessage: params.onScrollToBottom, diff --git a/src/components/chat/ui/ChatHeader.tsx b/src/components/chat/ui/ChatHeader.tsx index 515a7482f..faaf1b836 100644 --- a/src/components/chat/ui/ChatHeader.tsx +++ b/src/components/chat/ui/ChatHeader.tsx @@ -25,7 +25,6 @@ export const ChatHeader = ({ fullscreen }: Props) => { const { botInfo } = botStudioEditProps ?? {}; const botNickname = botInfo?.nickname ?? botUser?.nickname; - const profileUrl = botInfo?.profileUrl ?? botUser?.profileUrl; const buttonSize = isMobileView ? 24 : 16; const isExpandableMode = !fullscreen && !isMobileView; @@ -43,7 +42,7 @@ export const ChatHeader = ({ fullscreen }: Props) => { return (
- +
); }; @@ -54,13 +66,21 @@ const container = css` padding: 12px 16px; } + .sendbird-message-input--area { + background-color: ${themedColors.bg2}; + } + .sendbird-message-input { display: flex; align-items: center; .sendbird-message-input-text-field { - padding: 8px 16px; - height: 40px; - max-height: 116px; + min-height: 36px; + max-height: 100px; + height: 36px; + overflow-y: auto; + padding-top: 8px; + padding-bottom: 8px; + padding-inline-start: 16px; border-radius: 20px; // Not to zoom in on mobile set font-size to 16px which blocks the zooming on iOS // @link: https://weblog.west-wind.com/posts/2023/Apr/17/Preventing-iOS-Safari-Textbox-Zooming @@ -81,6 +101,20 @@ const container = css` bottom: unset; background-color: transparent; } + .sendbird-message-input--attach { + right: unset; + bottom: unset; + inset-inline-end: 12px; + inset-block-end: 2px; + & .sendbird-iconbutton__inner { + height: 16px; + } + &:hover { + path { + fill: ${themedColors.oncontent_inverse1}; + } + } + } .sendbird-message-input--placeholder { position: absolute; top: 50%; diff --git a/src/components/chat/ui/index.tsx b/src/components/chat/ui/index.tsx index 2cae28eaf..decbaebf9 100644 --- a/src/components/chat/ui/index.tsx +++ b/src/components/chat/ui/index.tsx @@ -3,15 +3,17 @@ import { css, cx } from '@linaria/core'; import { ChatHeader } from './ChatHeader'; import { ChatInput } from './ChatInput'; import { ChatMessageList } from './ChatMessageList'; -import { themedColors, themedColorVars } from '../../../foundation/colors/css'; +import { themedColorVars } from '../../../foundation/colors/css'; +import { useDragDropArea } from '../../../tools/hooks/useDragDropFiles'; import { PoweredByBanner } from '../../ui/PoweredByBanner'; type Props = { fullscreen: boolean; }; export const ChatUI = ({ fullscreen }: Props) => { + const dragHandlers = useDragDropArea(); return ( -
+
@@ -24,8 +26,13 @@ const container = css` font-family: var(--sendbird-font-family-default); height: 100%; width: 100%; - background-color: ${themedColors.bg1}; display: flex; flex-direction: column; flex: 1; + .sendbird-theme--light & { + background-color: var(--sendbird-light-background-50); + } + .sendbird-theme--dark & { + background-color: var(--sendbird-dark-background-700); + } `; diff --git a/src/components/messages/FormMessage.tsx b/src/components/messages/FormMessage.tsx index 70846b17c..433ae4dbc 100644 --- a/src/components/messages/FormMessage.tsx +++ b/src/components/messages/FormMessage.tsx @@ -4,13 +4,13 @@ import styled from 'styled-components'; import { isFormVersionCompatible } from '@uikit/modules/GroupChannel/context/utils'; import Button from '@uikit/ui/Button'; -import MessageFeedbackFailedModal from '@uikit/ui/MessageFeedbackFailedModal'; import FallbackUserMessage from './FallbackUserMessage'; -import { elementIds, widgetStringSet } from '../../const'; +import { widgetStringSet } from '../../const'; import { useConstantState } from '../../context/ConstantContext'; import { Label } from '../../foundation/components/Label'; import FormInput from '../FormInput'; +import { AlertModal } from '../ui/AlertModal'; interface Props { message: BaseMessage; @@ -196,15 +196,7 @@ export default function FormMessage(props: Props) { {isSubmitted ? 'Submitted successfully' : 'Submit'} - {submitFailed && ( - { - setSubmitFailed(false); - }} - /> - )} + {submitFailed && setSubmitFailed(false)} />} ); } diff --git a/src/components/messages/OutgoingFileMessage.tsx b/src/components/messages/OutgoingFileMessage.tsx new file mode 100644 index 000000000..b02693bcf --- /dev/null +++ b/src/components/messages/OutgoingFileMessage.tsx @@ -0,0 +1,183 @@ +import { css } from '@linaria/core'; +import { styled } from '@linaria/react'; +import { FileMessage } from '@sendbird/chat/message'; +import { useState } from 'react'; + +import { useConstantState } from '../../context/ConstantContext'; +import { Icon } from '../../foundation/components/Icon'; +import { Label } from '../../foundation/components/Label'; +import { Loader } from '../../foundation/components/Loader'; +import { META_ARRAY_ASPECT_RATIO_KEY } from '../../utils/getImageAspectRatio'; +import { formatCreatedAtToAMPM } from '../../utils/messageTimestamp'; +import { BodyComponent, BodyContainer, DefaultSentTime } from '../MessageComponent'; +import { FileViewer } from '../ui/FileViewer'; + +type Props = { + message: FileMessage; +}; + +export const OutgoingFileMessage = ({ message }: Props) => { + const { dateLocale } = useConstantState(); + + const hasMessageBubble = !!message.message; + const type = (() => { + if (message.type.startsWith('image/')) return 'image'; + if (message.type.startsWith('application/pdf')) return 'pdf'; + return 'unknown'; + })(); + const preview = (() => { + if (type === 'image') { + return ; + } + if (type === 'pdf') { + return ; + } + })(); + const renderTimestamp = () => { + return ( +
+ {formatCreatedAtToAMPM(message.createdAt, dateLocale)} +
+ ); + }; + + return ( +
+
+ {!hasMessageBubble && preview && renderTimestamp()} +
{preview}
+
+ + {(hasMessageBubble || type === 'unknown') && ( +
+ {renderTimestamp()} + + +
{message.message || 'Unknown file type'}
+
+
+
+ )} +
+ ); +}; + +const container = css` + display: flex; + width: 100%; + flex-direction: column; + align-items: flex-end; + gap: 2px; +`; +const bubbleContainer = css` + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: flex-end; + width: 100%; + gap: 4px; +`; +const previewContainer = css` + display: flex; + flex: 1; + max-width: 244px; +`; +const timestampContainer = css` + display: flex; + align-items: flex-end; +`; + +const ImagePreview = ({ message }: Props) => { + const [viewer, setViewer] = useState(false); + const [fileUrl] = useState(() => + message.messageParams?.file instanceof File ? URL.createObjectURL(message.messageParams?.file) : message.url, + ); + const aspectRatio = message.metaArrays.find((it) => it.key === META_ARRAY_ASPECT_RATIO_KEY)?.value?.[0]; + return ( + <> + setViewer(true)} + /> + {viewer && setViewer(false)} />} + + ); +}; + +// TODO: Refactor +const PDFPreview = ({ message }: Props) => { + return ( +
+
+
+ +
+
+ + +
+
+
+ ); +}; + +const ImageContainer = styled.div<{ ratio: string }>` + width: 100%; + height: auto; + aspect-ratio: ${(props) => props.ratio}; + position: relative; + overflow: hidden; + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + .sendbird-theme--light & { + background-color: var(--sendbird-dark-background-100); + } + .sendbird-theme--dark & { + background-color: var(--sendbird-dark-background-400); + } +`; + +const StyledImage = styled.img<{ loaded: boolean }>` + position: absolute; + width: 100%; + height: 100%; + inset-block-start: 0; + inset-inline-start: 0; + object-fit: cover; + opacity: ${(props) => (props.loaded ? 1 : 0)}; + transition: opacity 0.5s ease; +`; + +const ImageWithPlaceholder = ({ + src, + alt, + aspectRatio, + onClick, +}: { + src: string; + alt: string; + aspectRatio: string; + onClick?: () => void; +}) => { + const [loaded, setLoaded] = useState(false); + + return ( + + {!loaded && ( + + + + )} + setLoaded(true)} /> + + ); +}; diff --git a/src/components/ui/AlertModal.tsx b/src/components/ui/AlertModal.tsx new file mode 100644 index 000000000..bf3f88a33 --- /dev/null +++ b/src/components/ui/AlertModal.tsx @@ -0,0 +1,13 @@ +import MessageFeedbackFailedModal from '@uikit/ui/MessageFeedbackFailedModal'; + +import { elementIds } from '../../const'; + +interface Props { + message: string; + onClose: () => void; +} + +// TODO: Remove UIKit +export const AlertModal = ({ message, onClose }: Props) => { + return ; +}; diff --git a/src/components/ui/FileViewer.tsx b/src/components/ui/FileViewer.tsx new file mode 100644 index 000000000..84ede8d87 --- /dev/null +++ b/src/components/ui/FileViewer.tsx @@ -0,0 +1,12 @@ +import { FileMessage } from '@sendbird/chat/message'; + +import { FileViewerView } from '@uikit/modules/GroupChannel/components/FileViewer/FileViewerView'; + +interface Props { + message: FileMessage; + onClose: () => void; +} +// TODO: Remove UIKit +export const FileViewer = ({ message, onClose }: Props) => { + return ; +}; diff --git a/src/components/widget/ProviderContainer.tsx b/src/components/widget/ProviderContainer.tsx index ffdc7fd88..93311b25c 100644 --- a/src/components/widget/ProviderContainer.tsx +++ b/src/components/widget/ProviderContainer.tsx @@ -11,6 +11,7 @@ import { useWidgetSession, useWidgetSetting, WidgetSettingProvider } from '../.. import { useWidgetState, WidgetStateProvider } from '../../context/WidgetStateContext'; import { useStyledComponentsTarget } from '../../hooks/useStyledComponentsTarget'; import { getTheme } from '../../theme'; +import { DragDropProvider } from '../../tools/hooks/useDragDropFiles'; import { isDashboardPreview } from '../../utils'; const CHAT_AI_WIDGET_KEY = import.meta.env.VITE_CHAT_AI_WIDGET_KEY; @@ -33,7 +34,7 @@ const SBComponent = ({ children }: { children: React.ReactElement }) => { } = useConstantState(); const { setIsVisible } = useWidgetState(); - const { botStyle } = useWidgetSetting(); + const { botConfigs, botStyle } = useWidgetSetting(); const session = useWidgetSession(); const target = useStyledComponentsTarget(); @@ -107,8 +108,7 @@ const SBComponent = ({ children }: { children: React.ReactElement }) => { uikitOptions={{ groupChannel: { input: { - // To hide the file upload icon from the message input - enableDocument: false, + enableDocument: botConfigs.replyToFile, }, enableVoiceMessage: false, enableSuggestedReplies: true, @@ -134,7 +134,9 @@ export default function ProviderContainer(props: ProviderContainerProps) { - {props.children} + + {props.children} + diff --git a/src/context/WidgetSettingContext.tsx b/src/context/WidgetSettingContext.tsx index 2c8dc027f..0c9814fd6 100644 --- a/src/context/WidgetSettingContext.tsx +++ b/src/context/WidgetSettingContext.tsx @@ -24,10 +24,14 @@ export interface BotStyle { toggleButtonUrl?: string; autoOpen: boolean; } +export interface BotConfigs { + replyToFile: boolean; +} type Context = { initialized: boolean; botStyle: BotStyle; + botConfigs: BotConfigs; widgetSession: WidgetSession | null; initManualSession: (sdk: SendbirdChatWith<[GroupChannelModule]>) => void; resetSession: () => Promise; @@ -59,6 +63,8 @@ export const WidgetSettingProvider = ({ children }: React.PropsWithChildren) => const inProgress = React.useRef(false); const [initialized, setInitialized] = useState(false); + + const [botConfigs, setBotConfigs] = useState({ replyToFile: false }); const [botStyle, setBotStyle] = useState({ theme: 'light', accentColor: '#742DDD', @@ -95,6 +101,7 @@ export const WidgetSettingProvider = ({ children }: React.PropsWithChildren) => locale, }) .onError(callbacks?.onWidgetSettingFailure) + .onGetBotConfigs((configs) => setBotConfigs(configs)) .onGetBotStyle((style) => setBotStyle(style)) .onAutoNonCached(({ user, channel }) => { const session = { @@ -196,6 +203,7 @@ export const WidgetSettingProvider = ({ children }: React.PropsWithChildren) => botStudioEditProps?.styles?.accentColor ?? botStudioEditProps?.styles?.primaryColor ?? botStyle.accentColor, autoOpen: autoOpen ?? botStyle.autoOpen, }, + botConfigs, widgetSession, initManualSession, resetSession: () => initSessionByStrategy(sessionStrategy, true), diff --git a/src/css/index.css b/src/css/index.css index 9c0276e6b..e1d6f44b6 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -91,16 +91,6 @@ input:focus { z-index: 2147483647; } -/* Hide scroll bar in Firefox */ -.sendbird-message-input-text-field { - -ms-overflow-style: none; - scrollbar-width: none; -} -/* Hide scroll bar in WebKit */ -.sendbird-message-input-text-field::-webkit-scrollbar { - display: none; -} - .sendbird-modal__content { max-width: calc(100% - 80px); } diff --git a/src/foundation/components/Icon/index.tsx b/src/foundation/components/Icon/index.tsx index 3430dc3eb..e21abbe79 100644 --- a/src/foundation/components/Icon/index.tsx +++ b/src/foundation/components/Icon/index.tsx @@ -79,7 +79,8 @@ export type IconType = | 'chevron-down' | 'feedback-like' | 'feedback-dislike' - | 'done'; + | 'done' + | 'file-document'; type SVG = React.FC>; const components: Record Promise }> = { @@ -119,6 +120,10 @@ const components: Record Promise import('../../../../packages/uikit/src/svgs/icon-done.svg').then((it) => it.default), }, + 'file-document': { + module: null, + load: () => import('../../../../packages/uikit/src/svgs/icon-file-document.svg').then((it) => it.default), + }, }; type Props = SBUFoundationProps<{ diff --git a/src/libs/api/widgetSetting.ts b/src/libs/api/widgetSetting.ts index eb38afc7f..75a6b4cf7 100644 --- a/src/libs/api/widgetSetting.ts +++ b/src/libs/api/widgetSetting.ts @@ -3,6 +3,9 @@ import { SendbirdError } from '@sendbird/chat'; import { noop, resolvePath } from '../../utils'; type APIResponse = { + bot?: { + reply_to_file?: boolean; + }; bot_style: { color: { theme: 'light' | 'dark'; @@ -34,6 +37,9 @@ type Params = { }; type Response = { + bot: { + replyToFile: boolean; + }; botStyle: { theme: 'light' | 'dark'; accentColor: string; @@ -79,6 +85,9 @@ export async function getWidgetSetting({ const json = result as APIResponse; return { + bot: { + replyToFile: json.bot?.reply_to_file ?? false, + }, botStyle: { theme: json.bot_style.color.theme, accentColor: json.bot_style.color.accent_color, @@ -119,6 +128,7 @@ export const widgetSettingHandler = ( params: Omit, ) => { type Callbacks = { + BotConfigs: (bot: Response['bot']) => void; BotStyle: (botStyle: Response['botStyle']) => void; AutoNonCached: (response: { user: ResponseUser; channel: ResponseChannel }) => void; AutoCached: (response: { channel?: ResponseChannel }) => void; @@ -129,6 +139,7 @@ export const widgetSettingHandler = ( const callbacks: { onError: Callbacks['Error']; + onGetBotConfigs: Callbacks['BotConfigs']; onGetBotStyle: Callbacks['BotStyle']; onAutoNonCached: Callbacks['AutoNonCached']; onAutoCached: Callbacks['AutoCached']; @@ -136,6 +147,7 @@ export const widgetSettingHandler = ( onManualCached: Callbacks['ManualCached']; } = { onError: noop, + onGetBotConfigs: noop, onGetBotStyle: noop, onAutoNonCached: noop, onAutoCached: noop, @@ -148,6 +160,10 @@ export const widgetSettingHandler = ( if (callback) callbacks.onError = callback; return handlers; }, + onGetBotConfigs: (callback: Callbacks['BotConfigs']) => { + callbacks.onGetBotConfigs = callback; + return handlers; + }, onGetBotStyle: (callback: Callbacks['BotStyle']) => { callbacks.onGetBotStyle = callback; return handlers; @@ -177,6 +193,7 @@ export const widgetSettingHandler = ( ...getParamsByStrategy(strategy, useCachedSession, params), }); + callbacks.onGetBotConfigs(response.bot); callbacks.onGetBotStyle(response.botStyle); if (strategy === 'auto') handleAutoStrategy(response); if (strategy === 'manual') handleManualStrategy(response); diff --git a/src/tools/hooks/useDragDropFiles.tsx b/src/tools/hooks/useDragDropFiles.tsx new file mode 100644 index 000000000..8de8b75e7 --- /dev/null +++ b/src/tools/hooks/useDragDropFiles.tsx @@ -0,0 +1,58 @@ +import { createContext, ReactNode, DragEvent, useContext, useEffect, useRef } from 'react'; + +import { noop } from '../../utils'; + +type Unsubscribe = () => void; + +interface DragDropContextProps { + onDrop: (e: DragEvent) => void; + subscribe: (fn: (files: File[]) => void) => Unsubscribe; +} +const DragDropContext = createContext({ + onDrop: noop, + subscribe: () => noop, +}); + +interface DragDropContextProviderProps { + children?: ReactNode; +} +export const DragDropProvider = ({ children }: DragDropContextProviderProps) => { + const subscribers = useRef(new Set<(files: File[]) => void>()); + + return ( + { + e.preventDefault(); + if (e.dataTransfer?.files) { + const files = Array.from(e.dataTransfer.files); + if (files.length > 0) { + subscribers.current.forEach((fn) => fn(files)); + } + } + }, + subscribe: (fn: (files: File[]) => void) => { + subscribers.current.add(fn); + + return () => subscribers.current.delete(fn); + }, + }} + > + {children} + + ); +}; + +type UseDragDropFiles = { + onDropFiles: (files: File[]) => void; +}; +export const useDragDropFiles = ({ onDropFiles }: UseDragDropFiles) => { + const { subscribe } = useContext(DragDropContext); + useEffect(() => { + return subscribe(onDropFiles); + }, [onDropFiles]); +}; +export const useDragDropArea = () => { + const { onDrop } = useContext(DragDropContext); + return { onDrop, onDragOver: (e: DragEvent) => e.preventDefault() }; +}; diff --git a/src/utils/getImageAspectRatio.ts b/src/utils/getImageAspectRatio.ts new file mode 100644 index 000000000..8c0c310ae --- /dev/null +++ b/src/utils/getImageAspectRatio.ts @@ -0,0 +1,29 @@ +import { UserMessageCreateParams, FileMessageCreateParams, MessageMetaArray } from '@sendbird/chat/message'; + +export function getImageAspectRatio(file: File) { + return new Promise((resolve) => { + const img = new Image(); + + img.onload = function () { + const width = img.width; + const height = img.height; + + URL.revokeObjectURL(img.src); + resolve(width / height); + }; + + img.src = URL.createObjectURL(file); + }); +} + +export async function getImageAspectRatioMetaArray(params: FileMessageCreateParams | UserMessageCreateParams) { + if ('file' in params && params.file instanceof File && params.file.type.startsWith('image/')) { + const ratio = await getImageAspectRatio(params.file); + return new MessageMetaArray({ + key: META_ARRAY_ASPECT_RATIO_KEY, + value: [`${ratio}`], + }); + } +} + +export const META_ARRAY_ASPECT_RATIO_KEY = 'KEY_IMG_ASPECT_RATIO'; diff --git a/yarn.lock b/yarn.lock index cfe615cc6..7a487ce74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3308,9 +3308,9 @@ __metadata: languageName: unknown linkType: soft -"@sendbird/chat@npm:^4.14.1": - version: 4.14.1 - resolution: "@sendbird/chat@npm:4.14.1" +"@sendbird/chat@file:./sendbird-chat-4.14.2.tgz::locator=%40sendbird%2Fchat-ai-widget%40workspace%3A.": + version: 4.14.2 + resolution: "@sendbird/chat@file:./sendbird-chat-4.14.2.tgz#./sendbird-chat-4.14.2.tgz::hash=a20935&locator=%40sendbird%2Fchat-ai-widget%40workspace%3A." peerDependencies: "@react-native-async-storage/async-storage": ^1.17.6 react-native-mmkv: ^2.0.0 @@ -3319,7 +3319,7 @@ __metadata: optional: true react-native-mmkv: optional: true - checksum: 10c0/8cac6a9a3cb1ce4a5d35faaa6516274fcd182e4e7816dfff40ca468e498c2500e6fcb586b854eddb869029a571d52fe5c3b22dd24ba98613110e9c214d97e756 + checksum: 10c0/67397ec51dcecd605fb997d1827cd7f436711c779ce888f7cbac6de9ca3e0ec0e3b9be21a881433aaad16eee60a328d970a4c1ccc75ed641b078add3383dadba languageName: node linkType: hard