feat(chat): add image messages and enforce readonly sessions
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { ArrowLeft, Lock, Send } from "lucide-react"
|
import { ArrowLeft, ImagePlus, Lock, Send } from "lucide-react"
|
||||||
|
import Image from "next/image"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { use, useState } from "react"
|
import { use, useRef, useState } from "react"
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -17,7 +18,9 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
|
|||||||
const session = useChatStore((state) => state.sessions.find((item) => item.id === id))
|
const session = useChatStore((state) => state.sessions.find((item) => item.id === id))
|
||||||
const messages = useChatStore((state) => state.messages.filter((item) => item.sessionId === id))
|
const messages = useChatStore((state) => state.messages.filter((item) => item.sessionId === id))
|
||||||
const sendTextMessage = useChatStore((state) => state.sendTextMessage)
|
const sendTextMessage = useChatStore((state) => state.sendTextMessage)
|
||||||
|
const sendImageMessage = useChatStore((state) => state.sendImageMessage)
|
||||||
const [input, setInput] = useState("")
|
const [input, setInput] = useState("")
|
||||||
|
const imageInputRef = useRef<HTMLInputElement>(null)
|
||||||
const { user } = useAuthStore()
|
const { user } = useAuthStore()
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@@ -77,14 +80,25 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
|
|||||||
<AvatarFallback>{msg.senderName[0]}</AvatarFallback>
|
<AvatarFallback>{msg.senderName[0]}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className={cn("max-w-[70%]", isMine && "text-right")}>
|
<div className={cn("max-w-[70%]", isMine && "text-right")}>
|
||||||
<p
|
{msg.type === "image" ? (
|
||||||
className={cn(
|
<Image
|
||||||
"inline-block rounded-lg px-3 py-2 text-sm",
|
src={msg.content}
|
||||||
isMine ? "bg-primary text-primary-foreground" : "bg-muted",
|
alt="聊天图片"
|
||||||
)}
|
width={256}
|
||||||
>
|
height={192}
|
||||||
{msg.content}
|
unoptimized
|
||||||
</p>
|
className="inline-block rounded-lg max-h-48 max-w-64 border"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"inline-block rounded-lg px-3 py-2 text-sm",
|
||||||
|
isMine ? "bg-primary text-primary-foreground" : "bg-muted",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{msg.content}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<p className="text-[10px] text-muted-foreground mt-1">
|
<p className="text-[10px] text-muted-foreground mt-1">
|
||||||
{new Date(msg.createdAt).toLocaleTimeString("zh-CN", {
|
{new Date(msg.createdAt).toLocaleTimeString("zh-CN", {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
@@ -100,6 +114,27 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
|
|||||||
|
|
||||||
{!session.readonly ? (
|
{!session.readonly ? (
|
||||||
<div className="border-t p-4">
|
<div className="border-t p-4">
|
||||||
|
<input
|
||||||
|
ref={imageInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={(event) => {
|
||||||
|
const file = event.target.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
const sender = session.participants.find((participant) => participant.id === userId)
|
||||||
|
sendImageMessage(
|
||||||
|
session.id,
|
||||||
|
{
|
||||||
|
id: userId,
|
||||||
|
name: sender?.name ?? user?.nickname ?? "",
|
||||||
|
avatar: sender?.avatar ?? user?.avatar ?? "",
|
||||||
|
},
|
||||||
|
URL.createObjectURL(file),
|
||||||
|
)
|
||||||
|
event.target.value = ""
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<form
|
<form
|
||||||
className="flex gap-2 max-w-2xl mx-auto"
|
className="flex gap-2 max-w-2xl mx-auto"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
@@ -126,6 +161,14 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
|
|||||||
placeholder="输入消息..."
|
placeholder="输入消息..."
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => imageInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<ImagePlus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
<Button type="submit" size="icon" disabled={!input.trim()}>
|
<Button type="submit" size="icon" disabled={!input.trim()}>
|
||||||
<Send className="h-4 w-4" />
|
<Send className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface ChatState {
|
|||||||
messages: ChatMessage[]
|
messages: ChatMessage[]
|
||||||
ensureOrderSession: (order: Order) => ChatSession
|
ensureOrderSession: (order: Order) => ChatSession
|
||||||
sendTextMessage: (sessionId: string, sender: Sender, content: string) => void
|
sendTextMessage: (sessionId: string, sender: Sender, content: string) => void
|
||||||
|
sendImageMessage: (sessionId: string, sender: Sender, imageUrl: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveAvatar(userId: string) {
|
function resolveAvatar(userId: string) {
|
||||||
@@ -74,6 +75,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
sendTextMessage: (sessionId, sender, content) => {
|
sendTextMessage: (sessionId, sender, content) => {
|
||||||
const text = content.trim()
|
const text = content.trim()
|
||||||
if (!text) return
|
if (!text) return
|
||||||
|
const session = get().sessions.find((item) => item.id === sessionId)
|
||||||
|
if (!session || session.readonly) return
|
||||||
|
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
const message: ChatMessage = {
|
const message: ChatMessage = {
|
||||||
@@ -100,4 +103,35 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
sendImageMessage: (sessionId, sender, imageUrl) => {
|
||||||
|
const content = imageUrl.trim()
|
||||||
|
if (!content) return
|
||||||
|
const session = get().sessions.find((item) => item.id === sessionId)
|
||||||
|
if (!session || session.readonly) return
|
||||||
|
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
const message: ChatMessage = {
|
||||||
|
id: generateId("msg"),
|
||||||
|
sessionId,
|
||||||
|
senderId: sender.id,
|
||||||
|
senderName: sender.name,
|
||||||
|
senderAvatar: sender.avatar,
|
||||||
|
type: "image",
|
||||||
|
content,
|
||||||
|
createdAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
set((state) => ({
|
||||||
|
messages: [...state.messages, message],
|
||||||
|
sessions: state.sessions.map((item) =>
|
||||||
|
item.id === sessionId
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
lastMessage: "[图片]",
|
||||||
|
lastMessageAt: now,
|
||||||
|
}
|
||||||
|
: item,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user