Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit c987cb0

Browse filesBrowse files
committed
refactor: msg card, reaction btn, card side
1 parent 8defe23 commit c987cb0
Copy full SHA for c987cb0

File tree

Expand file treeCollapse file tree

4 files changed

+189
-118
lines changed
Filter options
Expand file treeCollapse file tree

4 files changed

+189
-118
lines changed

‎packages/webapp/components/chat/MessageReaction.tsx

Copy file name to clipboardExpand all lines: packages/webapp/components/chat/MessageReaction.tsx
+50-15Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,76 @@
11
import React, { useCallback } from 'react'
22
import { twx, cn } from '@utils/index'
33
import { MdOutlineAddReaction } from 'react-icons/md'
4-
import { useAuthStore, useChatStore } from '@stores'
4+
import { useAuthStore, useChatStore, useStore } from '@stores'
55

6-
type BtnIcon = React.ComponentProps<'button'> & { $active?: boolean; $size?: number }
6+
// Define proper types
7+
interface MessageType {
8+
id: string
9+
channel_id: string
10+
user_details?: {
11+
id: string
12+
}
13+
// Add other message properties as needed
14+
}
15+
16+
interface MessageReactionProps {
17+
message: MessageType
18+
className?: string
19+
}
20+
21+
interface UserProfile {
22+
id: string
23+
// Add other user profile properties as needed
24+
}
25+
26+
// Move button component type and definition outside of the main component
27+
type IconButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
28+
$active?: boolean
29+
$size?: number
30+
}
731

