big update: added reactions, groups, media share
This commit is contained in:
@@ -7,7 +7,7 @@ import { useAuth } from '../auth/AuthContext'
|
||||
import { useConversations } from '../hooks/useConversations'
|
||||
import { useMessages } from '../hooks/useMessages'
|
||||
import { useRealtime } from '../hooks/useRealtime'
|
||||
import type { ClientMessage, Message, User, WsEvent } from '../lib/types'
|
||||
import type { ClientMessage, Media, Member, Message, User, WsEvent } from '../lib/types'
|
||||
|
||||
interface ChatAppProps {
|
||||
user: User
|
||||
@@ -22,8 +22,9 @@ export default function ChatApp({ user }: ChatAppProps) {
|
||||
const [typing, setTyping] = useState<Record<number, boolean>>({})
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [focusTick, setFocusTick] = useState(0)
|
||||
const [membersVersion, setMembersVersion] = useState(0)
|
||||
|
||||
const { conversations, loading, error, reload, add, patch } = useConversations()
|
||||
const { conversations, loading, error, reload, add, remove, patch } = useConversations()
|
||||
const {
|
||||
messages,
|
||||
loading: messagesLoading,
|
||||
@@ -32,6 +33,7 @@ export default function ChatApp({ user }: ChatAppProps) {
|
||||
error: messagesError,
|
||||
loadOlder,
|
||||
upsert,
|
||||
patchMessage,
|
||||
addOptimistic,
|
||||
markSending,
|
||||
failOptimistic,
|
||||
@@ -96,8 +98,16 @@ export default function ChatApp({ user }: ChatAppProps) {
|
||||
(event: WsEvent) => {
|
||||
switch (event.type) {
|
||||
case 'message.created': {
|
||||
const { temp_id, conversation_id, message_id, sender_id, body, created_at } = event.data
|
||||
const message: Message = { id: message_id, conversation_id, sender_id, body, created_at }
|
||||
const { temp_id, conversation_id, message_id, sender_id, body, created_at, reactions, media } = event.data
|
||||
const message: Message = {
|
||||
id: message_id,
|
||||
conversation_id,
|
||||
sender_id,
|
||||
body,
|
||||
created_at,
|
||||
reactions: reactions ?? [],
|
||||
media: media ?? null,
|
||||
}
|
||||
if (conversation_id === selectedIdRef.current || sender_id === user.id) {
|
||||
upsert(message, temp_id)
|
||||
}
|
||||
@@ -146,23 +156,64 @@ export default function ChatApp({ user }: ChatAppProps) {
|
||||
if (conversation_id === selectedIdRef.current) markReadUpTo(message_id)
|
||||
break
|
||||
}
|
||||
case 'reaction.added': {
|
||||
const { conversation_id, message_id, user_id, emoji } = event.data
|
||||
if (conversation_id !== selectedIdRef.current) break
|
||||
patchMessage(message_id, (message) => {
|
||||
if (message.reactions.some((reaction) => reaction.emoji === emoji && reaction.user_id === user_id)) {
|
||||
return message
|
||||
}
|
||||
return { ...message, reactions: [...message.reactions, { emoji, user_id }] }
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'reaction.removed': {
|
||||
const { conversation_id, message_id, user_id, emoji } = event.data
|
||||
if (conversation_id !== selectedIdRef.current) break
|
||||
patchMessage(message_id, (message) => ({
|
||||
...message,
|
||||
reactions: message.reactions.filter(
|
||||
(reaction) => !(reaction.emoji === emoji && reaction.user_id === user_id),
|
||||
),
|
||||
}))
|
||||
break
|
||||
}
|
||||
case 'member.added':
|
||||
case 'member.removed': {
|
||||
if (event.data.conversation_id === selectedIdRef.current) {
|
||||
setMembersVersion((version) => version + 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'conversation.added': {
|
||||
void reload()
|
||||
break
|
||||
}
|
||||
case 'conversation.removed': {
|
||||
const { conversation_id } = event.data
|
||||
remove(conversation_id)
|
||||
if (selectedIdRef.current === conversation_id) setSelectedId(null)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
[markReadUpTo, patch, reload, upsert, user.id],
|
||||
[markReadUpTo, patch, patchMessage, reload, remove, upsert, user.id],
|
||||
)
|
||||
|
||||
const realtime = useRealtime(handleEvent)
|
||||
|
||||
const sendMessage = useCallback(
|
||||
async (conversationId: number, body: string) => {
|
||||
async (conversationId: number, body: string, media: Media | null) => {
|
||||
const text = body.trim()
|
||||
if (!text) return
|
||||
if (!text && !media) return
|
||||
const tempId = crypto.randomUUID()
|
||||
const optimistic: ClientMessage = {
|
||||
id: -Date.now(),
|
||||
conversation_id: conversationId,
|
||||
sender_id: user.id,
|
||||
body: text,
|
||||
reactions: [],
|
||||
media,
|
||||
created_at: new Date().toISOString(),
|
||||
temp_id: tempId,
|
||||
status: 'sending',
|
||||
@@ -170,7 +221,7 @@ export default function ChatApp({ user }: ChatAppProps) {
|
||||
addOptimistic(optimistic)
|
||||
patch(conversationId, (conversation) => ({ ...conversation, last_message: optimistic, unread_count: 0 }))
|
||||
try {
|
||||
const result = await api.sendMessage(conversationId, text, tempId)
|
||||
const result = await api.sendMessage(conversationId, text, tempId, media?.id)
|
||||
upsert(result.message, tempId)
|
||||
patch(conversationId, (conversation) => ({
|
||||
...conversation,
|
||||
@@ -190,7 +241,7 @@ export default function ChatApp({ user }: ChatAppProps) {
|
||||
if (!tempId) return
|
||||
markSending(tempId)
|
||||
try {
|
||||
const result = await api.sendMessage(message.conversation_id, message.body, tempId)
|
||||
const result = await api.sendMessage(message.conversation_id, message.body, tempId, message.media?.id)
|
||||
upsert(result.message, tempId)
|
||||
patch(message.conversation_id, (conversation) => ({
|
||||
...conversation,
|
||||
@@ -204,6 +255,22 @@ export default function ChatApp({ user }: ChatAppProps) {
|
||||
[failOptimistic, markSending, patch, upsert],
|
||||
)
|
||||
|
||||
const handleReaction = useCallback(
|
||||
async (message: ClientMessage, emoji: string) => {
|
||||
if (message.id <= 0) return
|
||||
const mine = message.reactions.some((reaction) => reaction.emoji === emoji && reaction.user_id === user.id)
|
||||
try {
|
||||
const updated = mine
|
||||
? await api.removeReaction(message.conversation_id, message.id, emoji)
|
||||
: await api.addReaction(message.conversation_id, message.id, emoji)
|
||||
upsert(updated)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
},
|
||||
[upsert, user.id],
|
||||
)
|
||||
|
||||
const sendTyping = useCallback(
|
||||
(conversationId: number, isTyping: boolean) => {
|
||||
realtime.sendTyping(conversationId, isTyping)
|
||||
@@ -233,6 +300,23 @@ export default function ChatApp({ user }: ChatAppProps) {
|
||||
[add, loadContacts, rememberUser],
|
||||
)
|
||||
|
||||
const handleCreateGroup = useCallback(
|
||||
async (name: string, memberIds: number[]) => {
|
||||
const conversation = await api.createGroup(name, memberIds)
|
||||
add(conversation)
|
||||
setSelectedId(conversation.id)
|
||||
setMembersVersion((version) => version + 1)
|
||||
},
|
||||
[add],
|
||||
)
|
||||
|
||||
const handleMembersLoaded = useCallback(
|
||||
(members: Member[]) => {
|
||||
members.forEach((member) => rememberUser(member.user))
|
||||
},
|
||||
[rememberUser],
|
||||
)
|
||||
|
||||
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null
|
||||
const lastMessageId = lastMessage && lastMessage.id > 0 ? lastMessage.id : null
|
||||
|
||||
@@ -264,6 +348,7 @@ export default function ChatApp({ user }: ChatAppProps) {
|
||||
/>
|
||||
|
||||
<ChatWindow
|
||||
key={selectedId ?? 'empty'}
|
||||
className={selectedId === null ? 'hidden md:flex' : 'flex'}
|
||||
conversation={conversations.find((conversation) => conversation.id === selectedId) ?? null}
|
||||
currentUser={user}
|
||||
@@ -276,11 +361,15 @@ export default function ChatApp({ user }: ChatAppProps) {
|
||||
loadingOlder={loadingOlder}
|
||||
hasMore={hasMore}
|
||||
error={messagesError}
|
||||
membersVersion={membersVersion}
|
||||
onBack={() => setSelectedId(null)}
|
||||
onSend={sendMessage}
|
||||
onTyping={sendTyping}
|
||||
onLoadOlder={() => void loadOlder()}
|
||||
onRetry={resendMessage}
|
||||
onReact={(message, emoji) => void handleReaction(message, emoji)}
|
||||
onMembersChanged={() => setMembersVersion((version) => version + 1)}
|
||||
onMembersLoaded={handleMembersLoaded}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -288,6 +377,7 @@ export default function ChatApp({ user }: ChatAppProps) {
|
||||
<NewChatDialog
|
||||
onClose={() => setDialogOpen(false)}
|
||||
onStartChat={handleStartChat}
|
||||
onCreateGroup={handleCreateGroup}
|
||||
onContactsChanged={() => void loadContacts()}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user