-
+
);
};
@@ -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