8-
const IconButton = twx.button<BtnIcon>((prop) =>
32+
const IconButton = twx.button<IconButtonProps>((prop) =>
933
cn(
10-
'btn btn-ghost w-8 h-8 btn-xs p-1 mr-2',
34+
'btn btn-ghost btn-square w-8 h-8 btn-xs p-1 mr-2',
1135
prop.$active && 'btn-active',
1236
prop.$size && `w-${prop.$size} h-${prop.$size}`
1337
)
1438
)
1539

16-
export default function MessageReaction({ message }: any) {
17-
const user = useAuthStore((state: any) => state.profile)
40+
export default function MessageReaction({ message, className }: MessageReactionProps) {
41+
const user = useAuthStore((state) => state.profile as UserProfile)
1842
const member = useChatStore((state) => state.channelMembers.get(message.channel_id))
43+
const {
44+
settings: {
45+
editor: { isMobile }
46+
}
47+
} = useStore((state) => state)
1948

20-
// Allow users who are members of the channel to react to the message.
49+
// User can only react if they are a member of the channel
50+
const canUserReact = user && member?.get(user?.id)
2151

2252
const openEmojiPicker = useCallback(
23-
(clickEvent: any) => {
24-
const event = new CustomEvent('toggelEmojiPicker', {
25-
detail: { clickEvent: clickEvent, message, type: 'react2Message' }
53+
(event: React.MouseEvent) => {
54+
const customEvent = new CustomEvent('toggelEmojiPicker', {
55+
detail: {
56+
clickEvent: event,
57+
message,
58+
type: 'react2Message'
59+
}
2660
})
27-
document.dispatchEvent(event)
61+
document.dispatchEvent(customEvent)
2862
},
2963
[message]
3064
)
3165

32-
if (user && !member?.get(user?.id)) return null
66+
if (!canUserReact) return null
67+
68+
const isCurrentUserMessage = message?.user_details?.id === user?.id
69+
const dropdownPosition = isCurrentUserMessage && isMobile ? 'dropdown-left ' : 'dropdown-right '
3370

3471
return (
3572
<div
36-
className={`dropdown dropdown-end dropdown-bottom absolute bottom-1 ${
37-
message?.user_details?.id === user?.id ? 'dropdown-left -left-5' : 'dropdown-right -right-7'
38-
} hidden group-hover/msgcard:block`}>
73+
className={`dropdown dropdown-end dropdown-bottom ${dropdownPosition} invisible group-hover/msgcard:visible ${className}`}>
3974
<IconButton onClick={openEmojiPicker}>
4075
<MdOutlineAddReaction size={24} />
4176
</IconButton>

‎packages/webapp/components/chat/components/chatContainer/MessageCard.tsx

Copy file name to clipboardExpand all lines: packages/webapp/components/chat/components/chatContainer/MessageCard.tsx
+75-59Lines changed: 75 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
22
import { TMessageWithUser } from '@api'
33
import { MessageContextMenu } from '../../MessageContextMenu'
4-
import MessageReaction from '../../MessageReaction'
54
import { Avatar } from '@components/ui/Avatar'
6-
import { useAuthStore, useChatStore } from '@stores'
5+
import { useAuthStore, useChatStore, useStore } from '@stores'
76
import MessageFooter from './MessageFooter'
87
import MessageHeader from './MessageHeader'
98
import MessageContent from './MessageContent'
@@ -18,17 +17,35 @@ type TMessageCardProps = {
1817
selectedEmoji: any
1918
}
2019

20+
// Define the extended HTMLDivElement interface with our custom properties
21+
interface MessageCardElement extends HTMLDivElement {
22+
msgId?: string
23+
readedAt?: string | null
24+
createdAt?: string | null
25+
}
26+
2127
function MessageCard({ data, toggleEmojiPicker, selectedEmoji }: TMessageCardProps, ref: any) {
2228
const { settings } = useChannel()
2329
const user = useAuthStore.use.profile()
2430
const setReplayMessageMemory = useChatStore((state) => state.setReplayMessageMemory)
25-
const cardRef = useRef<any>(null)
31+
const cardRef = useRef<MessageCardElement>(null)
32+
33+
const {
34+
settings: {
35+
editor: { isMobile }
36+
}
37+
} = useStore((state) => state)
2638

39+
const isGroupEnd = data.isGroupEnd
40+
const isOwnerMessage = data?.user_details?.id === user?.id
41+
const isEmojiOnlyMessage = isOnlyEmoji(data?.content)
42+
43+
// Attach ref and message data to DOM element
2744
useEffect(() => {
2845
if (ref) {
2946
ref.current = cardRef.current
3047
}
31-
// Attach the data.id to the cardRef directly
48+
3249
if (cardRef.current) {
3350
cardRef.current.msgId = data.id
3451
cardRef.current.readedAt = data.readed_at
@@ -38,73 +55,72 @@ function MessageCard({ data, toggleEmojiPicker, selectedEmoji }: TMessageCardPro
3855

3956
const handleDoubleClick = useCallback(() => {
4057
if (!settings.contextMenue?.reply) return
41-
setReplayMessageMemory(data.channel_id, data)
42-
// Triggering editor focus if needed
43-
const event = new CustomEvent('editor:focus')
44-
document.dispatchEvent(event)
45-
}, [data])
4658

47-
const isGroupEnd = useMemo(() => {
48-
return data.isGroupEnd
49-
}, [data.isGroupEnd])
59+
setReplayMessageMemory(data.channel_id, data)
5060

51-
const ownerMsg = useMemo(() => {
52-
return data?.user_details?.id === user?.id
53-
}, [data?.user_details?.id, user])
61+
// Trigger editor focus
62+
document.dispatchEvent(new CustomEvent('editor:focus'))
63+
}, [data, settings.contextMenue?.reply, setReplayMessageMemory])
5464

55-
return (
56-
<div
57-
className={`group/msgcard chat my-0.5 ${
58-
ownerMsg ? 'owner chat-end ml-auto' : 'chat-start mr-auto'
59-
} msg_card relative w-fit max-w-[90%] min-w-[80%] sm:min-w-[250px] ${isGroupEnd ? 'chat_group-end !mb-2' : 'chat_group-start'}`}
60-
ref={cardRef}
61-
onDoubleClick={handleDoubleClick}>
62-
{!ownerMsg && (
63-
<div className="avatar chat-image">
64-
<Avatar
65-
src={data?.user_details?.avatar_url}
66-
avatarUpdatedAt={data?.user_details?.avatar_updated_at}
67-
className="avatar chat-image w-10 cursor-pointer rounded-full transition-all hover:scale-105"
68-
style={{
69-
width: 40,
70-
height: 40,
71-
cursour: 'pointer',
72-
visibility: isGroupEnd ? 'visible' : 'hidden'
73-
}}
74-
id={data?.user_details?.id}
75-
alt={`avatar_${data?.user_details?.id}`}
76-
/>
77-
</div>
78-
)}
65+
const renderAvatar = () => {
66+
// On desktop, always show avatar; on mobile, only show for non-owner messages
67+
if (isMobile && isOwnerMessage) return null
7968

80-
<MessageHeader data={data} ownerMsg={ownerMsg} />
69+
return (
70+
<div className="avatar chat-image">
71+
<Avatar
72+
src={data?.user_details?.avatar_url}
73+
avatarUpdatedAt={data?.user_details?.avatar_updated_at}
74+
className="avatar chat-image w-10 cursor-pointer rounded-full transition-all hover:scale-105"
75+
style={{
76+
width: 40,
77+
height: 40,
78+
cursour: 'pointer',
79+
visibility: isGroupEnd ? 'visible' : 'hidden'
80+
}}
81+
id={data?.user_details?.id}
82+
alt={`avatar_${data?.user_details?.id}`}
83+
/>
84+
</div>
85+
)
86+
}
8187

82-
{isOnlyEmoji(data?.content) ? (
88+
const renderMessageContent = () => {
89+
if (isEmojiOnlyMessage) {
90+
return (
8391
<div className="mb-4 max-w-[70%] min-w-full">
8492
{data.reply_to_message_id && <MsgReplyTo data={data} />}
85-
8693
<MessageContent data={data} />
8794
<MessageFooter data={data} />
8895
</div>
89-
) : (
90-
<div
91-
className={`chat-bubble !mt-0 flex w-full flex-col border-2 border-transparent p-2 ${ownerMsg ? 'bg-chatBubble-owner' : 'bg-white drop-shadow'} ${
92-
isGroupEnd ? 'bubble_group-end' : 'bubble_group-start !rounded-ee-xl !rounded-es-xl'
93-
}`}>
94-
{data.metadata?.comment && <MsgComment data={data} />}
95-
{data.reply_to_message_id && <MsgReplyTo data={data} />}
96+
)
97+
}
9698

97-
<MessageContent data={data} />
98-
<MessageFooter data={data} />
99-
</div>
100-
)}
101-
102-
<MessageReaction
103-
message={data}
104-
selectedEmoji={selectedEmoji}
105-
toggleEmojiPicker={toggleEmojiPicker}
106-
/>
99+
return (
100+
<div
101+
className={`chat-bubble !mt-0 flex w-full flex-col border-2 border-transparent p-2 ${
102+
isOwnerMessage ? 'bg-chatBubble-owner' : 'bg-white drop-shadow'
103+
} ${isGroupEnd ? 'bubble_group-end' : 'bubble_group-start !rounded-ee-xl !rounded-es-xl'}`}>
104+
{data.metadata?.comment && <MsgComment data={data} />}
105+
{data.reply_to_message_id && <MsgReplyTo data={data} />}
106+
<MessageContent data={data} />
107+
<MessageFooter data={data} />
108+
</div>
109+
)
110+
}
107111

112+
return (
113+
<div
114+
className={`group/msgcard chat my-0.5 ${
115+
isOwnerMessage && isMobile ? 'owner chat-end ml-auto' : 'chat-start mr-auto'
116+
} msg_card relative w-fit max-w-[90%] min-w-[80%] sm:min-w-[250px] ${
117+
isGroupEnd ? 'chat_group-end !mb-2' : 'chat_group-start'
118+
}`}
119+
ref={cardRef}
120+
onDoubleClick={handleDoubleClick}>
121+
{renderAvatar()}
122+
<MessageHeader data={data} ownerMsg={isOwnerMessage} />
123+
{renderMessageContent()}
108124
<MessageContextMenu
109125
parrentRef={cardRef}
110126
messageData={data}

‎packages/webapp/components/chat/components/chatContainer/MessageFooter.tsx

Copy file name to clipboardExpand all lines: packages/webapp/components/chat/components/chatContainer/MessageFooter.tsx
+40-23Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import ReactionsCard from './ReactionsCard'
66
import { IoCheckmarkDoneSharp, IoTimeOutline, IoCheckmarkSharp } from 'react-icons/io5'
77
import { BiSolidMessageDetail } from 'react-icons/bi'
88
import { TMessageWithUser } from '@api'
9+
import MessageReaction from '@components/chat/MessageReaction'
910

1011
// Helper function to format date
1112
const formatDate = (dateString: string) => {
@@ -25,10 +26,10 @@ const ReplyIndicator = ({ count }: { count: number | undefined }) =>
2526
) : null
2627

2728
const PinIndicator = ({ isPinned }: { isPinned?: boolean }) =>
28-
isPinned ? <TbPinned className="size-4 rotate-45 text-base-content" /> : null
29+
isPinned ? <TbPinned className="text-base-content size-4 rotate-45" /> : null
2930

3031
const EditedIndicator = ({ isEdited }: { isEdited?: boolean }) =>
31-
isEdited ? <span className="text-xs text-base-content text-opacity-50">edited</span> : null
32+
isEdited ? <span className="text-base-content text-opacity-50 text-xs">edited</span> : null
3233

3334
const Timestamp = ({
3435
time,
@@ -40,12 +41,12 @@ const Timestamp = ({
4041
id: string | null
4142
}) => {
4243
return (
43-
<div className="flex space-x-1 rounded bg-base-100/10 px-1">
44-
<time className="whitespace-nowrap text-xs opacity-50">{time}</time>
45-
{id === 'fake_id' && <IoTimeOutline className="size-4 text-base-content" />}
44+
<div className="bg-base-100/10 flex space-x-1 rounded px-1">
45+
<time className="text-xs whitespace-nowrap opacity-50">{time}</time>
46+
{id === 'fake_id' && <IoTimeOutline className="text-base-content size-4" />}
4647
<div className={id !== 'fake_id' ? 'block' : 'hidden'}>
47-
{!readed_at ? <IoCheckmarkSharp className="size-4 text-base-content" /> : null}
48-
{readed_at ? <IoCheckmarkDoneSharp className="size-4 text-base-content" /> : null}
48+
{!readed_at ? <IoCheckmarkSharp className="text-base-content size-4" /> : null}
49+
{readed_at ? <IoCheckmarkDoneSharp className="text-base-content size-4" /> : null}
4950
</div>
5051
</div>
5152
)
@@ -54,9 +55,28 @@ const Timestamp = ({
5455
const ThreadIndicator = ({ count }: { count: number | null }) => {
5556
if (!count) return
5657
return (
57-
<div className="flex items-center justify-between space-x-1 rounded bg-base-100/10 px-1">
58-
<BiSolidMessageDetail className="size-4 text-base-content" />
59-
{count && <span className="whitespace-nowrap pb-[3px] text-xs opacity-100">{count}</span>}
58+
<div className="bg-base-100/10 flex items-center justify-between space-x-1 rounded px-1">
59+
<BiSolidMessageDetail className="text-base-content size-4" />
60+
{count && <span className="pb-[3px] text-xs whitespace-nowrap opacity-100">{count}</span>}
61+
</div>
62+
)
63+
}
64+
65+
const CardIndicators = ({ data }: { data: TMessageWithUser }) => {
66+
const countRepliedMessages = data.metadata?.replied?.length
67+
const createdAt = formatDate(data.created_at)
68+
return (
69+
<div className={`chat-footer text-base-content mt-1 flex items-center justify-end gap-2`}>
70+
<MessageReaction message={data} className="mr-auto" />
71+
<div className="flex items-center gap-2">
72+
<ReplyIndicator count={countRepliedMessages} />
73+
<PinIndicator isPinned={data.metadata?.pinned} />
74+
<EditedIndicator isEdited={!!data.edited_at} />
75+
<Timestamp time={createdAt} readed_at={data.readed_at} id={data.id} />
76+
</div>
77+
{data.is_thread_root && (
78+
<ThreadIndicator count={data.metadata?.thread?.message_count ?? null} />
79+
)}
6080
</div>
6181
)
6282
}
@@ -67,20 +87,17 @@ const MessageFooter: React.FC<{ data: TMessageWithUser }> = ({ data }) => {
6787

6888
return (
6989
<>
70-
<div className="chat-footer mt-1 flex items-center justify-end gap-2 pl-4 text-base-content">
71-
<div className="flex items-center gap-2">
72-
<ReplyIndicator count={countRepliedMessages} />
73-
<PinIndicator isPinned={data.metadata?.pinned} />
74-
<EditedIndicator isEdited={!!data.edited_at} />
75-
<Timestamp time={createdAt} readed_at={data.readed_at} id={data.id} />
76-
</div>
77-
{data.is_thread_root && (
78-
<ThreadIndicator count={data.metadata?.thread?.message_count ?? null} />
79-
)}
80-
</div>
90+
{!data.reactions && <CardIndicators data={data} />}
8191
{data.reactions && (
82-
<div className="mt-1">
83-
<ReactionsCard reactions={data.reactions} message={data} />
92+
<div className="">
93+
<ReactionsCard reactions={data.reactions} message={data}>
94+
<div className="flex items-center gap-2">
95+
<ReplyIndicator count={countRepliedMessages} />
96+
<PinIndicator isPinned={data.metadata?.pinned} />
97+
<EditedIndicator isEdited={!!data.edited_at} />
98+
<Timestamp time={createdAt} readed_at={data.readed_at} id={data.id} />
99+
</div>
100+
</ReactionsCard>
84101
</div>
85102
)}
86103
</>

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.