UI improvements, added attachments volume to docker-compose.yml

This commit is contained in:
slawk0
2025-01-03 17:31:04 +01:00
parent 9da939c66f
commit 8b545e056b
8 changed files with 65 additions and 27 deletions

View File

@@ -15,11 +15,13 @@ FROM nginx:alpine
# Remove the default Nginx configuration file
RUN rm /etc/nginx/conf.d/default.conf
RUN mkdir -p /app/attachments && chown -R node:node /app/attachments
# Copy the built files from the builder stage to the Nginx image
COPY --from=builder /app/client/dist /usr/share/nginx/html
# Expose port 80
EXPOSE 80
USER node
# Start Nginx in the foreground
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -28,7 +28,7 @@ const AnimatedMessage = ({ onDelete, message }: AnimatedMessageProps) => {
${isRemoving ? 'transition-all duration-300 opacity-0 -translate-x-full' : 'opacity-100'}`}
>
<div className="flex items-center justify-between">
<div>
<div className="max-w-full">
<span
title={`${new Intl.DateTimeFormat('en-GB', {
timeStyle: 'medium',
@@ -38,8 +38,8 @@ const AnimatedMessage = ({ onDelete, message }: AnimatedMessageProps) => {
{message.sender}: {message.message}
</span>
{message.attachment_urls && (
<div className="mt-2 flex flex-col gap-2">
{message.attachment_urls.length > 0
<div className="mt-2 flex flex-col gap-2 max-w-full">
{message.attachment_urls?.length > 0
? message.attachment_urls.map((url, index) => (
<AttachmentPreview
key={`${message.message_id}-${index}`}

View File

@@ -2,17 +2,14 @@ import { useEffect, useState } from 'react';
import LoadingWheel from '../LoadingWheel.tsx';
import FileBox from './FileBox.tsx';
// Cache to keep track of loaded media
const loadedMedia = new Set();
const AttachmentPreview = ({ url }: { url: string }) => {
const isImage = url.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/i);
const isVideo = url.match(/\.(mp4|webm|ogg|mov)$/i);
const [isLoading, setIsLoading] = useState(!loadedMedia.has(url));
useEffect(() => {
if (isImage && !loadedMedia.has(url)) {
// Preload image
const img = new Image();
img.onload = () => {
loadedMedia.add(url);
@@ -20,7 +17,6 @@ const AttachmentPreview = ({ url }: { url: string }) => {
};
img.src = url;
} else if (isVideo && !loadedMedia.has(url)) {
// Preload video metadata
const video = document.createElement('video');
video.onloadedmetadata = () => {
loadedMedia.add(url);
@@ -28,6 +24,7 @@ const AttachmentPreview = ({ url }: { url: string }) => {
};
video.src = url;
}
// setIsLoading(true);
}, [url, isImage, isVideo]);
if (!isImage && !isVideo) {
@@ -36,15 +33,15 @@ const AttachmentPreview = ({ url }: { url: string }) => {
if (isVideo) {
return (
<div className="relative min-h-64 w-full">
<div className="relative w-full">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-800 bg-opacity-50 rounded">
<div className="absolute max-h-64 inset-0 flex items-center justify-center bg-gray-800 bg-opacity-50 rounded h-40">
<LoadingWheel />
</div>
)}
<video
controls
className={`max-w-full max-h-64 w-full rounded transition-opacity duration-200 ${
className={`w-full max-h-64 rounded transition-opacity duration-200 ${
isLoading ? 'opacity-0' : 'opacity-100'
}`}
onLoadedMetadata={() => setIsLoading(false)}
@@ -57,19 +54,17 @@ const AttachmentPreview = ({ url }: { url: string }) => {
}
return (
<div className="relative min-h-64 w-full ">
<div className="relative inline-block max-w-full">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-800 bg-opacity-50 rounded">
<LoadingWheel />
</div>
<div className="absolute inset-0 flex items-center justify-center bg-gray-800 bg-opacity-50 rounded "></div>
)}
<a href={url} target="_blank" rel="noopener noreferrer">
<img
src={url}
alt="attachment"
className={`max-w-full max-h-64 object-contain rounded transition-opacity duration-200 ${
isLoading ? 'opacity-0' : 'opacity-100'
}`}
className={`min-h-14 max-h-64 max-w-full rounded
transition-opacity duration-300 ease-in
${isLoading ? 'opacity-0' : 'opacity-100'}`}
/>
</a>
</div>

View File

@@ -115,7 +115,7 @@ const MessageForm = () => {
};
const submitMessage: SubmitHandler<InputProps> = async (data) => {
if ((data.message.trim().length < 1 && files.length === 0) || isSending)
if ((data.message.trim()?.length < 1 && files.length === 0) || isSending)
return;
setErrorMessage(null);
setIsSending(true);

View File

@@ -30,7 +30,7 @@ function ContactForm() {
);
setSuggestions(response.data);
setSelectedIndex(0); // Reset selection to first item when suggestions update
if (response.data.length < 1) {
if (response.data?.length < 1) {
setIsLoading(false);
setNotFound(true);
} else {
@@ -102,7 +102,7 @@ function ContactForm() {
);
break;
case 'Enter':
if (suggestions.length > 0) {
if (suggestions?.length > 0) {
e.preventDefault();
reset({ contact: suggestions[selectedIndex] });
handleSubmit(submitContact)();

View File

@@ -12,7 +12,7 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog.tsx';
import { Dot } from 'lucide-react';
import { Dot, Paperclip } from 'lucide-react';
import LastActiveTime from '@/components/chat/leftSidebar/LastActiveTime.tsx';
import { ContactsProps } from '@/types/types.ts';
import { useChat } from '@/context/chat/useChat.ts';
@@ -116,9 +116,24 @@ function ContactsList() {
</div>
<div className="flex">
<span className="text-sm text-gray-500 text-left">
{contact.last_message}
{contact.last_message?.length > 0 ? (
contact.last_message?.length > 15 ? (
<div className="flex">
{contact.last_message?.substring(0, 12) + '...'}
<Dot className="text-gray-200" />
</div>
) : (
<div className="flex">
{contact.last_message} <Dot className="text-gray-200" />
</div>
)
) : contact.last_message_time ? (
<div className="flex">
<Paperclip className="w-4 mr-1" /> attachment
<Dot className="text-gray-200" />
</div>
) : null}
</span>
<Dot className="text-gray-200" />
<LastActiveTime contact={contact} />
</div>
</div>

View File

@@ -14,7 +14,7 @@ services:
ports:
- "3000"
volumes:
- attachments:/app/client/server/attachments
- attachments:/app/attachments
depends_on:
db:
condition: service_healthy
@@ -60,3 +60,4 @@ networks:
volumes:
postgres-data:
attachments:

View File

@@ -568,7 +568,29 @@ async function insertContact(userUsername, receiverUsername, read) {
]);
const contact = contactResult.rows[0];
// 6. Return formatted result with contact's user_id
// 6. Retrieve the last message, last active time, and last message sender
const lastMessageQuery = `
SELECT DISTINCT ON (m.conversation_id)
m.content AS last_message,
m.sent_at AS last_message_time,
a.username AS last_message_sender
FROM Messages m
JOIN Accounts a ON m.user_id = a.user_id
WHERE m.conversation_id = $1
ORDER BY m.conversation_id, m.sent_at DESC
`;
const lastMessageResult = await client.query(lastMessageQuery, [
conversation_id,
]);
let lastMessage, lastMessageTime, lastMessageSender;
if (lastMessageResult.rows.length > 0) {
lastMessage = lastMessageResult.rows[0].last_message;
lastMessageTime = lastMessageResult.rows[0].last_message_time;
lastMessageSender = lastMessageResult.rows[0].last_message_sender;
}
// 7. Return formatted result with contact's user_id, last message, last active time, and last message sender
return {
id: contact.contact_id,
user_id: contact_id, // Now using the contact's user_id instead of the initiator's
@@ -577,6 +599,9 @@ async function insertContact(userUsername, receiverUsername, read) {
conversation_id: contact.conversation_id,
type: "direct",
read: contact.read,
last_message: lastMessage,
last_message_time: lastMessageTime,
last_message_sender: lastMessageSender,
};
} catch (error) {
console.error("Failed to insert contact:", error);