Fix some mobile layout problems
This commit is contained in:
@@ -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')
|
||||||
|
@@ -139,25 +139,44 @@ const uploadFiles = async () => {
|
|||||||
error.value = ''
|
error.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// For single file, use the filename as message content
|
||||||
|
// For multiple files, show count
|
||||||
|
const messageContent = selectedFiles.value.length === 1
|
||||||
|
? selectedFiles.value[0].name
|
||||||
|
: `Uploaded ${selectedFiles.value.length} files`
|
||||||
|
|
||||||
// Create a message first to attach files to
|
// Create a message first to attach files to
|
||||||
const message = await apiService.createMessage(appStore.currentChannelId,
|
const message = await apiService.createMessage(appStore.currentChannelId, messageContent)
|
||||||
`Uploaded ${selectedFiles.value.length} file${selectedFiles.value.length === 1 ? '' : 's'}`)
|
|
||||||
|
|
||||||
// Upload each file
|
// Upload the first file (backend uses single file per message)
|
||||||
for (let i = 0; i < selectedFiles.value.length; i++) {
|
const file = selectedFiles.value[0]
|
||||||
const file = selectedFiles.value[i]
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiService.uploadFile(appStore.currentChannelId, message.id, file)
|
const uploadedFile = await apiService.uploadFile(appStore.currentChannelId, message.id, file)
|
||||||
uploadProgress.value[i] = 100
|
uploadProgress.value[0] = 100
|
||||||
} catch (fileError) {
|
|
||||||
console.error(`Failed to upload ${file.name}:`, fileError)
|
// Immediately update the local message with file metadata
|
||||||
toastStore.error(`Failed to upload ${file.name}`)
|
const updatedMessage = {
|
||||||
uploadProgress.value[i] = 0
|
...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) {
|
||||||
|
@@ -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()
|
||||||
|
@@ -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 }) => {
|
||||||
|
@@ -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,
|
||||||
|
@@ -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>
|
Reference in New Issue
Block a user