UI improvements, added attachments volume to docker-compose.yml
This commit is contained in:
@@ -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;"]
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)();
|
||||
|
||||
@@ -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}
|
||||
</span>
|
||||
{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>
|
||||
<LastActiveTime contact={contact} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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:
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user