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
|
# Remove the default Nginx configuration file
|
||||||
RUN rm /etc/nginx/conf.d/default.conf
|
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 the built files from the builder stage to the Nginx image
|
||||||
COPY --from=builder /app/client/dist /usr/share/nginx/html
|
COPY --from=builder /app/client/dist /usr/share/nginx/html
|
||||||
|
|
||||||
# Expose port 80
|
# Expose port 80
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
USER node
|
||||||
# Start Nginx in the foreground
|
# Start Nginx in the foreground
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
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'}`}
|
${isRemoving ? 'transition-all duration-300 opacity-0 -translate-x-full' : 'opacity-100'}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div className="max-w-full">
|
||||||
<span
|
<span
|
||||||
title={`${new Intl.DateTimeFormat('en-GB', {
|
title={`${new Intl.DateTimeFormat('en-GB', {
|
||||||
timeStyle: 'medium',
|
timeStyle: 'medium',
|
||||||
@@ -38,8 +38,8 @@ const AnimatedMessage = ({ onDelete, message }: AnimatedMessageProps) => {
|
|||||||
{message.sender}: {message.message}
|
{message.sender}: {message.message}
|
||||||
</span>
|
</span>
|
||||||
{message.attachment_urls && (
|
{message.attachment_urls && (
|
||||||
<div className="mt-2 flex flex-col gap-2">
|
<div className="mt-2 flex flex-col gap-2 max-w-full">
|
||||||
{message.attachment_urls.length > 0
|
{message.attachment_urls?.length > 0
|
||||||
? message.attachment_urls.map((url, index) => (
|
? message.attachment_urls.map((url, index) => (
|
||||||
<AttachmentPreview
|
<AttachmentPreview
|
||||||
key={`${message.message_id}-${index}`}
|
key={`${message.message_id}-${index}`}
|
||||||
|
|||||||
@@ -2,17 +2,14 @@ import { useEffect, useState } from 'react';
|
|||||||
import LoadingWheel from '../LoadingWheel.tsx';
|
import LoadingWheel from '../LoadingWheel.tsx';
|
||||||
import FileBox from './FileBox.tsx';
|
import FileBox from './FileBox.tsx';
|
||||||
|
|
||||||
// Cache to keep track of loaded media
|
|
||||||
const loadedMedia = new Set();
|
const loadedMedia = new Set();
|
||||||
|
|
||||||
const AttachmentPreview = ({ url }: { url: string }) => {
|
const AttachmentPreview = ({ url }: { url: string }) => {
|
||||||
const isImage = url.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/i);
|
const isImage = url.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/i);
|
||||||
const isVideo = url.match(/\.(mp4|webm|ogg|mov)$/i);
|
const isVideo = url.match(/\.(mp4|webm|ogg|mov)$/i);
|
||||||
const [isLoading, setIsLoading] = useState(!loadedMedia.has(url));
|
const [isLoading, setIsLoading] = useState(!loadedMedia.has(url));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isImage && !loadedMedia.has(url)) {
|
if (isImage && !loadedMedia.has(url)) {
|
||||||
// Preload image
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
loadedMedia.add(url);
|
loadedMedia.add(url);
|
||||||
@@ -20,7 +17,6 @@ const AttachmentPreview = ({ url }: { url: string }) => {
|
|||||||
};
|
};
|
||||||
img.src = url;
|
img.src = url;
|
||||||
} else if (isVideo && !loadedMedia.has(url)) {
|
} else if (isVideo && !loadedMedia.has(url)) {
|
||||||
// Preload video metadata
|
|
||||||
const video = document.createElement('video');
|
const video = document.createElement('video');
|
||||||
video.onloadedmetadata = () => {
|
video.onloadedmetadata = () => {
|
||||||
loadedMedia.add(url);
|
loadedMedia.add(url);
|
||||||
@@ -28,6 +24,7 @@ const AttachmentPreview = ({ url }: { url: string }) => {
|
|||||||
};
|
};
|
||||||
video.src = url;
|
video.src = url;
|
||||||
}
|
}
|
||||||
|
// setIsLoading(true);
|
||||||
}, [url, isImage, isVideo]);
|
}, [url, isImage, isVideo]);
|
||||||
|
|
||||||
if (!isImage && !isVideo) {
|
if (!isImage && !isVideo) {
|
||||||
@@ -36,15 +33,15 @@ const AttachmentPreview = ({ url }: { url: string }) => {
|
|||||||
|
|
||||||
if (isVideo) {
|
if (isVideo) {
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-64 w-full">
|
<div className="relative w-full">
|
||||||
{isLoading && (
|
{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 />
|
<LoadingWheel />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<video
|
<video
|
||||||
controls
|
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'
|
isLoading ? 'opacity-0' : 'opacity-100'
|
||||||
}`}
|
}`}
|
||||||
onLoadedMetadata={() => setIsLoading(false)}
|
onLoadedMetadata={() => setIsLoading(false)}
|
||||||
@@ -57,19 +54,17 @@ const AttachmentPreview = ({ url }: { url: string }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-64 w-full ">
|
<div className="relative inline-block max-w-full">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-800 bg-opacity-50 rounded">
|
<div className="absolute inset-0 flex items-center justify-center bg-gray-800 bg-opacity-50 rounded "></div>
|
||||||
<LoadingWheel />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||||
<img
|
<img
|
||||||
src={url}
|
src={url}
|
||||||
alt="attachment"
|
alt="attachment"
|
||||||
className={`max-w-full max-h-64 object-contain rounded transition-opacity duration-200 ${
|
className={`min-h-14 max-h-64 max-w-full rounded
|
||||||
isLoading ? 'opacity-0' : 'opacity-100'
|
transition-opacity duration-300 ease-in
|
||||||
}`}
|
${isLoading ? 'opacity-0' : 'opacity-100'}`}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ const MessageForm = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const submitMessage: SubmitHandler<InputProps> = async (data) => {
|
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;
|
return;
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
setIsSending(true);
|
setIsSending(true);
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function ContactForm() {
|
|||||||
);
|
);
|
||||||
setSuggestions(response.data);
|
setSuggestions(response.data);
|
||||||
setSelectedIndex(0); // Reset selection to first item when suggestions update
|
setSelectedIndex(0); // Reset selection to first item when suggestions update
|
||||||
if (response.data.length < 1) {
|
if (response.data?.length < 1) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setNotFound(true);
|
setNotFound(true);
|
||||||
} else {
|
} else {
|
||||||
@@ -102,7 +102,7 @@ function ContactForm() {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
if (suggestions.length > 0) {
|
if (suggestions?.length > 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
reset({ contact: suggestions[selectedIndex] });
|
reset({ contact: suggestions[selectedIndex] });
|
||||||
handleSubmit(submitContact)();
|
handleSubmit(submitContact)();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from '@/components/ui/alert-dialog.tsx';
|
} 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 LastActiveTime from '@/components/chat/leftSidebar/LastActiveTime.tsx';
|
||||||
import { ContactsProps } from '@/types/types.ts';
|
import { ContactsProps } from '@/types/types.ts';
|
||||||
import { useChat } from '@/context/chat/useChat.ts';
|
import { useChat } from '@/context/chat/useChat.ts';
|
||||||
@@ -116,9 +116,24 @@ function ContactsList() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<span className="text-sm text-gray-500 text-left">
|
<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>
|
</span>
|
||||||
<Dot className="text-gray-200" />
|
|
||||||
<LastActiveTime contact={contact} />
|
<LastActiveTime contact={contact} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3000"
|
- "3000"
|
||||||
volumes:
|
volumes:
|
||||||
- attachments:/app/client/server/attachments
|
- attachments:/app/attachments
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -59,4 +59,5 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
|
attachments:
|
||||||
@@ -568,7 +568,29 @@ async function insertContact(userUsername, receiverUsername, read) {
|
|||||||
]);
|
]);
|
||||||
const contact = contactResult.rows[0];
|
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 {
|
return {
|
||||||
id: contact.contact_id,
|
id: contact.contact_id,
|
||||||
user_id: contact_id, // Now using the contact's user_id instead of the initiator's
|
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,
|
conversation_id: contact.conversation_id,
|
||||||
type: "direct",
|
type: "direct",
|
||||||
read: contact.read,
|
read: contact.read,
|
||||||
|
last_message: lastMessage,
|
||||||
|
last_message_time: lastMessageTime,
|
||||||
|
last_message_sender: lastMessageSender,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to insert contact:", error);
|
console.error("Failed to insert contact:", error);
|
||||||
|
|||||||
Reference in New Issue
Block a user