diff --git a/client/.env.development b/client/.env.development
index 2ec742f..0b11248 100644
--- a/client/.env.development
+++ b/client/.env.development
@@ -1 +1 @@
-VITE_WS_URL=ws://localhost:8080
+VITE_WS_URL=ws://localhost:8081
diff --git a/client/src/components/Message.css b/client/src/components/Message.css
index 9c1e407..1b2e927 100644
--- a/client/src/components/Message.css
+++ b/client/src/components/Message.css
@@ -118,6 +118,11 @@
background: rgba(255, 255, 255, 0.1);
}
+.message-file-link.disabled {
+ opacity: 0.5;
+ cursor: default;
+}
+
.file-icon {
font-size: 1.1rem;
}
diff --git a/client/src/components/Message.jsx b/client/src/components/Message.jsx
index d89f7fd..3f3bbf4 100644
--- a/client/src/components/Message.jsx
+++ b/client/src/components/Message.jsx
@@ -58,6 +58,8 @@ export default function Message({ message, isOwn, onPin }) {
+ ) : message.filename ? (
+ {FILE_ICON}{message.filename}
) :
Failed to load image
)}
{isFile && typeGroup === 'audio' && (fileUrl ? (
@@ -67,6 +69,8 @@ export default function Message({ message, isOwn, onPin }) {
{message.filename}
+ ) : message.filename ? (
+ {FILE_ICON}{message.filename}
) : Failed to load audio
)}
{isFile && typeGroup === 'video' && (fileUrl ? (
@@ -82,12 +86,16 @@ export default function Message({ message, isOwn, onPin }) {
{message.filename}
+ ) : message.filename ? (
+ {FILE_ICON}{message.filename}
) : Failed to load video
)}
{isFile && typeGroup === 'file' && (fileUrl ? (
{FILE_ICON}
{message.filename}
+ ) : message.filename ? (
+ {FILE_ICON}{message.filename}
) : Failed to load file
)}
diff --git a/client/src/components/MessageInput.jsx b/client/src/components/MessageInput.jsx
index f9b70cc..97cd76e 100644
--- a/client/src/components/MessageInput.jsx
+++ b/client/src/components/MessageInput.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useRef } from 'react';
+import React, { useState, useRef, useCallback } from 'react';
import VoiceRecorder from './VoiceRecorder';
import './MessageInput.css';
@@ -13,6 +13,24 @@ function readFileAsBase64(file) {
});
}
+function sendFile(ws, file) {
+ if (file.size > MAX_FILE_SIZE) {
+ alert('File too large. Maximum is 10 MB.');
+ return;
+ }
+
+ readFileAsBase64(file).then((data) => {
+ ws.send(JSON.stringify({
+ type: 'file',
+ filename: file.name,
+ mime: file.type || 'application/octet-stream',
+ data,
+ }));
+ }).catch(() => {
+ alert('Failed to read file');
+ });
+}
+
export default function MessageInput({ ws }) {
const [text, setText] = useState('');
const fileRef = useRef(null);
@@ -24,31 +42,34 @@ export default function MessageInput({ ws }) {
setText('');
};
- const handleFileSelect = async (e) => {
+ const handleFileSelect = (e) => {
const file = e.target.files[0];
e.target.value = '';
if (!file) return;
-
- if (file.size > MAX_FILE_SIZE) {
- alert('File too large. Maximum is 10 MB.');
- return;
- }
-
- try {
- const data = await readFileAsBase64(file);
- ws.send(JSON.stringify({
- type: 'file',
- filename: file.name,
- mime: file.type || 'application/octet-stream',
- data,
- }));
- } catch {
- alert('Failed to read file');
- }
+ sendFile(ws, file);
};
+ const handlePaste = useCallback((e) => {
+ const items = e.clipboardData?.items;
+ if (!items) return;
+
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ if (item.kind === 'file' && item.type.startsWith('image/')) {
+ e.preventDefault();
+ const file = item.getAsFile();
+ if (!file) continue;
+ const ext = file.type.split('/')[1] || 'png';
+ const blob = file.slice(0, file.size, file.type);
+ const pastedFile = new File([blob], `pasted-image-${Date.now()}.${ext}`, { type: file.type });
+ sendFile(ws, pastedFile);
+ break;
+ }
+ }
+ }, [ws]);
+
return (
-