Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AC-3468] feat: file message support #355

Merged
merged 14 commits into from
Sep 11, 2024
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,8 @@
"workspaces": [
"packages/*"
],
"packageManager": "[email protected]"
"packageManager": "[email protected]",
"resolutions": {
"@sendbird/chat": "file:./sendbird-chat-4.14.2.tgz"
liamcho marked this conversation as resolved.
Show resolved Hide resolved
}
}
Binary file added sendbird-chat-4.14.2.tgz
Binary file not shown.
10 changes: 9 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import ChatAiWidget, { ChatAiWidgetProps } from './components/widget/ChatAiWidget';

const App = (props: ChatAiWidgetProps) => {
return <ChatAiWidget {...props} />;
return (
<ChatAiWidget
{...props}
applicationId={'B13D7DE1-7F2B-4343-A896-E812E2BBC67A'}
botId={'onboarding_bot'}
apiHost={'https://api-no2.sendbirdtest.com'}
wsHost={'wss://ws-no2.sendbirdtest.com'}
/>
);
};

export default App;
10 changes: 2 additions & 8 deletions src/components/BotMessageFeedback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -137,13 +137,7 @@ function BotMessageFeedback({ message }: { message: BaseMessage }) {
}
{
// error modal
!!state.errorText && (
<MessageFeedbackFailedModal
text={state.errorText}
rootElementId={elementIds.widgetWindow}
onCancel={() => setState({ errorText: '' })}
/>
)
!!state.errorText && <AlertModal message={state.errorText} onClose={() => setState({ errorText: '' })} />
}
</>
);
Expand Down
5 changes: 2 additions & 3 deletions src/components/BotMessageWithBodyInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Root>
{displayProfileImage ? (
<div style={{ paddingBottom: profilePaddingBottom }}>
<BotProfileImage size={28} profileUrl={botProfileUrl} />
<BotProfileImage size={28} />
</div>
) : (
<EmptyImageContainer />
Expand Down
10 changes: 8 additions & 2 deletions src/components/BotProfileImage.tsx
Original file line number Diff line number Diff line change
@@ -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 }>`
Expand All @@ -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 <img src={profileUrl} style={{ borderRadius: '50%', width: size, height: size }} alt={'bot profile'} />;
}
Expand Down
16 changes: 12 additions & 4 deletions src/components/CustomMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 = () => {
Expand Down Expand Up @@ -75,6 +74,15 @@ export default function CustomMessage(props: Props) {
</div>
);
}

if (message.isFileMessage()) {
return (
<div>
<OutgoingFileMessage message={message} />
{isWaitingForBotReply && <CustomTypingIndicatorBubble />}
</div>
);
}
}

// Sent by bot user
Expand All @@ -95,7 +103,7 @@ export default function CustomMessage(props: Props) {
<BotMessageWithBodyInput
wideContainer={isVideoMessage(message)}
{...props}
bodyComponent={<FileMessage message={message} profileUrl={botProfileUrl} />}
bodyComponent={<FileMessage message={message} />}
createdAt={message.createdAt}
messageFeedback={renderFeedbackButtons()}
/>
Expand Down
10 changes: 1 addition & 9 deletions src/components/CustomTypingIndicatorBubble.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 8, marginTop: 16 }}>
<BotProfileImage size={28} profileUrl={botProfileUrl} />
<BotProfileImage size={28} />
<TypingBubble />
</div>
);
Expand Down
14 changes: 3 additions & 11 deletions src/components/FileMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -42,13 +40,7 @@ export default function FileMessage(props: Props) {
onClick={() => setShowFileViewer(true)}
/>
)}
{showFileViewer && (
<FileViewer
message={message}
onCancel={() => setShowFileViewer(false)}
profile={<BotProfileImage size={32} profileUrl={profileUrl} />}
/>
)}
{showFileViewer && <FileViewer message={message} onClose={() => setShowFileViewer(false)} />}
</div>
);
}
10 changes: 10 additions & 0 deletions src/components/FormInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ const Chip = styled.div<ChipProps>`
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': {
Expand All @@ -133,6 +138,11 @@ const Chip = styled.div<ChipProps>`
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': {
Expand Down
17 changes: 13 additions & 4 deletions src/components/chat/hooks/useWidgetChatHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: <T extends UserMessageCreateParams | FileMessageCreateParams>(params: T) => T;
onBeforeSendMessage: <T extends UserMessageCreateParams | FileMessageCreateParams>(params: T) => Promise<T>;
onAfterSendMessage: () => void;
}

Expand All @@ -14,11 +15,19 @@ export const useWidgetChatHandlers = (params: { onScrollToBottom: () => void })
aiAttributesRef.current = botStudioEditProps?.aiAttributes;

return {
onBeforeSendMessage: <T extends UserMessageCreateParams | FileMessageCreateParams>(params: T) => {
onBeforeSendMessage: async <T extends UserMessageCreateParams | FileMessageCreateParams>(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,
bang9 marked this conversation as resolved.
Show resolved Hide resolved
metaArrays: metaArray ? [metaArray] : undefined,
};
}
},
onAfterSendMessage: params.onScrollToBottom,
Expand Down
3 changes: 1 addition & 2 deletions src/components/chat/ui/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -43,7 +42,7 @@ export const ChatHeader = ({ fullscreen }: Props) => {
return (
<div className={container}>
<div style={{ marginRight: 6 }}>
<BotProfileImage size={34} profileUrl={profileUrl} />
<BotProfileImage size={34} />
</div>
<div className={headerCenter}>
<Label type={'h2'} color={'onbackground1'} className={titleInline}>
Expand Down
52 changes: 43 additions & 9 deletions src/components/chat/ui/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { css } from '@linaria/core';
import { useRef } from 'react';
import { useRef, useState } from 'react';

import useSendbirdStateContext from '@uikit/hooks/useSendbirdStateContext';
import MessageInputWrapperView from '@uikit/modules/GroupChannel/components/MessageInputWrapper/MessageInputWrapperView';

import { themedColors } from '../../../foundation/colors/css';
import { useBlockWhileBotResponding } from '../../../hooks/useBlockWhileBotResponding';
import { isIOSMobile } from '../../../utils';
import { AlertModal } from '../../ui/AlertModal';
import { useChatContext } from '../context/ChatProvider';

// TODO: Remove UIKit
export const ChatInput = () => {
const { channel, botUser, dataSource, handlers } = useChatContext();

const ref = useRef<HTMLDivElement>(null);
const [limitError, setLimitError] = useState(false);

const { config } = useSendbirdStateContext();
const isMessageInputDisabled = useBlockWhileBotResponding({
Expand All @@ -28,14 +31,23 @@ export const ChatInput = () => {
messageInputRef={ref}
currentChannel={channel as any}
messages={dataSource.messages}
sendUserMessage={(params) => {
const processedParams = handlers.onBeforeSendMessage(params);
dataSource.sendUserMessage(processedParams, handlers.onAfterSendMessage).then(handlers.onAfterSendMessage);
sendUserMessage={async (params) => {
const processedParams = await handlers.onBeforeSendMessage(params);
const message = await dataSource.sendUserMessage(processedParams, () => handlers.onAfterSendMessage());
handlers.onAfterSendMessage();
liamcho marked this conversation as resolved.
Show resolved Hide resolved
return message;
}}
sendFileMessage={() => {
throw new Error('Not implemented');
sendFileMessage={async (params) => {
const processedParams = await handlers.onBeforeSendMessage(params);
const message = await dataSource.sendFileMessage(processedParams, () => handlers.onAfterSendMessage());
handlers.onAfterSendMessage();
liamcho marked this conversation as resolved.
Show resolved Hide resolved
return message;
}}
onFileLimitError={() => setLimitError(true)}
/>
{limitError && (
<AlertModal message={"You can't upload more than one image"} onClose={() => setLimitError(false)} />
)}
</div>
);
};
Expand All @@ -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
Expand All @@ -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%;
Expand Down
Loading