feat(communication): federate tenant collaboration with signed delivery
This commit is contained in:
@@ -7,9 +7,14 @@ let generation = 0, timer, polling = false, pending = null
|
||||
const endpoint = (id, suffix = '') => `${props.base}/rooms/${encodeURIComponent(id)}${suffix}`
|
||||
const merge = (incoming) => {
|
||||
const byId = new Map(messages.value.map(m => [m.id, m]))
|
||||
incoming.forEach(m => byId.set(m.id, m))
|
||||
messages.value = [...byId.values()].sort((a, b) => a.seq - b.seq)
|
||||
incoming.forEach(m => {
|
||||
if (m.own && m.seq) for (const [id, old] of byId) if (old.own && old.clientId === m.clientId && !old.seq) byId.delete(id)
|
||||
if (!m.seq && [...byId.values()].some(old => old.own && old.seq && old.clientId === m.clientId)) return
|
||||
byId.set(m.id, m)
|
||||
})
|
||||
messages.value = [...byId.values()].sort((a, b) => (a.seq || Number.MAX_SAFE_INTEGER) - (b.seq || Number.MAX_SAFE_INTEGER))
|
||||
}
|
||||
const lastSequence = () => Math.max(0, ...messages.value.map(m => m.seq || 0))
|
||||
const messageError = e => e?.data?.message || e?.data?.error || 'Unterraum konnte nicht geladen werden.'
|
||||
const scroll = async () => { await nextTick(); if (viewport.value) viewport.value.scrollTop = viewport.value.scrollHeight }
|
||||
function failed(e) {
|
||||
@@ -21,7 +26,8 @@ function failed(e) {
|
||||
}
|
||||
async function read(id, version) {
|
||||
if (version !== generation || document.hidden || !messages.value.length) return
|
||||
await props.api(endpoint(id, '/read'), { method: 'POST', body: { seq: messages.value.at(-1).seq } })
|
||||
if (!lastSequence()) return
|
||||
await props.api(endpoint(id, '/read'), { method: 'POST', body: { seq: lastSequence() } })
|
||||
if (version === generation) emit('changed')
|
||||
}
|
||||
async function load() {
|
||||
@@ -41,10 +47,12 @@ async function poll() {
|
||||
const version = generation, id = props.roomId
|
||||
polling = true
|
||||
try {
|
||||
const after = messages.value.at(-1)?.seq || 0
|
||||
const after = lastSequence()
|
||||
const [detail, result] = await Promise.all([props.api(endpoint(id)), props.api(endpoint(id, `/messages?after=${after}`))])
|
||||
if (version !== generation) return
|
||||
const rightsChanged = room.value?.revision !== detail.revision
|
||||
room.value = detail
|
||||
messages.value = messages.value.filter(m => m.seq ? m.seq >= detail.historyFromSeq : !rightsChanged)
|
||||
const nearBottom = !viewport.value || viewport.value.scrollHeight - viewport.value.scrollTop - viewport.value.clientHeight < 90
|
||||
if (result.messages.length) {
|
||||
merge(result.messages)
|
||||
@@ -94,6 +102,7 @@ onBeforeUnmount(() => { ++generation; clearInterval(timer) })
|
||||
<p v-if="members.length" class="mt-2 text-xs text-gray-500">{{ members.map(m => `${m.name}${m.kind === 'guest' ? ' (Gast)' : ''}`).join(' · ') }}</p>
|
||||
</header>
|
||||
<p v-if="error" role="alert" class="m-4 rounded-lg bg-red-50 p-3 text-sm text-red-700">{{ error }} <button class="ml-2 underline" @click="load">Erneut laden</button></p>
|
||||
<p v-if="room?.offline" role="status" class="mx-4 mt-4 rounded-lg bg-amber-50 p-3 text-sm text-amber-900">Die Partnerinstanz ist gerade nicht erreichbar. Du siehst bereits geladene Nachrichten. Neue Nachrichten warten auf Zustellung.</p>
|
||||
<div v-if="room?.overview && Object.keys(room.overview).length" class="m-4 rounded-lg bg-green-50 p-3 text-sm text-gray-800">
|
||||
<h3 class="mb-1 font-semibold">Freigegebene Projektinformationen</h3>
|
||||
<p v-if="room.overview.title">{{ room.overview.title }}</p>
|
||||
@@ -109,7 +118,7 @@ onBeforeUnmount(() => { ++generation; clearInterval(timer) })
|
||||
<div class="max-w-[85%] rounded-xl px-4 py-2" :class="message.own ? 'bg-green-700 text-white' : 'bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-100'">
|
||||
<p class="text-xs font-semibold">{{ message.own ? 'Du' : message.senderName }}</p>
|
||||
<p class="whitespace-pre-wrap break-words text-sm">{{ message.body }}</p>
|
||||
<p class="mt-1 text-right text-[10px] opacity-70">{{ time(message.createdAt) }}</p>
|
||||
<p class="mt-1 text-right text-[10px] opacity-70">{{ time(message.createdAt) }}{{ message.pending ? ' · Wartet auf Zustellung' : message.failed ? ' · Zustellung fehlgeschlagen' : ' · Bestätigt' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user