From 4dcacd0d7332bbdd4d59d48519ff0e3bab4c9d24 Mon Sep 17 00:00:00 2001 From: Talon Date: Thu, 21 Aug 2025 14:06:37 +0200 Subject: [PATCH] Fix time display, fix file attachments not working properly after sending without refresh --- backend/src/controllers/file-controller.ts | 11 ++- backend/src/services/file-service.ts | 9 +- .../src/components/chat/FileAttachment.vue | 1 + .../src/components/chat/MessageItem.vue | 32 +++++-- .../src/components/dialogs/SearchDialog.vue | 14 +-- frontend-vue/src/utils/time.ts | 94 +++++++++++++++++++ frontend-vue/src/views/MainView.vue | 9 +- 7 files changed, 142 insertions(+), 28 deletions(-) create mode 100644 frontend-vue/src/utils/time.ts diff --git a/backend/src/controllers/file-controller.ts b/backend/src/controllers/file-controller.ts index a67c530..bf590cd 100644 --- a/backend/src/controllers/file-controller.ts +++ b/backend/src/controllers/file-controller.ts @@ -18,7 +18,16 @@ export const uploadFile = async (req: Request, res: Response) => { const result = await FileService.uploadFile(channelId, messageId, filePath, fileType!, fileSize!, originalName!); logger.info(`File ${originalName} uploaded to message ${messageId} as ${filePath}`); - res.json({ id: result.lastInsertRowid, channelId, messageId, filePath, fileType }); + res.json({ + id: result.lastInsertRowid, + channel_id: parseInt(channelId), + message_id: parseInt(messageId), + file_path: filePath, + file_type: fileType, + file_size: fileSize, + original_name: originalName, + created_at: new Date().toISOString() + }); } diff --git a/backend/src/services/file-service.ts b/backend/src/services/file-service.ts index 3dba4f7..ee02dee 100644 --- a/backend/src/services/file-service.ts +++ b/backend/src/services/file-service.ts @@ -11,11 +11,16 @@ export const uploadFile = async (channelId: string, messageId: string, filePath: const result2 = updateQuery.run({ fileId: fileId, messageId: messageId }); events.emit('file-uploaded', result.lastInsertRowid, channelId, messageId, filePath, fileType, fileSize, originalName); - return result2; '' + return result; } export const getFiles = async (messageId: string) => { - const query = db.prepare(`SELECT * FROM files WHERE messageId = $messageId`); + // Get the file linked to this message via the fileId in the messages table + const query = db.prepare(` + SELECT files.* FROM files + JOIN messages ON messages.fileId = files.id + WHERE messages.id = $messageId + `); const rows = query.all({ messageId: messageId }); return rows; } \ No newline at end of file diff --git a/frontend-vue/src/components/chat/FileAttachment.vue b/frontend-vue/src/components/chat/FileAttachment.vue index 61cc11c..5d939f4 100644 --- a/frontend-vue/src/components/chat/FileAttachment.vue +++ b/frontend-vue/src/components/chat/FileAttachment.vue @@ -34,6 +34,7 @@ interface Props { const props = defineProps() const fileExtension = computed(() => { + if (!props.file.original_name) return '' return props.file.original_name.split('.').pop()?.toLowerCase() || '' }) diff --git a/frontend-vue/src/components/chat/MessageItem.vue b/frontend-vue/src/components/chat/MessageItem.vue index d3e6292..54a8ef1 100644 --- a/frontend-vue/src/components/chat/MessageItem.vue +++ b/frontend-vue/src/components/chat/MessageItem.vue @@ -20,8 +20,12 @@
-
@@ -33,6 +37,7 @@ import { computed } from 'vue' import { useAudio } from '@/composables/useAudio' import { useToastStore } from '@/stores/toast' import { useAppStore } from '@/stores/app' +import { formatSmartTimestamp, formatTimestampForScreenReader } from '@/utils/time' import FileAttachment from './FileAttachment.vue' import type { ExtendedMessage, UnsentMessage, FileAttachment as FileAttachmentType } from '@/types' @@ -61,21 +66,30 @@ const hasFileAttachment = computed(() => { const fileAttachment = computed((): FileAttachmentType | null => { if (!hasFileAttachment.value || !('fileId' in props.message)) return null + // Check if we have the minimum required file metadata + if (!props.message.filePath || !props.message.originalName) { + console.warn('File attachment missing metadata:', { + fileId: props.message.fileId, + filePath: props.message.filePath, + originalName: props.message.originalName, + fileType: props.message.fileType + }) + return null + } + return { id: props.message.fileId!, channel_id: props.message.channel_id, message_id: props.message.id, file_path: props.message.filePath!, - file_type: props.message.fileType!, - file_size: props.message.fileSize!, + file_type: props.message.fileType || 'application/octet-stream', + file_size: props.message.fileSize || 0, original_name: props.message.originalName!, created_at: props.message.fileCreatedAt || props.message.created_at } }) -const formatTime = (timestamp: string): string => { - return new Date(timestamp).toLocaleTimeString() -} +// formatTime function removed - now using formatSmartTimestamp from utils // Create comprehensive aria-label for screen readers const messageAriaLabel = computed(() => { @@ -95,8 +109,8 @@ const messageAriaLabel = computed(() => { // Add timestamp if ('created_at' in props.message && props.message.created_at) { - const time = formatTime(props.message.created_at) - label += `. Sent at ${time}` + const time = formatTimestampForScreenReader(props.message.created_at) + label += `. Sent ${time}` } // Add status for unsent messages diff --git a/frontend-vue/src/components/dialogs/SearchDialog.vue b/frontend-vue/src/components/dialogs/SearchDialog.vue index 097ce5d..cccd2e8 100644 --- a/frontend-vue/src/components/dialogs/SearchDialog.vue +++ b/frontend-vue/src/components/dialogs/SearchDialog.vue @@ -58,7 +58,7 @@ {{ result.content }}
- {{ formatTime(result.created_at) }} + {{ formatSmartTimestamp(result.created_at) }}
@@ -79,6 +79,7 @@ import { ref, onMounted } from 'vue' import { useAppStore } from '@/stores/app' import { useToastStore } from '@/stores/toast' import { apiService } from '@/services/api' +import { formatSmartTimestamp } from '@/utils/time' import BaseInput from '@/components/base/BaseInput.vue' import BaseButton from '@/components/base/BaseButton.vue' import type { Message, ExtendedMessage } from '@/types' @@ -140,16 +141,7 @@ const getChannelName = (channelId: number): string => { return channel?.name || `Channel ${channelId}` } -const formatTime = (timestamp: string): string => { - if (!timestamp) return 'Unknown time' - - const date = new Date(timestamp) - if (isNaN(date.getTime())) { - return 'Invalid date' - } - - return date.toLocaleString() -} +// formatTime function removed - now using formatSmartTimestamp from utils onMounted(() => { searchInput.value?.focus() diff --git a/frontend-vue/src/utils/time.ts b/frontend-vue/src/utils/time.ts new file mode 100644 index 0000000..c10bfbc --- /dev/null +++ b/frontend-vue/src/utils/time.ts @@ -0,0 +1,94 @@ +/** + * Smart timestamp formatting that shows appropriate level of detail based on message age + */ +export function formatSmartTimestamp(timestamp: string): string { + const now = new Date() + const date = new Date(timestamp) + + // Handle invalid dates + if (isNaN(date.getTime())) { + return 'Invalid date' + } + + const diffMs = now.getTime() - date.getTime() + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + + // Same day (today) + if (diffDays === 0) { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + } + + // Yesterday + if (diffDays === 1) { + const timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + return `Yesterday ${timeStr}` + } + + // This week (2-6 days ago) + if (diffDays <= 6) { + const dayStr = date.toLocaleDateString([], { weekday: 'short' }) + const timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + return `${dayStr} ${timeStr}` + } + + // This year (more than a week ago) + if (now.getFullYear() === date.getFullYear()) { + return date.toLocaleDateString([], { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + } + + // Different year + return date.toLocaleDateString([], { + month: 'short', + day: 'numeric', + year: 'numeric' + }) +} + +/** + * Format timestamp for accessibility/screen readers with full context + */ +export function formatTimestampForScreenReader(timestamp: string): string { + const date = new Date(timestamp) + + if (isNaN(date.getTime())) { + return 'Invalid date' + } + + const now = new Date() + const diffMs = now.getTime() - date.getTime() + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + + // Same day + if (diffDays === 0) { + const timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + return `today at ${timeStr}` + } + + // Yesterday + if (diffDays === 1) { + const timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + return `yesterday at ${timeStr}` + } + + // This week + if (diffDays <= 6) { + const dayStr = date.toLocaleDateString([], { weekday: 'long' }) + const timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + return `${dayStr} at ${timeStr}` + } + + // Older messages - use full date and time + return date.toLocaleDateString([], { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) +} \ No newline at end of file diff --git a/frontend-vue/src/views/MainView.vue b/frontend-vue/src/views/MainView.vue index 6a36555..d76d978 100644 --- a/frontend-vue/src/views/MainView.vue +++ b/frontend-vue/src/views/MainView.vue @@ -130,6 +130,7 @@ import { useOfflineSync } from '@/composables/useOfflineSync' import { useWebSocket } from '@/composables/useWebSocket' import { useKeyboardShortcuts } from '@/composables/useKeyboardShortcuts' import { useAudio } from '@/composables/useAudio' +import { formatTimestampForScreenReader } from '@/utils/time' import { apiService } from '@/services/api' import { syncService } from '@/services/sync' @@ -364,9 +365,7 @@ const handleSelectMessage = async (message: ExtendedMessage) => { } } -const formatTime = (timestamp: string): string => { - return new Date(timestamp).toLocaleTimeString() -} +// formatTime function removed - now using formatTimestampForScreenReader from utils const handleVoiceSent = () => { // Voice message was sent successfully @@ -396,8 +395,8 @@ const announceLastMessage = (position: number) => { } const message = messages[messageIndex] - const timeStr = formatTime(message.created_at) - const announcement = `${message.content}; ${timeStr}` + const timeStr = formatTimestampForScreenReader(message.created_at) + const announcement = `${message.content}; sent ${timeStr}` toastStore.info(announcement)