Skip to content

Commit 309ad8e

Browse files
committed
refactor(mobile): redesign chatroom message composer
1 parent 7bc22f9 commit 309ad8e

16 files changed

Lines changed: 229 additions & 33 deletions

File tree

packages/webapp/components/BottomSheet.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import ChatContainerMobile from './pages/document/components/chat/ChatContainerM
88
import useKeyboardHeight from '@hooks/useKeyboardHeight'
99

1010
const BottomSheet = () => {
11-
const { activeSheet, sheetData, closeSheet } = useSheetStore()
11+
const { activeSheet, closeSheet } = useSheetStore()
1212
const chatRoom = useChatStore((state) => state.chatRoom)
1313
const closeChatRoom = useChatStore((state) => state.closeChatRoom)
1414
const destroyChatRoom = useChatStore((state) => state.destroyChatRoom)
15-
const containerRef = useRef<HTMLDivElement>(null)
15+
const setSheetContainerRef = useSheetStore((state) => state.setSheetContainerRef)
1616
const { height: keyboardHeight } = useKeyboardHeight()
1717

1818
// Sync chat store with bottom sheet
@@ -47,7 +47,8 @@ const BottomSheet = () => {
4747
case 'chatroom':
4848
return {
4949
id: 'chatroom_sheet',
50-
snapPoints: [1, 0.5, 0]
50+
detent: 'full-height' as SheetProps['detent'],
51+
disableScrollLocking: true
5152
}
5253
default:
5354
return {
@@ -63,6 +64,12 @@ const BottomSheet = () => {
6364
// Fix the bottom sheet height when keyboard is open
6465
style: { paddingBottom: keyboardHeight }
6566
}
67+
case 'chatroom':
68+
return {
69+
style: {
70+
paddingBottom: keyboardHeight
71+
}
72+
}
6673
default:
6774
return {}
6875
}
@@ -84,7 +91,7 @@ const BottomSheet = () => {
8491
isOpen={!!activeSheet}
8592
onClose={handleClose}
8693
{...getSheetProps()}>
87-
<Sheet.Container ref={containerRef}>
94+
<Sheet.Container ref={setSheetContainerRef}>
8895
<Sheet.Header />
8996
<Sheet.Content {...getSheetContentProps()}>{renderContent()}</Sheet.Content>
9097
</Sheet.Container>

packages/webapp/components/TipTap/toolbar/Icon.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react'
2-
import { MdCode, MdFormatColorText, MdOutlineEmojiEmotions } from 'react-icons/md'
2+
import { MdCode, MdFormatColorText, MdOutlineEmojiEmotions, MdOutlineAdd } from 'react-icons/md'
33
import { RiAtLine, RiCodeBlock } from 'react-icons/ri'
4-
import { IoSend } from 'react-icons/io5'
4+
import { IoSend, IoCloseOutline as Close } from 'react-icons/io5'
55
import {
66
Bold,
77
Italic,
@@ -45,7 +45,9 @@ const icons: { [key: string]: React.ComponentType<{ size?: number; fill?: string
4545
MdOutlineEmojiEmotions,
4646
RiAtLine,
4747
IoSend,
48-
TbBlockquote
48+
TbBlockquote,
49+
Close,
50+
MdOutlineAdd
4951
}
5052

5153
type TIcon = {

packages/webapp/components/chat/components/ChannelActionBar.tsx

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,94 @@ import JoinBroadcastChannel from './JoinBroadcastChannel'
33
import JoinGroupChannel from './JoinGroupChannel'
44
import JoinPrivateChannel from './JoinDirectChannel'
55
import JoinDirectChannel from './JoinPrivateChannel'
6-
import { useAuthStore, useChatStore } from '@stores'
6+
import { useAuthStore, useChatStore, useStore } from '@stores'
77
import { useChannel } from '../context/ChannelProvider'
88
import SignInToJoinChannel from './SignInToJoinChannel'
99
import { TChannelSettings } from '@types'
1010
import MessageComposer from './MessageComposer/MessageComposer'
11+
import useKeyboardHeight from '@hooks/useKeyboardHeight'
12+
import { useMessageComposer } from './MessageComposer/hooks/useMessageComposer'
13+
14+
const MobileToolbar = () => {
15+
const { isOpen: isKeyboardOpen } = useKeyboardHeight()
16+
const { isToolbarOpen, toggleToolbar } = useMessageComposer()
17+
18+
if (!isKeyboardOpen) return null
19+
20+
return (
21+
<div className="relative h-10 overflow-hidden">
22+
<div
23+
className={`absolute inset-0 transition-transform duration-300 ease-in-out ${
24+
isToolbarOpen ? 'translate-y-0' : '-translate-y-full'
25+
}`}>
26+
<MessageComposer.Actions>
27+
<MessageComposer.ToggleToolbarButton />
28+
<MessageComposer.EmojiButton />
29+
<MessageComposer.MentionButton />
30+
<MessageComposer.SendButton />
31+
</MessageComposer.Actions>
32+
</div>
33+
34+
<div
35+
className={`absolute inset-0 transition-transform duration-300 ease-in-out ${
36+
isToolbarOpen ? 'translate-y-full' : 'translate-y-0'
37+
}`}>
38+
<MessageComposer.Toolbar className="bg-base-200 h-full border-b p-2 px-1">
39+
<MessageComposer.ToggleToolbarButton
40+
iconType="Close"
41+
onPress={toggleToolbar}
42+
size={22}
43+
className="btn-square !bg-gray-200"
44+
/>
45+
<div className="divided m-0 w-0" />
46+
<div className="flex snap-x items-center gap-1 overflow-x-scroll overflow-y-hidden">
47+
<MessageComposer.BoldButton size={10} className="snap-center" />
48+
<MessageComposer.ItalicButton size={10} className="snap-center" />
49+
<MessageComposer.StrikethroughButton size={14} className="snap-center" />
50+
<div className="divided snap-center" />
51+
<MessageComposer.HyperlinkButton size={18} className="snap-center" />
52+
<MessageComposer.BulletListButton size={16} className="snap-center" />
53+
<MessageComposer.OrderedListButton size={16} className="snap-center" />
54+
<div className="divided snap-center" />
55+
<MessageComposer.BlockquoteButton size={20} className="snap-center" />
56+
<MessageComposer.CodeButton size={20} className="snap-center" />
57+
<MessageComposer.CodeBlockButton size={20} className="snap-center" />
58+
</div>
59+
</MessageComposer.Toolbar>
60+
</div>
61+
</div>
62+
)
63+
}
1164

1265
const SendMessage = () => {
66+
const {
67+
settings: {
68+
editor: { isMobile }
69+
}
70+
} = useStore((state) => state)
71+
72+
if (isMobile)
73+
return (
74+
<div className="chat_editor_container flex w-full flex-col">
75+
<MessageComposer className="rounded-t-md border border-b-0 border-gray-300 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]">
76+
<MessageComposer.MobileWrapper>
77+
<MessageComposer.Context>
78+
<MessageComposer.ReplyContext />
79+
<MessageComposer.EditContext />
80+
<MessageComposer.CommentContext />
81+
</MessageComposer.Context>
82+
83+
<div className="flex flex-row items-center gap-2 px-2 py-1.5">
84+
<MessageComposer.AttachmentButton size={22} className="btn-square bg-gray-200" />
85+
<MessageComposer.Input />
86+
</div>
87+
<MobileToolbar />
88+
</MessageComposer.MobileWrapper>
89+
</MessageComposer>
90+
</div>
91+
)
1392
return (
14-
<MessageComposer className="chat_editor_container mb-2 flex w-full flex-col">
93+
<MessageComposer className="chat_editor_container m-auto mb-2 flex w-[98%] flex-col">
1594
<MessageComposer.Context>
1695
<MessageComposer.ReplyContext />
1796
<MessageComposer.EditContext />

packages/webapp/components/chat/components/MessageComposer/MessageComposer.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { messageInsert } from '../../hooks/listner/helpers'
2929

3030
import {
3131
Actions,
32+
AttachmentButton,
3233
EmojiButton,
3334
MentionButton,
3435
SendButton,
@@ -40,6 +41,7 @@ import { twMerge } from 'tailwind-merge'
4041
import ReplyContext from './components/Context/ReplyContext'
4142
import EditContext from './components/Context/EditContext'
4243
import CommentContext from './components/Context/CommentContext'
44+
import { MobileWrapper } from './Mobile'
4345

4446
const MessageComposer = ({
4547
children,
@@ -49,15 +51,15 @@ const MessageComposer = ({
4951
className?: string
5052
}) => {
5153
const { channelId } = useChannel()
54+
5255
const user = useAuthStore((state) => state.profile)
5356
const setOrUpdateUserPresence = useChatStore((state: any) => state.setOrUpdateUserPresence)
5457
const usersPresence = useStore((state: any) => state.usersPresence)
5558
const startThreadMessage = useChatStore((state) => state.startThreadMessage)
5659
const channels = useChatStore((state) => state.channels)
5760
const { workspaceId } = useChatStore((state) => state.workspaceSettings)
5861
const editorRef = useRef<HTMLDivElement | null>(null)
59-
60-
const [toggleToolbar, setToggleToolbar] = useState(() => toolbarStorage.get())
62+
const [isToolbarOpen, setIsToolbarOpen] = useState(() => toolbarStorage.get())
6163

6264
const setEditMsgMemory = useChatStore((state) => state.setEditMessageMemory)
6365
const setReplyMsgMemory = useChatStore((state) => state.setReplyMessageMemory)
@@ -388,11 +390,6 @@ const MessageComposer = ({
388390
setAttributes()
389391
}, [editor])
390392

391-
// Save toolbar toggle state to localStorage
392-
useEffect(() => {
393-
toolbarStorage.set(toggleToolbar)
394-
}, [toggleToolbar])
395-
396393
// Handle Draft Memory
397394
useEffect(() => {
398395
return () => {
@@ -427,6 +424,15 @@ const MessageComposer = ({
427424
}, 1000)
428425
}, [editor])
429426

427+
// Save toolbar toggle state to localStorage
428+
useEffect(() => {
429+
toolbarStorage.set(isToolbarOpen)
430+
}, [isToolbarOpen])
431+
432+
const toggleToolbar = useCallback(() => {
433+
setIsToolbarOpen(!isToolbarOpen)
434+
}, [setIsToolbarOpen, isToolbarOpen])
435+
430436
const contextValue = {
431437
sendMsg,
432438
sendComment,
@@ -448,8 +454,8 @@ const MessageComposer = ({
448454
setReplyMsgMemory,
449455
setCommentMsgMemory,
450456
contextType,
457+
isToolbarOpen,
451458
toggleToolbar,
452-
setToggleToolbar,
453459
submitMessage,
454460
editorRef
455461
}
@@ -490,6 +496,10 @@ MessageComposer.EmojiButton = EmojiButton
490496
MessageComposer.MentionButton = MentionButton
491497
MessageComposer.SendButton = SendButton
492498
MessageComposer.ToggleToolbarButton = ToggleToolbarButton
499+
MessageComposer.AttachmentButton = AttachmentButton
493500

494501
// Input
495502
MessageComposer.Input = Input
503+
504+
// Mobile
505+
MessageComposer.MobileWrapper = MobileWrapper
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { useSheetStore, useStore } from '@stores'
2+
import { useEffect } from 'react'
3+
import useKeyboardHeight from '@hooks/useKeyboardHeight'
4+
import { applyStyles } from '../helpers/domUtils'
5+
6+
type Props = {
7+
children: React.ReactNode
8+
}
9+
10+
const SHEET_STYLES = {
11+
keyboardOpen: {
12+
height: '100%',
13+
borderRadius: '0'
14+
},
15+
keyboardClosed: {
16+
height: 'calc(100% - env(safe-area-inset-top) - 34px)',
17+
borderTopLeftRadius: '8px',
18+
borderTopRightRadius: '8px'
19+
}
20+
} as const
21+
22+
export const MobileWrapper = ({ children }: Props) => {
23+
const { sheetContainerRef } = useSheetStore()
24+
const { isOpen: isKeyboardOpen } = useKeyboardHeight()
25+
const {
26+
settings: {
27+
editor: { isMobile }
28+
}
29+
} = useStore((state) => state)
30+
31+
useEffect(() => {
32+
if (!sheetContainerRef || !isMobile) return
33+
34+
const styles = isKeyboardOpen ? SHEET_STYLES.keyboardOpen : SHEET_STYLES.keyboardClosed
35+
applyStyles(sheetContainerRef, styles)
36+
}, [isMobile, isKeyboardOpen, sheetContainerRef])
37+
38+
return <div>{children}</div>
39+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './MobileWrapper'
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Icon from '@components/TipTap/toolbar/Icon'
2+
import Button from '../../ui/Button'
3+
4+
type Props = {
5+
size?: number
6+
} & React.ComponentProps<typeof Button>
7+
8+
export const AttachmentButton = ({ size = 16, ...props }: Props) => {
9+
return (
10+
<Button type="attachment" tooltip="Attachment" {...props}>
11+
<Icon type="MdOutlineAdd" size={size} />
12+
</Button>
13+
)
14+
}
Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
import Button from '../../ui/Button'
22
import Icon from '@components/TipTap/toolbar/Icon'
33
import { useMessageComposer } from '../../../hooks/useMessageComposer'
4-
type Props = React.HTMLAttributes<HTMLButtonElement> & {
5-
className?: string
4+
5+
interface Props extends React.ComponentProps<typeof Button> {
66
size?: number
7+
iconType?: string
78
}
89

9-
export const ToggleToolbarButton = ({ className, size = 20, ...props }: Props) => {
10-
const { toggleToolbar, setToggleToolbar } = useMessageComposer()
10+
export const ToggleToolbarButton = ({
11+
className,
12+
size = 20,
13+
iconType = 'MdFormatColorText',
14+
...props
15+
}: Props) => {
16+
const { isToolbarOpen, toggleToolbar } = useMessageComposer()
1117

1218
return (
1319
<Button
1420
className={className}
15-
onPress={() => setToggleToolbar(!toggleToolbar)}
21+
onPress={toggleToolbar}
1622
tooltip="Toolbar"
1723
tooltipPosition="tooltip-top"
18-
isActive={!toggleToolbar}
24+
isActive={!isToolbarOpen}
1925
{...props}>
20-
<Icon type="MdFormatColorText" size={size} />
26+
<Icon type={iconType} size={size} />
2127
</Button>
2228
)
2329
}

packages/webapp/components/chat/components/MessageComposer/components/Actions/ActionButtons/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './EmojiButton'
22
export * from './MentionButton'
33
export * from './SendButton'
44
export * from './ToggleToolbarButton'
5+
export * from './AttachmentButton'

packages/webapp/components/chat/components/MessageComposer/components/Input/Input.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { EditorContent } from '@tiptap/react'
22
import { useMessageComposer } from '../../hooks/useMessageComposer'
33
import { useEffect } from 'react'
4+
import { twMerge } from 'tailwind-merge'
45

5-
export const Input = () => {
6+
export const Input = ({ className }: { className?: string }) => {
67
const { editorRef, editor, messageDraftMemory } = useMessageComposer()
78

89
useEffect(() => {
@@ -11,7 +12,7 @@ export const Input = () => {
1112
}
1213
}, [messageDraftMemory, editor])
1314
return (
14-
<div className="flex-1 px-1 py-2 text-base sm:px-2">
15+
<div className={twMerge('flex-1 px-1 py-2 text-base sm:px-2', className)}>
1516
<EditorContent
1617
ref={editorRef}
1718
className="max-h-52 w-full overflow-auto"

0 commit comments

Comments
 (0)