+ {isFriend && (
+
+ )}
+ {!isFriend && otherIsPending && (
+
+ )}
+ {!isFriend && !pending && (
+
+ )}
+ {!isFriend && iAmPending && (
+
Friend request pending…
+ )}
+
+
+
+ )
+}
diff --git a/client/src/components/MessageInput.jsx b/client/src/components/MessageInput.jsx
index 24e8dc5..0eb2703 100644
--- a/client/src/components/MessageInput.jsx
+++ b/client/src/components/MessageInput.jsx
@@ -1,21 +1,38 @@
import { useState, useRef } from 'react'
import { useApp } from '../context/AppContext'
-export default function MessageInput() {
+function fmtSize(bytes) {
+ if (bytes < 1024) return `${bytes} B`
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`
+}
+
+export default function MessageInput({ pendingFile, setPendingFile }) {
const { state, sendMessage } = useApp()
const [text, setText] = useState('')
+ const [busy, setBusy] = useState(false)
+ const [error, setError] = useState('')
const textareaRef = useRef(null)
+ const fileInputRef = useRef(null)
async function handleSend() {
const trimmed = text.trim()
- if (!trimmed || !state.selectedId) return
+ if ((!trimmed && !pendingFile) || !state.selectedId || busy) return
+ const file = pendingFile
+ setError('')
setText('')
+ setPendingFile(null)
+ setBusy(true)
try {
- await sendMessage(state.selectedId, trimmed)
- } catch {
+ await sendMessage(state.selectedId, trimmed, file)
+ } catch (err) {
setText(trimmed)
+ setPendingFile(file)
+ setError(err.message ?? String(err))
+ } finally {
+ setBusy(false)
+ textareaRef.current?.focus()
}
- textareaRef.current?.focus()
}
function onKeyDown(e) {
@@ -27,27 +44,68 @@ export default function MessageInput() {
if (!state.selectedId) return null
+ const canSend = !busy && (text.trim() || pendingFile)
+
return (
-
+ {pendingFile && (
+
+ 📎
+ {pendingFile.name}
+ {fmtSize(pendingFile.size)}
+
+
+ )}
+
+
+ {
+ const f = e.target.files?.[0]
+ if (f) setPendingFile(f)
+ e.target.value = ''
+ }}
+ />
+
+
+ {error && (
+
{error}
+ )}
)
}
diff --git a/client/src/components/MessageItem.jsx b/client/src/components/MessageItem.jsx
index 277ad2d..c1c1a1f 100644
--- a/client/src/components/MessageItem.jsx
+++ b/client/src/components/MessageItem.jsx
@@ -1,15 +1,50 @@
+import { useEffect, useState } from 'react'
import { useApp } from '../context/AppContext'
+import { apiFetch } from '../api/http'
import { colorToCss } from '../utils/color'
+const IMAGE_EXT = /\.(png|jpe?g|gif|webp|bmp)$/i
+
function formatTime(iso) {
const d = new Date(iso)
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
}
+function fileNameFromKey(key) {
+ return key.split('/').pop() || key
+}
+
export default function MessageItem({ msg, showHeader }) {
const { state, userId } = useApp()
const isMe = msg.sender === userId
const sender = isMe ? state.currentUser : state.userMap[msg.sender]
+ const [downloading, setDownloading] = useState(false)
+ const isImage = !!msg.attachedFile && IMAGE_EXT.test(msg.attachedFile)
+ const [imgUrl, setImgUrl] = useState(null)
+
+ useEffect(() => {
+ if (!isImage) return
+ let cancelled = false
+ apiFetch('GET', '/file', { query: { key: msg.attachedFile, connectionid: msg.receiver } })
+ .then(meta => { if (!cancelled && meta?.url) setImgUrl(meta.url) })
+ .catch(err => console.error('[MessageItem] preview failed', err))
+ return () => { cancelled = true }
+ }, [isImage, msg.attachedFile, msg.receiver])
+
+ async function openAttachment() {
+ if (downloading) return
+ setDownloading(true)
+ try {
+ const meta = await apiFetch('GET', '/file', {
+ query: { key: msg.attachedFile, connectionid: msg.receiver },
+ })
+ if (meta?.url) window.open(meta.url, '_blank', 'noopener')
+ } catch (err) {
+ console.error('[MessageItem] download failed', err)
+ } finally {
+ setDownloading(false)
+ }
+ }
return (
@@ -36,7 +71,41 @@ export default function MessageItem({ msg, showHeader }) {
{formatTime(msg.createdAt)}
)}
-
{msg.content}
+ {msg.content && (
+
{msg.content}
+ )}
+ {msg.attachedFile && isImage && (
+ imgUrl ? (
+
+
+
+ ) : (
+
+ Loading image…
+
+ )
+ )}
+ {msg.attachedFile && !isImage && (
+
+ )}