Fix some mobile layout problems

This commit is contained in:
2025-08-12 14:16:05 +02:00
parent 8aeef63886
commit 864f0a5a45
6 changed files with 237 additions and 34 deletions

View File

@@ -278,7 +278,21 @@ const sendPhoto = async () => {
}) })
// Upload photo // Upload photo
await apiService.uploadFile(appStore.currentChannelId!, message.id, file) const uploadedFile = await apiService.uploadFile(appStore.currentChannelId!, message.id, file)
// Immediately update the local message with file metadata
const updatedMessage = {
...message,
fileId: uploadedFile.id,
filePath: uploadedFile.file_path,
fileType: uploadedFile.file_type,
fileSize: uploadedFile.file_size,
originalName: uploadedFile.original_name,
fileCreatedAt: uploadedFile.created_at
}
// Update the message in the store
appStore.updateMessage(message.id, updatedMessage)
toastStore.success('Photo sent!') toastStore.success('Photo sent!')
emit('sent') emit('sent')

View File

@@ -139,25 +139,44 @@ const uploadFiles = async () => {
error.value = '' error.value = ''
try { try {
// Create a message first to attach files to // For single file, use the filename as message content
const message = await apiService.createMessage(appStore.currentChannelId, // For multiple files, show count
`Uploaded ${selectedFiles.value.length} file${selectedFiles.value.length === 1 ? '' : 's'}`) const messageContent = selectedFiles.value.length === 1
? selectedFiles.value[0].name
// Upload each file : `Uploaded ${selectedFiles.value.length} files`
for (let i = 0; i < selectedFiles.value.length; i++) {
const file = selectedFiles.value[i]
try { // Create a message first to attach files to
await apiService.uploadFile(appStore.currentChannelId, message.id, file) const message = await apiService.createMessage(appStore.currentChannelId, messageContent)
uploadProgress.value[i] = 100
} catch (fileError) { // Upload the first file (backend uses single file per message)
console.error(`Failed to upload ${file.name}:`, fileError) const file = selectedFiles.value[0]
toastStore.error(`Failed to upload ${file.name}`)
uploadProgress.value[i] = 0 try {
const uploadedFile = await apiService.uploadFile(appStore.currentChannelId, message.id, file)
uploadProgress.value[0] = 100
// Immediately update the local message with file metadata
const updatedMessage = {
...message,
fileId: uploadedFile.id,
filePath: uploadedFile.file_path,
fileType: uploadedFile.file_type,
fileSize: uploadedFile.file_size,
originalName: uploadedFile.original_name,
fileCreatedAt: uploadedFile.created_at
} }
// Update the message in the store
appStore.updateMessage(message.id, updatedMessage)
toastStore.success('File uploaded successfully!')
} catch (fileError) {
console.error(`Failed to upload ${file.name}:`, fileError)
toastStore.error(`Failed to upload ${file.name}`)
uploadProgress.value[0] = 0
} }
toastStore.success('Files uploaded successfully!')
emit('uploaded') emit('uploaded')
} catch (err) { } catch (err) {

View File

@@ -186,7 +186,21 @@ const sendVoiceMessage = async () => {
}) })
// Upload voice file // Upload voice file
await apiService.uploadFile(appStore.currentChannelId!, message.id, file) const uploadedFile = await apiService.uploadFile(appStore.currentChannelId!, message.id, file)
// Immediately update the local message with file metadata
const updatedMessage = {
...message,
fileId: uploadedFile.id,
filePath: uploadedFile.file_path,
fileType: uploadedFile.file_type,
fileSize: uploadedFile.file_size,
originalName: uploadedFile.original_name,
fileCreatedAt: uploadedFile.created_at
}
// Update the message in the store
appStore.updateMessage(message.id, updatedMessage)
toastStore.success('Voice message sent!') toastStore.success('Voice message sent!')
clearRecording() clearRecording()

View File

