Fix time display, fix file attachments not working properly after sending without refresh
This commit is contained in:
@@ -18,7 +18,16 @@ export const uploadFile = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const result = await FileService.uploadFile(channelId, messageId, filePath, fileType!, fileSize!, originalName!);
|
const result = await FileService.uploadFile(channelId, messageId, filePath, fileType!, fileSize!, originalName!);
|
||||||
logger.info(`File ${originalName} uploaded to message ${messageId} as ${filePath}`);
|
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()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -11,11 +11,16 @@ export const uploadFile = async (channelId: string, messageId: string, filePath:
|
|||||||
const result2 = updateQuery.run({ fileId: fileId, messageId: messageId });
|
const result2 = updateQuery.run({ fileId: fileId, messageId: messageId });
|
||||||
|
|
||||||
events.emit('file-uploaded', result.lastInsertRowid, channelId, messageId, filePath, fileType, fileSize, originalName);
|
events.emit('file-uploaded', result.lastInsertRowid, channelId, messageId, filePath, fileType, fileSize, originalName);
|
||||||
return result2; ''
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFiles = async (messageId: string) => {
|
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 });
|
const rows = query.all({ messageId: messageId });
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
@@ -34,6 +34,7 @@ interface Props {
|
|||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
const fileExtension = computed(() => {
|
const fileExtension = computed(() => {
|
||||||
|
if (!props.file.original_name) return ''
|
||||||
return props.file.original_name.split('.').pop()?.toLowerCase() || ''
|
return props.file.original_name.split('.').pop()?.toLowerCase() || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -20,8 +20,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message__meta">
|
<div class="message__meta">
|
||||||
<time v-if="!isUnsent && 'created_at' in message" class="message__time">
|
<time
|
||||||
{{ formatTime(message.created_at) }}
|
v-if="!isUnsent && 'created_at' in message"
|
||||||
|
class="message__time"
|
||||||
|
:datetime="message.created_at"
|
||||||
|
>
|
||||||
|
{{ formatSmartTimestamp(message.created_at) }}
|
||||||
</time>
|
</time>
|
||||||
<span v-else class="message__status">Sending...</span>
|
<span v-else class="message__status">Sending...</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,6 +37,7 @@ import { computed } from 'vue'
|
|||||||
import { useAudio } from '@/composables/useAudio'
|
import { useAudio } from '@/composables/useAudio'
|
||||||
import { useToastStore } from '@/stores/toast'
|
import { useToastStore } from '@/stores/toast'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { formatSmartTimestamp, formatTimestampForScreenReader } from '@/utils/time'
|
||||||
import FileAttachment from './FileAttachment.vue'
|
import FileAttachment from './FileAttachment.vue'
|
||||||
import type { ExtendedMessage, UnsentMessage, FileAttachment as FileAttachmentType } from '@/types'
|
import type { ExtendedMessage, UnsentMessage, FileAttachment as FileAttachmentType } from '@/types'
|
||||||
|
|
||||||
@@ -61,21 +66,30 @@ const hasFileAttachment = computed(() => {
|
|||||||
const fileAttachment = computed((): FileAttachmentType | null => {
|
const fileAttachment = computed((): FileAttachmentType | null => {
|
||||||
if (!hasFileAttachment.value || !('fileId' in props.message)) return 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 {
|
return {
|
||||||
id: props.message.fileId!,
|
id: props.message.fileId!,
|
||||||
channel_id: props.message.channel_id,
|
channel_id: props.message.channel_id,
|
||||||
message_id: props.message.id,
|
message_id: props.message.id,
|
||||||
file_path: props.message.filePath!,
|
file_path: props.message.filePath!,
|
||||||
file_type: props.message.fileType!,
|
file_type: props.message.fileType || 'application/octet-stream',
|
||||||
file_size: props.message.fileSize!,
|
file_size: props.message.fileSize || 0,
|
||||||
original_name: props.message.originalName!,
|
original_name: props.message.originalName!,
|
||||||
created_at: props.message.fileCreatedAt || props.message.created_at
|
created_at: props.message.fileCreatedAt || props.message.created_at
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const formatTime = (timestamp: string): string => {
|
// formatTime function removed - now using formatSmartTimestamp from utils
|
||||||
return new Date(timestamp).toLocaleTimeString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create comprehensive aria-label for screen readers
|
// Create comprehensive aria-label for screen readers
|
||||||
const messageAriaLabel = computed(() => {
|
const messageAriaLabel = computed(() => {
|
||||||
@@ -95,8 +109,8 @@ const messageAriaLabel = computed(() => {
|
|||||||
|
|
||||||
// Add timestamp
|
// Add timestamp
|
||||||
if ('created_at' in props.message && props.message.created_at) {
|
if ('created_at' in props.message && props.message.created_at) {
|
||||||
const time = formatTime(props.message.created_at)
|
const time = formatTimestampForScreenReader(props.message.created_at)
|
||||||
label += `. Sent at ${time}`
|
label += `. Sent ${time}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add status for unsent messages
|
// Add status for unsent messages
|
||||||
|
@@ -58,7 +58,7 @@
|
|||||||
{{ result.content }}
|
{{ result.content }}
|
||||||
</div>
|
</div>
|
||||||
<div class="result-time">
|
<div class="result-time">
|
||||||
{{ formatTime(result.created_at) }}
|
{{ formatSmartTimestamp(result.created_at) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,6 +79,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { useToastStore } from '@/stores/toast'
|
import { useToastStore } from '@/stores/toast'
|
||||||
import { apiService } from '@/services/api'
|
import { apiService } from '@/services/api'
|
||||||
|
import { formatSmartTimestamp } from '@/utils/time'
|
||||||
import BaseInput from '@/components/base/BaseInput.vue'
|
import BaseInput from '@/components/base/BaseInput.vue'
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
import type { Message, ExtendedMessage } from '@/types'
|
import type { Message, ExtendedMessage } from '@/types'
|
||||||
@@ -140,16 +141,7 @@ const getChannelName = (channelId: number): string => {
|
|||||||
return channel?.name || `Channel ${channelId}`
|
return channel?.name || `Channel ${channelId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatTime = (timestamp: string): string => {
|
// formatTime function removed - now using formatSmartTimestamp from utils
|
||||||
if (!timestamp) return 'Unknown time'
|
|
||||||
|
|
||||||
const date = new Date(timestamp)
|
|
||||||
if (isNaN(date.getTime())) {
|
|
||||||
return 'Invalid date'
|
|
||||||
}
|
|
||||||
|
|
||||||
return date.toLocaleString()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
searchInput.value?.focus()
|
searchInput.value?.focus()
|
||||||
|
94
frontend-vue/src/utils/time.ts
Normal file
94
frontend-vue/src/utils/time.ts
Normal file
@@ -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'
|
||||||
|
})
|
||||||
|
}
|
@@ -130,6 +130,7 @@ import { useOfflineSync } from '@/composables/useOfflineSync'
|
|||||||
import { useWebSocket } from '@/composables/useWebSocket'
|
import { useWebSocket } from '@/composables/useWebSocket'
|
||||||
import { useKeyboardShortcuts } from '@/composables/useKeyboardShortcuts'
|
import { useKeyboardShortcuts } from '@/composables/useKeyboardShortcuts'
|
||||||
import { useAudio } from '@/composables/useAudio'
|
import { useAudio } from '@/composables/useAudio'
|
||||||
|
import { formatTimestampForScreenReader } from '@/utils/time'
|
||||||
import { apiService } from '@/services/api'
|
import { apiService } from '@/services/api'
|
||||||
import { syncService } from '@/services/sync'
|
import { syncService } from '@/services/sync'
|
||||||
|
|
||||||
@@ -364,9 +365,7 @@ const handleSelectMessage = async (message: ExtendedMessage) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatTime = (timestamp: string): string => {
|
// formatTime function removed - now using formatTimestampForScreenReader from utils
|
||||||
return new Date(timestamp).toLocaleTimeString()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleVoiceSent = () => {
|
const handleVoiceSent = () => {
|
||||||
// Voice message was sent successfully
|
// Voice message was sent successfully
|
||||||
@@ -396,8 +395,8 @@ const announceLastMessage = (position: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const message = messages[messageIndex]
|
const message = messages[messageIndex]
|
||||||
const timeStr = formatTime(message.created_at)
|
const timeStr = formatTimestampForScreenReader(message.created_at)
|
||||||
const announcement = `${message.content}; ${timeStr}`
|
const announcement = `${message.content}; sent ${timeStr}`
|
||||||
|
|
||||||
toastStore.info(announcement)
|
toastStore.info(announcement)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user