Skip to content

Commit e10efde

Browse files
updating chat UI for attachments
1 parent 22603e2 commit e10efde

File tree

4 files changed

+187
-79
lines changed

4 files changed

+187
-79
lines changed

apps/api/src/chat/chat.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export class ChatController {
123123
return await this.chatService.sendMessageWithId(
124124
sender,
125125
receiver,
126-
text || `Sent a file: ${file.originalname}`, // Include full filename with extension in message
126+
text || "",
127127
messageId,
128128
uploadResult.secure_url,
129129
file.originalname, // Use full original filename with extension

apps/api/src/main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ async function bootstrap() {
1616
app.useGlobalPipes(new ValidationPipe());
1717

1818
// Body parser configuration
19-
app.use(bodyParser.json({ limit: '10mb' }));
20-
app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }));
19+
app.use(bodyParser.json({ limit: '150mb' }));
20+
app.use(bodyParser.urlencoded({ limit: '150mb', extended: true }));
2121

2222
// Enable CORS
2323
app.enableCors({

apps/client/src/components/Chat.tsx

Lines changed: 90 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { v4 as uuidv4 } from "uuid";
1212
import { PaperClipIcon } from "@heroicons/react/20/solid";
1313
import { SocketContext } from "../contexts/SocketContext";
1414
import { uploadFile as apiUploadFile } from "../services/MindsMeshAPI";
15+
import { toast } from "./shadcn/ui/use-toast";
16+
import FilePreview from "./chat/FilePreview";
1517

1618
interface Message {
1719
id: string;
@@ -35,7 +37,6 @@ const formatTime = (date: Date) => {
3537
return format(new Date(date), "HH:mm");
3638
};
3739

38-
3940
const Chat: React.FC<{ chatPartner?: User | null; onClose?: () => void }> = ({
4041
chatPartner,
4142
}) => {
@@ -51,6 +52,8 @@ const Chat: React.FC<{ chatPartner?: User | null; onClose?: () => void }> = ({
5152

5253
const { socket } = useContext(SocketContext); // Access socket from context
5354
const senderId = localStorage.getItem("userId");
55+
const MAX_FILE_SIZE = 150 * 1024 * 1024;
56+
const [uploadProgress, setUploadProgress] = useState(0);
5457

5558
// Track window focus/blur
5659
const [isActive, setIsActive] = useState(document.hasFocus());
@@ -236,7 +239,19 @@ const Chat: React.FC<{ chatPartner?: User | null; onClose?: () => void }> = ({
236239

237240
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
238241
if (e.target.files && e.target.files.length > 0) {
239-
setSelectedFile(e.target.files[0]);
242+
const file = e.target.files[0];
243+
244+
// Validate file size
245+
if (file.size > MAX_FILE_SIZE) {
246+
toast({
247+
title: "File too large",
248+
description: `Maximum file size is 150MB. Your file is ${(file.size / (1024 * 1024)).toFixed(2)}MB.`,
249+
variant: "destructive",
250+
});
251+
return;
252+
}
253+
254+
setSelectedFile(file);
240255
}
241256
};
242257

@@ -261,8 +276,25 @@ const Chat: React.FC<{ chatPartner?: User | null; onClose?: () => void }> = ({
261276

262277
try {
263278
setIsUploading(true);
279+
setUploadProgress(0);
280+
281+
// Simulated progress updates (in real app, you would get this from your upload API)
282+
const progressInterval = setInterval(() => {
283+
setUploadProgress((prev) => {
284+
if (prev >= 90) {
285+
clearInterval(progressInterval);
286+
return prev;
287+
}
288+
return prev + 10;
289+
});
290+
}, 300);
291+
264292
const data = await apiUploadFile(chatPartner.id, formData);
265293

294+
// Complete the progress
295+
clearInterval(progressInterval);
296+
setUploadProgress(100);
297+
266298
console.log("File upload response:", data);
267299
return {
268300
url: data.fileUrl,
@@ -274,6 +306,7 @@ const Chat: React.FC<{ chatPartner?: User | null; onClose?: () => void }> = ({
274306
return null;
275307
} finally {
276308
setIsUploading(false);
309+
setTimeout(() => setUploadProgress(0), 1000); // Reset progress after a delay
277310
}
278311
};
279312

@@ -294,9 +327,7 @@ const Chat: React.FC<{ chatPartner?: User | null; onClose?: () => void }> = ({
294327
id: messageId,
295328
senderId,
296329
receiverId: chatPartner.id,
297-
text:
298-
newMessage.trim() ||
299-
(selectedFile ? `Sent a file: ${selectedFile.name}` : ""),
330+
text: newMessage.trim(),
300331
timestamp: new Date(),
301332
status: "sending",
302333
isRead: false,
@@ -374,65 +405,69 @@ const Chat: React.FC<{ chatPartner?: User | null; onClose?: () => void }> = ({
374405
const renderFilePreview = (message: Message) => {
375406
if (!message.fileUrl) return null;
376407

377-
console.log(
378-
"Rendering file preview:",
379-
message.fileUrl,
380-
message.fileType,
381-
message.fileName
382-
); // Debug log
383-
384-
// Ensure we have the file name with extension
385408
const fileName = message.fileName || "file";
409+
const fileType = message.fileType || "";
410+
const isImage = fileType.startsWith("image/");
411+
const isPdf = fileType === "application/pdf";
412+
const isText = fileType.startsWith("text/");
413+
const isDoc = fileType.includes("word") || fileType.includes("document");
386414

387-
const isImage = message.fileType?.startsWith("image/");
388-
const isPdf = message.fileType === "application/pdf";
389-
const isText = message.fileType?.startsWith("text/");
390-
391-
// Helper function to get appropriate file icon
415+
// Helper function to get appropriate file icon with color
392416
const getFileIcon = () => {
393-
if (isPdf) return <FileText size={16} className="mr-2" />;
394-
if (isText) return <FileText size={16} className="mr-2" />;
395-
return <File size={16} className="mr-2" />;
417+
if (isPdf) return <FileText size={16} className="mr-2 text-red-500" />;
418+
if (isText) return <FileText size={16} className="mr-2 text-green-500" />;
419+
if (isDoc) return <FileText size={16} className="mr-2 text-blue-500" />;
420+
return <File size={16} className="mr-2 text-gray-500" />;
396421
};
397422

398423
return (
399424
<div className="mt-2 max-w-full">
400425
{isImage ? (
401-
<a
402-
href={message.fileUrl}
403-
target="_blank"
404-
rel="noopener noreferrer"
405-
className="block"
406-
download={fileName}
407-
>
408-
<img
409-
src={message.fileUrl}
410-
alt={fileName || "Attached image"}
411-
className="max-w-full max-h-48 rounded-lg object-contain"
412-
/>
413-
<span className="text-xs mt-1 flex items-center">
414-
<Image size={12} className="mr-1" />
415-
{fileName}
416-
</span>
417-
</a>
418-
) : (
419-
<div className="flex flex-col space-y-2">
426+
<div className="rounded-lg overflow-hidden border border-gray-200">
420427
<a
421428
href={message.fileUrl}
422429
target="_blank"
423430
rel="noopener noreferrer"
424-
className="flex items-center p-2 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
431+
className="block"
425432
download={fileName}
426433
>
427-
{getFileIcon()}
428-
<span className="text-sm truncate">{fileName}</span>
434+
<div className="relative pb-[60%] bg-gray-100">
435+
<img
436+
src={message.fileUrl}
437+
alt={fileName || "Attached image"}
438+
className="absolute inset-0 w-full h-full object-contain"
439+
/>
440+
</div>
441+
<div className="p-2 bg-white text-xs flex items-center text-gray-600">
442+
<Image size={12} className="mr-1" />
443+
<span className="truncate">{fileName}</span>
444+
</div>
429445
</a>
446+
</div>
447+
) : (
448+
<div className="flex flex-col space-y-2">
430449
<a
431450
href={message.fileUrl}
451+
target="_blank"
452+
rel="noopener noreferrer"
453+
className="flex items-center p-3 bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 transition-colors"
432454
download={fileName}
433-
className="text-xs text-blue-600 hover:underline flex items-center"
434455
>
435-
<span>Download {fileName}</span>
456+
{getFileIcon()}
457+
<div className="flex-1 min-w-0">
458+
<span className="text-sm font-medium text-gray-800 truncate block">
459+
{fileName}
460+
</span>
461+
<span className="text-xs text-gray-500">
462+
{isPdf
463+
? "PDF Document"
464+
: isText
465+
? "Text File"
466+
: isDoc
467+
? "Word Document"
468+
: "File"}
469+
</span>
470+
</div>
436471
</a>
437472
</div>
438473
)}
@@ -575,38 +610,17 @@ const Chat: React.FC<{ chatPartner?: User | null; onClose?: () => void }> = ({
575610
{chatPartner && (
576611
<CardFooter className="p-4 bg-white border-t flex flex-col">
577612
{selectedFile && (
578-
<div className="w-full px-3 py-2 mb-3 bg-blue-50 border border-blue-100 rounded-lg">
579-
<div className="flex items-center">
580-
{/* Add file type icon based on mimetype */}
581-
{selectedFile.type.startsWith("image/") ? (
582-
<Image size={16} className="text-blue-500 mr-2" />
583-
) : selectedFile.type === "application/pdf" ? (
584-
<FileText size={16} className="text-red-500 mr-2" />
585-
) : (
586-
<File size={16} className="text-gray-500 mr-2" />
587-
)}
613+
<div className="w-full mb-3">
614+
<FilePreview file={selectedFile} onRemove={handleRemoveFile} />
615+
</div>
616+
)}
588617

589-
{/* Filename with better truncation */}
590-
<div
591-
className="flex-1 truncate max-w-[calc(100%-60px)]"
592-
title={selectedFile.name}
593-
>
594-
<span className="text-sm font-medium text-gray-700">
595-
{selectedFile.name}
596-
</span>
597-
<span className="text-xs text-gray-500 block">
598-
{(selectedFile.size / 1024).toFixed(0)} KB
599-
</span>
600-
</div>
601-
602-
<button
603-
onClick={handleRemoveFile}
604-
className="ml-2 p-1 rounded-full hover:bg-gray-200 text-gray-500"
605-
title="Remove file"
606-
>
607-
<X size={16} />
608-
</button>
609-
</div>
618+
{selectedFile && isUploading && uploadProgress > 0 && (
619+
<div className="w-full h-1 bg-gray-200 rounded-full mt-2 mb-3">
620+
<div
621+
className="h-full bg-blue-500 rounded-full transition-all duration-300 ease-out"
622+
style={{ width: `${uploadProgress}%` }}
623+
/>
610624
</div>
611625
)}
612626

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React, { useState, useEffect } from "react";
2+
import { X, Image, FileText, File, Loader2 } from "lucide-react";
3+
4+
interface FilePreviewProps {
5+
file: File;
6+
onRemove: () => void;
7+
}
8+
9+
const FilePreview: React.FC<FilePreviewProps> = ({ file, onRemove }) => {
10+
const [preview, setPreview] = useState<string | null>(null);
11+
const [loading, setLoading] = useState(true);
12+
13+
useEffect(() => {
14+
if (!file) return;
15+
16+
// Generate preview for images
17+
if (file.type.startsWith("image/")) {
18+
const reader = new FileReader();
19+
reader.onloadend = () => {
20+
setPreview(reader.result as string);
21+
setLoading(false);
22+
};
23+
reader.readAsDataURL(file);
24+
} else {
25+
setLoading(false);
26+
}
27+
28+
return () => {
29+
if (preview && file.type.startsWith("image/")) {
30+
URL.revokeObjectURL(preview);
31+
}
32+
};
33+
}, [file]);
34+
35+
// Get icon based on file type
36+
const getFileIcon = () => {
37+
if (file.type.startsWith("image/")) return <Image size={24} className="text-blue-500" />;
38+
if (file.type === "application/pdf") return <FileText size={24} className="text-red-500" />;
39+
if (file.type.startsWith("text/")) return <FileText size={24} className="text-green-500" />;
40+
return <File size={24} className="text-gray-500" />;
41+
};
42+
43+
// Get color based on file type for the border
44+
const getBorderColor = () => {
45+
if (file.type.startsWith("image/")) return "border-blue-200";
46+
if (file.type === "application/pdf") return "border-red-200";
47+
if (file.type.startsWith("text/")) return "border-green-200";
48+
return "border-gray-200";
49+
};
50+
51+
return (
52+
<div className={`w-full p-3 rounded-lg border-2 ${getBorderColor()} bg-white shadow-sm`}>
53+
<div className="flex items-start">
54+
{/* Preview area */}
55+
<div className="w-12 h-12 mr-3 flex items-center justify-center rounded bg-gray-100">
56+
{loading ? (
57+
<Loader2 className="h-6 w-6 animate-spin text-gray-400" />
58+
) : file.type.startsWith("image/") && preview ? (
59+
<img
60+
src={preview}
61+
alt="Preview"
62+
className="w-full h-full object-cover rounded"
63+
/>
64+
) : (
65+
getFileIcon()
66+
)}
67+
</div>
68+
69+
{/* File info */}
70+
<div className="flex-1 overflow-hidden">
71+
<p className="font-medium text-sm text-gray-800 truncate" title={file.name}>
72+
{file.name}
73+
</p>
74+
<div className="flex items-center text-xs text-gray-500 mt-1">
75+
<span>{(file.size / 1024).toFixed(0)} KB</span>
76+
<span className="mx-2"></span>
77+
<span>{file.type.split('/')[1]?.toUpperCase() || 'FILE'}</span>
78+
</div>
79+
</div>
80+
81+
{/* Remove button */}
82+
<button
83+
onClick={onRemove}
84+
className="ml-2 p-1.5 rounded-full hover:bg-gray-100 text-gray-500"
85+
title="Remove file"
86+
>
87+
<X size={18} />
88+
</button>
89+
</div>
90+
</div>
91+
);
92+
};
93+
94+
export default FilePreview;

0 commit comments

Comments
 (0)