@@ -19,7 +19,14 @@ export function useWebSocket() {
channel_id: parseInt(data.channelId), // Convert channelId string to channel_id number channel_id: parseInt(data.channelId), // Convert channelId string to channel_id number
content: data.content, content: data.content,
created_at: data.createdAt || new Date().toISOString(), created_at: data.createdAt || new Date().toISOString(),
file_id: data.fileId file_id: data.fileId,
// Handle flattened file fields
fileId: data.fileId,
filePath: data.filePath,
fileType: data.fileType,
fileSize: data.fileSize,
originalName: data.originalName,
fileCreatedAt: data.fileCreatedAt
} }
console.log('WebSocket: Transformed message:', message) console.log('WebSocket: Transformed message:', message)
console.log('Transformed content:', JSON.stringify(message.content)) console.log('Transformed content:', JSON.stringify(message.content))
@@ -32,26 +39,41 @@ export function useWebSocket() {
} }
} }
const handleMessageUpdated = (data: { id: string, content: string }) => { const handleMessageUpdated = (data: any) => {
appStore.updateMessage(parseInt(data.id), { content: data.content }) // Handle full message updates including file metadata
const messageUpdate: Partial<ExtendedMessage> = {
content: data.content
}
// Handle flattened file fields from server
if (data.fileId) {
messageUpdate.fileId = data.fileId
messageUpdate.filePath = data.filePath
messageUpdate.fileType = data.fileType
messageUpdate.fileSize = data.fileSize
messageUpdate.originalName = data.originalName
messageUpdate.fileCreatedAt = data.fileCreatedAt
}
appStore.updateMessage(parseInt(data.id), messageUpdate)
} }
const handleMessageDeleted = (data: { id: string }) => { const handleMessageDeleted = (data: { id: string }) => {
appStore.removeMessage(parseInt(data.id)) appStore.removeMessage(parseInt(data.id))
} }
const handleFileUploaded = (data: FileAttachment) => { const handleFileUploaded = (data: any) => {
// Find the message and add the file to it // Handle file upload events with flattened format
const channelMessages = appStore.messages[data.channel_id] || [] const messageUpdate: Partial<ExtendedMessage> = {
const messageIndex = channelMessages.findIndex(m => m.id === data.message_id) fileId: data.fileId,
if (messageIndex !== -1) { filePath: data.filePath,
const message = channelMessages[messageIndex] fileType: data.fileType,
const updatedMessage = { fileSize: data.fileSize,
...message, originalName: data.originalName,
files: [...(message.files || []), data] fileCreatedAt: data.fileCreatedAt
}
appStore.updateMessage(message.id, updatedMessage)
} }
appStore.updateMessage(data.message_id, messageUpdate)
} }
const handleChannelCreated = (data: { channel: Channel }) => { const handleChannelCreated = (data: { channel: Channel }) => {

View File

@@ -40,7 +40,7 @@ export class SyncService {
}) })
// Add/update with server messages (server wins for conflicts) // Add/update with server messages (server wins for conflicts)
serverMessages.forEach(msg => { serverMessages.forEach((msg: any) => {
// Transform server message format to match our types // Transform server message format to match our types
const transformedMsg: ExtendedMessage = { const transformedMsg: ExtendedMessage = {
id: msg.id, id: msg.id,

View File

@@ -1,12 +1,39 @@
<template> <template>
<div class="main-view"> <div class="main-view">
<!-- Mobile Header -->
<header class="mobile-header">
<button
class="mobile-menu-button"
@click="sidebarOpen = !sidebarOpen"
:aria-label="sidebarOpen ? 'Close menu' : 'Open menu'"
>
<Icon name="menu" />
</button>
<h1 class="mobile-title">{{ appStore.currentChannel?.name || 'Notebrook' }}</h1>
<button
class="mobile-search-button"
@click="showSearchDialog = true"
aria-label="Search messages"
>
<Icon name="search" />
</button>
</header>
<!-- Sidebar Overlay -->
<div
v-if="sidebarOpen"
class="sidebar-overlay"
@click="sidebarOpen = false"
></div>
<!-- Sidebar --> <!-- Sidebar -->
<Sidebar <Sidebar
:class="{ 'sidebar-open': sidebarOpen }"
:channels="appStore.channels" :channels="appStore.channels"
:current-channel-id="appStore.currentChannelId" :current-channel-id="appStore.currentChannelId"
:unread-counts="unreadCounts" :unread-counts="unreadCounts"
@create-channel="showChannelDialog = true" @create-channel="showChannelDialog = true"
@select-channel="selectChannel" @select-channel="(id) => { selectChannel(id); sidebarOpen = false }"
@channel-info="handleChannelInfo" @channel-info="handleChannelInfo"
@settings="showSettings = true" @settings="showSettings = true"
/> />
@@ -14,8 +41,9 @@
<!-- Main Content --> <!-- Main Content -->
<main class="main-content"> <main class="main-content">
<div v-if="appStore.currentChannel" class="chat-container"> <div v-if="appStore.currentChannel" class="chat-container">
<!-- Chat Header --> <!-- Chat Header (Desktop only) -->
<ChatHeader <ChatHeader
class="desktop-header"
:channel-name="appStore.currentChannel.name" :channel-name="appStore.currentChannel.name"
@search="showSearchDialog = true" @search="showSearchDialog = true"
/> />
@@ -107,6 +135,7 @@ import { syncService } from '@/services/sync'
// Components // Components
import BaseDialog from '@/components/base/BaseDialog.vue' import BaseDialog from '@/components/base/BaseDialog.vue'
import Icon from '@/components/base/Icon.vue'
import Sidebar from '@/components/sidebar/Sidebar.vue' import Sidebar from '@/components/sidebar/Sidebar.vue'
import ChatHeader from '@/components/chat/ChatHeader.vue' import ChatHeader from '@/components/chat/ChatHeader.vue'
import MessagesContainer from '@/components/chat/MessagesContainer.vue' import MessagesContainer from '@/components/chat/MessagesContainer.vue'
@@ -147,6 +176,9 @@ const showFileDialog = ref(false)
const showVoiceDialog = ref(false) const showVoiceDialog = ref(false)
const showCameraDialog = ref(false) const showCameraDialog = ref(false)
// Mobile sidebar state
const sidebarOpen = ref(false)
// Channel info state // Channel info state
const selectedChannelForInfo = ref<Channel | null>(null) const selectedChannelForInfo = ref<Channel | null>(null)
@@ -475,10 +507,112 @@ onMounted(async () => {
} }
} }
.mobile-header {
display: none;
align-items: center;
justify-content: space-between;
padding: 1rem;
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
position: sticky;
top: 0;
z-index: 100;
}
.mobile-menu-button,
.mobile-search-button {
background: none;
border: none;
padding: 0.5rem;
cursor: pointer;
color: #6b7280;
}
.mobile-menu-button:hover,
.mobile-search-button:hover {
color: #374151;
}
.mobile-title {
font-size: 1.125rem;
font-weight: 600;
margin: 0;
color: #111827;
}
.sidebar-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 200;
}
/* Responsive design */ /* Responsive design */
@media (max-width: 768px) { @media (max-width: 768px) {
.main-view { .main-view {
flex-direction: column; flex-direction: column;
height: 100vh;
}
.mobile-header {
display: flex;
flex-shrink: 0;
}
.sidebar {
position: fixed;
top: 0;
left: 0;
height: 100vh;
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 300;
}
.sidebar.sidebar-open {
transform: translateX(0);
}
.sidebar-overlay {
display: block;
}
.main-content {
flex: 1;
overflow: hidden;
}
.chat-container {
height: 100%;
}
.desktop-header {
display: none;
}
}
@media (prefers-color-scheme: dark) {
.mobile-header {
background: #1f2937;
border-bottom-color: #374151;
}
.mobile-title {
color: rgba(255, 255, 255, 0.87);
}
.mobile-menu-button,
.mobile-search-button {
color: rgba(255, 255, 255, 0.6);
}
.mobile-menu-button:hover,
.mobile-search-button:hover {
color: rgba(255, 255, 255, 0.87);
} }
} }
</style> </style>