diff --git a/app/components/thread/MessageBubble.tsx b/app/components/thread/MessageBubble.tsx index 2f12749..8ee236e 100644 --- a/app/components/thread/MessageBubble.tsx +++ b/app/components/thread/MessageBubble.tsx @@ -15,9 +15,17 @@ import { Button } from "../ui/button"; import { useState, useRef, useEffect } from "react"; // Message bubble component +// +// isCurrentUser: whether this message belongs to "our side" of the chat. +// In seller-side threads that includes coworkers, not just yourself. +// isCoworker: this message was written by a teammate (owner or staff) other +// than the viewer. We still put it on the right side, but we surface the +// sender's name so you can tell who typed it — and we don't offer to +// delete it (backend only lets a user delete their own messages). export const MessageBubble = ({ message, isCurrentUser, + isCoworker = false, onReply, onDelete, replyToMessageContent, @@ -27,6 +35,7 @@ export const MessageBubble = ({ }: { message: MessageList; isCurrentUser: boolean; + isCoworker?: boolean; onReply: (message: MessageList) => void; onDelete?: (messageId: string) => void; replyToMessageContent?: string | undefined; @@ -63,6 +72,11 @@ export const MessageBubble = ({ isCurrentUser ? "bg-BLUE2 text-WHITE" : "bg-GRAY3" }`} > + {isCoworker && message.senderUsername && ( +
+ {message.senderUsername} +
+ )} {message.replyTo && (
)} - {message.content &&

{message.content}

} + {message.imageUrl && ( + window.open(message.imageUrl, "_blank")} + /> + )} + {message.content &&

{message.content}

}
@@ -135,7 +157,7 @@ export const MessageBubble = ({ پاسخ - {isCurrentUser && ( + {isCurrentUser && !isCoworker && ( { setShowDropdownMenu(false); diff --git a/app/components/thread/MessageInput.tsx b/app/components/thread/MessageInput.tsx index bd17039..8046e88 100644 --- a/app/components/thread/MessageInput.tsx +++ b/app/components/thread/MessageInput.tsx @@ -1,12 +1,24 @@ -import { ArrowRight, Loader2, Reply, SmilePlus, X } from "lucide-react"; +import { ArrowRight, Image as ImageIcon, Loader2, Reply, SmilePlus, X } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { useRef, useState } from "react"; import EmojiPicker from "./EmojiPicker"; import type { MessageList } from "../../../src/api/types/models"; +import { useAuthToken } from "../../hooks/use-root-data"; +import { getApiBaseUrl } from "../../utils/api-config"; +import { toast } from "../../hooks/use-toast"; + +const MAX_IMAGE_BYTES = 8 * 1024 * 1024; +const ALLOWED_IMAGE_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/heic", "image/heif"]; // Message input component +// +// `canSendImage` toggles the paperclip button — only team members (owner or +// active staff of the thread's seller) can attach images. The buyer side +// leaves it off so customers can only send text. const MessageInput = ({ onSend, + onSendImage, + canSendImage = false, replyToMessage, onCancelReply, messageText, @@ -15,6 +27,8 @@ const MessageInput = ({ setReplyToMessage, }: { onSend: (text: string) => void; + onSendImage?: (imageUrl: string) => void; + canSendImage?: boolean; replyToMessage: MessageList | undefined; onCancelReply: () => void; inputRef: React.RefObject; @@ -23,6 +37,10 @@ const MessageInput = ({ isSendingMessage: boolean; setReplyToMessage: (message: MessageList | undefined) => void; }) => { + const token = useAuthToken(); + const apiBase = getApiBaseUrl(); + const fileInputRef = useRef(null); + const [isUploadingImage, setIsUploadingImage] = useState(false); const [caretPosition, setCaretPosition] = useState(null); // Track current textarea height @@ -77,6 +95,65 @@ const MessageInput = ({ setMessageText(e.target.value); }; + const handlePickImage = () => { + if (!canSendImage || isUploadingImage) return; + fileInputRef.current?.click(); + }; + + const handleFileSelected = async ( + e: React.ChangeEvent, + ) => { + const file = e.target.files?.[0]; + // Reset the input so the same file can be re-picked after cancel. + if (e.target) e.target.value = ""; + if (!file || !canSendImage || !onSendImage) return; + + if (!ALLOWED_IMAGE_TYPES.includes(file.type)) { + toast({ + title: "فرمت پشتیبانی نمی‌شود", + description: "فقط تصاویر JPG / PNG / WebP / HEIC مجاز است.", + variant: "destructive", + }); + return; + } + if (file.size > MAX_IMAGE_BYTES) { + toast({ + title: "حجم زیاد", + description: "حداکثر حجم تصویر ۸ مگابایت است.", + variant: "destructive", + }); + return; + } + + setIsUploadingImage(true); + try { + const form = new FormData(); + form.append("file", file); + form.append("usage", "chat"); + const headers: Record = {}; + if (token) headers["Authorization"] = `Bearer ${token}`; + const res = await fetch(`${apiBase}/api/media/v1/upload/`, { + method: "POST", + headers, + body: form, + }); + if (!res.ok) throw new Error(`upload failed: ${res.status}`); + const data = await res.json(); + const url: string | undefined = data.originalUrl || data.original_url; + if (!url) throw new Error("no url in upload response"); + onSendImage(url); + } catch (err) { + console.error("chat image upload failed:", err); + toast({ + title: "خطا در ارسال تصویر", + description: "لطفاً دوباره تلاش کنید.", + variant: "destructive", + }); + } finally { + setIsUploadingImage(false); + } + }; + return ( <> {/* Reply indicator */} @@ -130,6 +207,31 @@ const MessageInput = ({ + {canSendImage && ( + <> + + + + )} +