Skip to content

Commit

Permalink
Made toolbars layout configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
abirc8010 committed Dec 24, 2024
1 parent 0cc4ce4 commit e379c57
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 85 deletions.
2 changes: 2 additions & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@
"react-dom": ">=17.0.2 <19.0.0"
},
"dependencies": {
"@dnd-kit/core": "6.1.0",
"@dnd-kit/sortable": "8.0.0",
"@embeddedchat/api": "0.0.2",
"@embeddedchat/ui-elements": "workspace:^",
"@embeddedchat/ui-kit": "workspace:^",
Expand Down
18 changes: 14 additions & 4 deletions packages/react/src/views/ChatInput/AudioMessageRecorder.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import RCContext from '../../context/RCInstance';
import useMessageStore from '../../store/messageStore';
import { getCommonRecorderStyles } from './ChatInput.styles';

const AudioMessageRecorder = () => {
const AudioMessageRecorder = (props) => {
const { isItemInPopOver, applyPopOverStyles } = props;
const videoRef = useRef(null);
const { theme } = useTheme();
const styles = getCommonRecorderStyles(theme);
Expand Down Expand Up @@ -139,9 +140,18 @@ const AudioMessageRecorder = () => {

if (state === 'idle') {
return (
<ActionButton ghost square onClick={handleRecordButtonClick}>
<Icon size="1.25rem" name="mic" />
</ActionButton>
<>
{isItemInPopOver ? (
<Box onClick={handleRecordButtonClick} css={applyPopOverStyles}>
<Icon name="mic" size="1.25rem" />
<span>audio message</span>
</Box>
) : (
<ActionButton ghost square onClick={handleRecordButtonClick}>
<Icon size="1.25rem" name="mic" />
</ActionButton>
)}
</>
);
}

Expand Down
3 changes: 1 addition & 2 deletions packages/react/src/views/ChatInput/ChatInput.styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,7 @@ export const getChatInputFormattingToolbarStyles = ({ theme, mode }) => {
`,
popOverItemStyles: css`
display: flex;
gap: 0.5rem;
align-items: center;
gap: 1rem;
cursor: pointer;
padding: 0.5rem;
`,
Expand Down
256 changes: 186 additions & 70 deletions packages/react/src/views/ChatInput/ChatInputFormattingToolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ import {
useComponentOverrides,
useTheme,
} from '@embeddedchat/ui-elements';
import {
DndContext,
closestCenter,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { SortableContext, useSortable, arrayMove } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { EmojiPicker } from '../EmojiPicker/index';
import { useMessageStore } from '../../store';
import { formatter } from '../../lib/textFormat';
Expand All @@ -16,12 +25,27 @@ import VideoMessageRecorder from './VideoMessageRecoder';
import { getChatInputFormattingToolbarStyles } from './ChatInput.styles';
import formatSelection from '../../lib/formatSelection';

const DraggableItem = ({ id, children }) => {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });

const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
{children}
</div>
);
};

const ChatInputFormattingToolbar = ({
messageRef,
inputRef,
triggerButton,
optionConfig = {
surfaceItems: ['emoji', 'formatter', 'audio', 'video', 'file'],
surfaceItems: ['audio', 'video', 'file'],
formatters: ['bold', 'italic', 'strike', 'code', 'multiline'],
},
}) => {
Expand All @@ -34,13 +58,19 @@ const ChatInputFormattingToolbar = ({
configOverrides.optionConfig?.surfaceItems || optionConfig.surfaceItems;
const formatters =
configOverrides.optionConfig?.formatters || optionConfig.formatters;

const isRecordingMessage = useMessageStore(
(state) => state.isRecordingMessage
);

const [isEmojiOpen, setEmojiOpen] = useState(false);
const [isPopoverOpen, setPopoverOpen] = useState(false);
const [toolbarItems, setItems] = useState([
...optionConfig.formatters.map((item) => ({ id: item, type: 'formatter' })),
...optionConfig.surfaceItems.map((item) => ({
id: item,
type: 'surfaceItem',
})),
]);
const popoverRef = useRef(null);

useEffect(() => {
Expand Down Expand Up @@ -68,44 +98,90 @@ const ChatInputFormattingToolbar = ({
)}: `;
triggerButton?.(null, message);
};

const chatToolMap = {
audio: (
<Tooltip text="Audio Message" position="top" key="audio">
<AudioMessageRecorder />
<AudioMessageRecorder
isItemInPopOver={
isPopoverOpen === true &&
toolbarItems.slice(0, 5).some((item) => item.id === 'audio')
}
applyPopOverStyles={styles.popOverItemStyles}
/>
</Tooltip>
),
video: (
<Tooltip text="Video Message" position="top" key="video">
<VideoMessageRecorder />
</Tooltip>
),
file: (
<Tooltip text="Upload File" position="top" key="file">
<ActionButton
square
ghost
disabled={isRecordingMessage}
onClick={handleClickToOpenFiles}
>
<Icon name="attachment" size="1.25rem" />
</ActionButton>
<VideoMessageRecorder
isItemInPopOver={
isPopoverOpen === true &&
toolbarItems.slice(0, 5).some((item) => item.id === 'video')
}
applyPopOverStyles={styles.popOverItemStyles}
/>
</Tooltip>
),
file:
isPopoverOpen === true &&
toolbarItems.slice(0, 5).some((item) => item.id === 'file') ? (
<Box onClick={handleClickToOpenFiles} css={styles.popOverItemStyles}>
<Icon
disabled={isRecordingMessage}
name="attachment"
size="1.25rem"
/>
<span> upload file</span>
</Box>
) : (
<Tooltip text="Upload File" position="top" key="file">
<ActionButton
square
ghost
disabled={isRecordingMessage}
onClick={handleClickToOpenFiles}
>
<Icon name="attachment" size="1.25rem" />
</ActionButton>
</Tooltip>
),
};

const handleFormatterClick = (item) => {
formatSelection(messageRef, item.pattern);
setPopoverOpen(false);
};

const onDragEnd = ({ active, over }) => {
if (!over) return;
const activeItemIndex = toolbarItems.findIndex(
(item) => item.id === active.id
);
const overItemIndex = toolbarItems.findIndex((item) => item.id === over.id);
if (activeItemIndex === -1 || overItemIndex === -1) return;
if (activeItemIndex === overItemIndex) return;
const updatedItems = arrayMove(
toolbarItems,
activeItemIndex,
overItemIndex
);
setItems(updatedItems);
};

const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
})
);

return (
<Box
css={styles.chatFormat}
className={`ec-chat-input-formatting-toolbar ${classNames}`}
style={styleOverrides}
>
<Tooltip text="Emoji" position="top" key="emoji-btn">
<Tooltip text="Emoji" position="top" key="emoji">
<ActionButton
square
ghost
Expand All @@ -115,39 +191,6 @@ const ChatInputFormattingToolbar = ({
<Icon name="emoji" size="1.25rem" />
</ActionButton>
</Tooltip>

<Box
css={css`
display: flex;
@media (max-width: 399px) {
display: none;
}
`}
>
{formatters
.map((name) => formatter.find((item) => item.name === name))
.map((item) => (
<Tooltip
text={item.name}
position="top"
key={`formatter-${item.name}`}
>
<ActionButton
square
disabled={isRecordingMessage}
ghost
onClick={() => formatSelection(messageRef, item.pattern)}
>
<Icon
disabled={isRecordingMessage}
name={item.name}
size="1.25rem"
/>
</ActionButton>
</Tooltip>
))}
</Box>

<Box
css={css`
@media (min-width: 400px) {
Expand All @@ -167,29 +210,102 @@ const ChatInputFormattingToolbar = ({
</Tooltip>
</Box>

{isPopoverOpen && (
<Box ref={popoverRef} css={styles.popOverStyles}>
{formatters
.map((name) => formatter.find((item) => item.name === name))
.map((item) => (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onDragEnd}
>
{isPopoverOpen ? (
<Box ref={popoverRef} css={styles.popOverStyles}>
<SortableContext items={toolbarItems}>
{toolbarItems.slice(0, 5).map((item) => (
<DraggableItem id={item.id} key={item.id} type={item.type}>
{item.type === 'surfaceItem' ? (
chatToolMap[item.id]
) : (
<Box
key={item.id}
disabled={isRecordingMessage}
css={styles.popOverItemStyles}
onClick={() =>
handleFormatterClick(
formatter.find((f) => f.name === item.id)
)
}
>
<Icon
disabled={isRecordingMessage}
name={item.id}
size="1.25rem"
/>
<span>{item.id}</span>
</Box>
)}
</DraggableItem>
))}
</SortableContext>
</Box>
) : (
<SortableContext items={toolbarItems}>
{toolbarItems.slice(0, 5).map((item) => (
<Box
key={item.name}
disabled={isRecordingMessage}
onClick={() => handleFormatterClick(item)}
css={styles.popOverItemStyles}
key={item.id}
css={css`
display: flex;
@media (max-width: 399px) {
display: none;
}
`}
>
<Icon
disabled={isRecordingMessage}
name={item.name}
size="1rem"
/>
<span>{item.name}</span>
<DraggableItem id={item.id} key={item.id} type={item.type}>
{item.type === 'surfaceItem' ? (
chatToolMap[item.id]
) : (
<Tooltip text={item.id} position="top">
<ActionButton
square
disabled={isRecordingMessage}
ghost
onClick={() =>
handleFormatterClick(
formatter.find((f) => f.name === item.id)
)
}
>
<Icon name={item.id} size="1.25rem" />
</ActionButton>
</Tooltip>
)}
</DraggableItem>
</Box>
))}
</Box>
)}

{surfaceItems.map((key) => chatToolMap[key])}
</SortableContext>
)}
<SortableContext items={toolbarItems.slice(5)}>
{toolbarItems.slice(5).map((item) => (
<DraggableItem id={item.id} key={item.id} type={item.type}>
{item.type === 'surfaceItem' ? (
chatToolMap[item.id]
) : (
<Tooltip text={item.id} position="top">
<ActionButton
square
disabled={isRecordingMessage}
ghost
onClick={() =>
handleFormatterClick(
formatter.find((f) => f.name === item.id)
)
}
>
<Icon name={item.id} size="1.25rem" />
</ActionButton>
</Tooltip>
)}
</DraggableItem>
))}
</SortableContext>
</DndContext>

{isEmojiOpen && (
<EmojiPicker
Expand Down
Loading

0 comments on commit e379c57

Please sign in to comment.