feat(communication): add room visibility and project chat controls
This commit is contained in:
@@ -12,6 +12,8 @@ const requestedRoom = () => typeof route.query.room === 'string' ? route.query.r
|
||||
const activeRoomKey = ref(requestedRoom())
|
||||
const loading = ref(true), roomLoading = ref(false), sending = ref(false), creating = ref(false)
|
||||
const showRoomForm = ref(false), messagesViewport = ref(null)
|
||||
const hiddenRoomKeys = ref([]), showHiddenRooms = ref(false), preferenceBusy = ref(''), preferenceError = ref('')
|
||||
const preferencesReady = ref(false)
|
||||
const roomForm = reactive({ name: '', key: '', topic: '' })
|
||||
const session = computed(() => `${auth.user?.id || auth.user?.user_id || ''}:${auth.activeTenant || ''}`)
|
||||
const hasSession = computed(() => !!auth.user && !!auth.activeTenant)
|
||||
@@ -38,10 +40,12 @@ const decoratedRooms = computed(() => [
|
||||
...collaborationRooms.value.map(room => ({ ...room, key: collaborationKey(room.id), source: 'collaboration', exists: true,
|
||||
group: 'Zusammenarbeit', icon: 'i-heroicons-user-group', unread: room.unread || 0 }))
|
||||
])
|
||||
const visibleRooms = computed(() => decoratedRooms.value.filter(room => !hiddenRoomKeys.value.includes(room.key)))
|
||||
const hiddenRooms = computed(() => decoratedRooms.value.filter(room => hiddenRoomKeys.value.includes(room.key)))
|
||||
const groupedRooms = computed(() => ['Räume', 'Projekte', 'Direktnachrichten', 'Zusammenarbeit']
|
||||
.map(label => ({ label, rooms: decoratedRooms.value.filter(room => room.group === label) }))
|
||||
.map(label => ({ label, rooms: visibleRooms.value.filter(room => room.group === label) }))
|
||||
.filter(group => group.rooms.length))
|
||||
const activeRoom = computed(() => decoratedRooms.value.find(room => room.key === activeRoomKey.value) || null)
|
||||
const activeRoom = computed(() => visibleRooms.value.find(room => room.key === activeRoomKey.value) || null)
|
||||
const isCollaboration = computed(() => activeRoom.value?.source === 'collaboration')
|
||||
const isInternal = computed(() => activeRoom.value?.source === 'internal')
|
||||
|
||||
@@ -96,6 +100,40 @@ async function refreshLists() {
|
||||
await Promise.all([loadRoomLists(scope), loadUnread(scope), loadCollaboration(scope)])
|
||||
}
|
||||
|
||||
async function loadRoomPreferences(scope = scopeVersion) {
|
||||
if (!hasSession.value || !currentScope(scope)) return
|
||||
try {
|
||||
const result = await $api('/api/communication/chat/room-preferences')
|
||||
if (!currentScope(scope)) return
|
||||
hiddenRoomKeys.value = result.hiddenRoomKeys || []
|
||||
preferencesReady.value = true; preferenceError.value = ''
|
||||
} catch (error) {
|
||||
if (currentScope(scope)) preferenceError.value = errorMessage(error)
|
||||
}
|
||||
}
|
||||
async function setRoomHidden(room, hidden) {
|
||||
if (preferenceBusy.value || !preferencesReady.value) return
|
||||
const scope = scopeVersion
|
||||
preferenceBusy.value = room.key
|
||||
try {
|
||||
await $api('/api/communication/chat/room-preferences', { method: 'PUT', body: { roomKey: room.key, hidden } })
|
||||
if (!currentScope(scope)) return
|
||||
hiddenRoomKeys.value = hidden
|
||||
? [...new Set([...hiddenRoomKeys.value, room.key])]
|
||||
: hiddenRoomKeys.value.filter(key => key !== room.key)
|
||||
if (hidden && activeRoomKey.value === room.key) {
|
||||
++roomVersion; activeRoomKey.value = ''; clearMessages()
|
||||
const next = visibleRooms.value.find(candidate => candidate.exists)
|
||||
if (next) await selectRoom(next)
|
||||
else await router.replace({ query: { ...route.query, room: undefined } })
|
||||
} else if (!hidden) {
|
||||
const restored = visibleRooms.value.find(candidate => candidate.key === room.key)
|
||||
if (restored) await selectRoom(restored)
|
||||
}
|
||||
} catch (error) { if (currentScope(scope)) toast.add({ title: errorMessage(error), color: 'error' }) }
|
||||
finally { if (currentScope(scope)) preferenceBusy.value = '' }
|
||||
}
|
||||
|
||||
async function markRead(request) {
|
||||
if (!currentSelection(request) || !isInternal.value || !activeRoom.value?.exists) return
|
||||
const messageId = Number(messages.value.at(-1)?.id || 0)
|
||||
@@ -119,6 +157,7 @@ async function loadActiveRoom(request = selection()) {
|
||||
} finally { if (currentSelection(request)) roomLoading.value = false }
|
||||
}
|
||||
async function selectRoom(room) {
|
||||
if (hiddenRoomKeys.value.includes(room.key)) return
|
||||
++roomVersion; activeRoomKey.value = room.key; clearMessages()
|
||||
let request = selection()
|
||||
try {
|
||||
@@ -199,18 +238,22 @@ async function initialize() {
|
||||
++roomVersion; clearMessages()
|
||||
baseRooms.value = []; projectRooms.value = []; directRooms.value = []; collaborationRooms.value = []; invitations.value = []
|
||||
unreadRooms.value = {}; invitationBusy.value = ''; collaborationError.value = ''; listError.value = ''; creating.value = false
|
||||
hiddenRoomKeys.value = []; showHiddenRooms.value = false; preferenceBusy.value = ''; preferenceError.value = ''; preferencesReady.value = false
|
||||
showRoomForm.value = false; Object.assign(roomForm, { name: '', key: '', topic: '' })
|
||||
activeRoomKey.value = requestedRoom(); loading.value = true
|
||||
await refreshLists()
|
||||
await Promise.all([refreshLists(), loadRoomPreferences(scope)])
|
||||
if (!currentScope(scope)) return
|
||||
loading.value = false
|
||||
const room = activeRoom.value || (activeRoomKey.value === 'allgemein' ? decoratedRooms.value[0] : null)
|
||||
const room = activeRoom.value || (activeRoomKey.value === 'allgemein' ? visibleRooms.value.find(room => room.exists) : null)
|
||||
if (room) await selectRoom(room)
|
||||
}
|
||||
watch(session, () => { if (mounted) void initialize() }, { flush: 'sync' })
|
||||
watch(() => activeRoom.value?.key, (key, previous) => {
|
||||
if (!key && previous && !loading.value) { ++roomVersion; clearMessages() }
|
||||
})
|
||||
watch(() => route.query.room, () => {
|
||||
if (!mounted || loading.value || requestedRoom() === activeRoomKey.value) return
|
||||
const room = decoratedRooms.value.find(room => room.key === requestedRoom())
|
||||
const room = visibleRooms.value.find(room => room.key === requestedRoom())
|
||||
if (room) void selectRoom(room)
|
||||
else { ++roomVersion; activeRoomKey.value = requestedRoom(); clearMessages() }
|
||||
})
|
||||
@@ -223,8 +266,8 @@ onBeforeUnmount(() => { disposed = true; ++scopeVersion; ++roomVersion; clearInt
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex min-h-0 flex-1 bg-gray-50 p-4 sm:p-6">
|
||||
<div class="mx-auto grid h-[calc(100vh-8rem)] w-full max-w-7xl grid-cols-1 grid-rows-[minmax(8rem,30%)_minmax(0,1fr)] overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm lg:grid-rows-1"
|
||||
<div class="flex h-full min-h-0 min-w-0 w-full flex-1 overflow-hidden bg-white">
|
||||
<div class="grid h-full min-h-0 w-full flex-1 grid-cols-1 grid-rows-[minmax(8rem,30%)_minmax(0,1fr)] overflow-hidden lg:grid-rows-1"
|
||||
:class="isCollaboration ? 'lg:grid-cols-[19rem_minmax(0,1fr)]' : 'lg:grid-cols-[19rem_minmax(0,1fr)_15rem]'">
|
||||
<aside class="min-h-0 overflow-y-auto border-r border-gray-200 p-4">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
@@ -248,6 +291,7 @@ onBeforeUnmount(() => { disposed = true; ++scopeVersion; ++roomVersion; clearInt
|
||||
<UButton to="/communication/collaboration" icon="i-heroicons-user-group" color="neutral" variant="outline" size="xs" class="mb-4 w-full justify-center">Unterräume und Freigaben</UButton>
|
||||
<p v-if="listError" role="alert" class="mb-3 text-xs text-red-700">{{ listError }} <button class="underline" @click="loadRoomLists()">Erneut laden</button></p>
|
||||
<p v-if="collaborationError" role="alert" class="mb-3 text-xs text-red-700">Zusammenarbeit: {{ collaborationError }} <button class="underline" @click="loadCollaboration()">Erneut laden</button></p>
|
||||
<p v-if="preferenceError" role="alert" class="mb-3 text-xs text-red-700">Raumeinstellungen: {{ preferenceError }} <button class="underline" @click="loadRoomPreferences()">Erneut laden</button></p>
|
||||
<section v-if="invitations.length" class="mb-5 space-y-2" aria-label="Einladungen">
|
||||
<h2 class="px-2 text-xs font-semibold uppercase tracking-wide text-gray-400">Einladungen</h2>
|
||||
<div v-for="invitation in invitations" :key="invitation.id" class="rounded-lg border border-primary-200 bg-primary-50 p-3">
|
||||
@@ -265,26 +309,43 @@ onBeforeUnmount(() => { disposed = true; ++scopeVersion; ++roomVersion; clearInt
|
||||
<div v-else class="space-y-5">
|
||||
<section v-for="group in groupedRooms" :key="group.label">
|
||||
<h2 class="mb-1 px-2 text-xs font-semibold uppercase tracking-wide text-gray-400">{{ group.label }}</h2>
|
||||
<button
|
||||
<div
|
||||
v-for="room in group.rooms"
|
||||
:key="room.key"
|
||||
type="button"
|
||||
class="mb-1 flex w-full items-center gap-3 rounded-lg px-2 py-2 text-left transition hover:bg-gray-100"
|
||||
class="mb-1 flex w-full items-center rounded-lg transition hover:bg-gray-100"
|
||||
:class="room.key === activeRoomKey ? 'bg-primary-50 text-primary-700' : 'text-gray-700'"
|
||||
@click="selectRoom(room)"
|
||||
>
|
||||
<UIcon :name="room.icon" class="size-5 shrink-0" />
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate text-sm font-medium">{{ room.name }}</span>
|
||||
<span class="block truncate text-xs text-gray-400">{{ room.source === 'collaboration' ? room.ownerName || 'Projekt-Unterraum' : room.topic }}{{ room.remote ? ' · Extern' : '' }}{{ room.status === 'archived' ? ' · Archiviert' : '' }}</span>
|
||||
</span>
|
||||
<UBadge v-if="room.unread" color="primary" size="xs">{{ room.unread }}</UBadge>
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-w-0 flex-1 items-center gap-3 px-2 py-2 text-left"
|
||||
@click="selectRoom(room)"
|
||||
>
|
||||
<UIcon :name="room.icon" class="size-5 shrink-0" />
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate text-sm font-medium">{{ room.name }}</span>
|
||||
<span class="block truncate text-xs text-gray-400">{{ room.source === 'collaboration' ? room.ownerName || 'Projekt-Unterraum' : room.topic }}{{ room.remote ? ' · Extern' : '' }}{{ room.status === 'archived' ? ' · Archiviert' : '' }}</span>
|
||||
</span>
|
||||
<UBadge v-if="room.unread" color="primary" size="xs">{{ room.unread }}</UBadge>
|
||||
</button>
|
||||
<UButton icon="i-heroicons-eye-slash" color="neutral" variant="ghost" size="xs" class="mr-1 shrink-0" :aria-label="`${room.name} ausblenden`" title="Für mich ausblenden" :disabled="!preferencesReady || !!preferenceBusy" @click="setRoomHidden(room, true)" />
|
||||
</div>
|
||||
</section>
|
||||
<p v-if="!visibleRooms.length" class="px-2 text-sm text-gray-500">Keine sichtbaren Räume. Du kannst ausgeblendete Räume unten wieder einblenden.</p>
|
||||
<section v-if="hiddenRooms.length" aria-label="Ausgeblendete Räume">
|
||||
<button type="button" class="flex w-full items-center justify-between px-2 py-2 text-left text-xs font-semibold text-gray-500" :aria-expanded="showHiddenRooms" @click="showHiddenRooms = !showHiddenRooms">
|
||||
<span>Ausgeblendete Räume ({{ hiddenRooms.length }})</span><UIcon :name="showHiddenRooms ? 'i-heroicons-chevron-up' : 'i-heroicons-chevron-down'" />
|
||||
</button>
|
||||
<div v-if="showHiddenRooms" class="space-y-1">
|
||||
<div v-for="room in hiddenRooms" :key="room.key" class="flex items-center gap-2 px-2 py-1">
|
||||
<span class="min-w-0 flex-1 truncate text-sm text-gray-500">{{ room.name }}</span>
|
||||
<UButton icon="i-heroicons-eye" variant="ghost" color="neutral" size="xs" :aria-label="`${room.name} einblenden`" :disabled="!!preferenceBusy" @click="setRoomHidden(room, false)">Einblenden</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<CollaborationRoom v-if="isCollaboration" :key="`${session}:${activeRoom.id}`" :room-id="activeRoom.id" :api="$api" embedded @changed="loadCollaboration()" @unauthorized="loadCollaboration()">
|
||||
<CollaborationRoom v-if="!loading && isCollaboration" :key="`${session}:${activeRoom.id}`" :room-id="activeRoom.id" :api="$api" embedded @changed="loadCollaboration()" @unauthorized="loadCollaboration()">
|
||||
<template #actions><UButton :to="{ path: '/communication/collaboration', query: { room: activeRoom.id } }" color="neutral" variant="outline" size="xs">Details und Freigaben</UButton></template>
|
||||
</CollaborationRoom>
|
||||
<main v-else class="flex min-h-0 flex-col">
|
||||
|
||||
Reference in New Issue
Block a user