feat(chat): preserve message drafts in session temp store

This commit is contained in:
2026-10-15 17:45:17 +02:00
parent e933782731
commit 697f538b6e
8 changed files with 426 additions and 16 deletions

View File

@@ -1,9 +1,11 @@
<script setup>
const props = defineProps({ roomId: { type: String, required: true }, api: { type: Function, required: true }, base: { type: String, default: '/api/collaboration' }, embedded: { type: Boolean, default: false } })
const emit = defineEmits(['changed', 'unauthorized'])
const room = ref(null), messages = ref([]), members = ref([]), draft = ref(''), draftImage = ref(null), error = ref('')
const room = ref(null), messages = ref([]), members = ref([]), error = ref('')
// Guest access has separate credentials and must never reuse an internal user's drafts.
const { draft, draftImage, draftSending, clearDraft, startDraftSend, finishDraftSend } = useChatDraft(() => `collaboration:${props.roomId}`, { persist: () => props.base === '/api/collaboration' })
const loading = ref(false), sending = ref(false), olderLoading = ref(false), hasMore = ref(false), viewport = ref(null)
let generation = 0, timer, polling = false, pending = null
let generation = 0, timer, polling = false
const endpoint = (id, suffix = '') => `${props.base}/rooms/${encodeURIComponent(id)}${suffix}`
const merge = (incoming) => {
const byId = new Map(messages.value.map(m => [m.id, m]))
@@ -20,7 +22,7 @@ const scroll = async () => { await nextTick(); if (viewport.value) viewport.valu
function failed(e) {
error.value = messageError(e)
if ([401, 403, 404, 410].includes(e?.statusCode || e?.status)) {
room.value = null; messages.value = []; members.value = []; draftImage.value = null
room.value = null; messages.value = []; members.value = []
if ((e?.statusCode || e?.status) === 401) emit('unauthorized')
}
}
@@ -32,7 +34,7 @@ async function read(id, version) {
}
async function load() {
const version = ++generation, id = props.roomId
room.value = null; messages.value = []; members.value = []; draft.value = ''; draftImage.value = null; error.value = ''; pending = null
room.value = null; messages.value = []; members.value = []; error.value = ''
loading.value = true; sending.value = false; olderLoading.value = false
try {
const [detail, result, people] = await Promise.all([props.api(endpoint(id)), props.api(endpoint(id, '/messages')), props.api(endpoint(id, '/members'))])
@@ -78,15 +80,18 @@ async function send() {
const text = draft.value.trim(), image = draftImage.value
if ((!text && !image) || sending.value || !room.value?.canSend) return
const version = generation, id = props.roomId
if (!pending || pending.text !== text || pending.image !== image) pending = { text, image, clientId: crypto.randomUUID() }
const savedDraft = startDraftSend()
if (!savedDraft) return
const pending = { text, image, clientId: savedDraft.value.clientId }
sending.value = true; error.value = ''
try {
const message = await props.api(endpoint(id, '/messages'), { method: 'POST', body: pending })
clearDraft(savedDraft)
if (version !== generation) return
merge([message]); draft.value = ''; draftImage.value = null; pending = null
merge([message])
await scroll(); await read(id, version)
} catch (e) { if (version === generation) failed(e) }
finally { if (version === generation) sending.value = false }
finally { finishDraftSend(savedDraft); if (version === generation) sending.value = false }
}
const time = value => new Date(value).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short' })
watch(() => props.roomId, load, { immediate: true })
@@ -126,6 +131,6 @@ onBeforeUnmount(() => { ++generation; clearInterval(timer) })
</div>
</ChatReactions>
</div>
<ChatComposer v-if="room" :key="roomId" v-model="draft" v-model:image="draftImage" :busy="sending" :disabled="!room.canSend" :placeholder="room.canSend ? 'Nachricht schreiben …' : 'Dieser Unterraum ist nur lesbar'" @send="send" />
<ChatComposer v-if="room" :key="roomId" v-model="draft" v-model:image="draftImage" :busy="sending || draftSending" :disabled="!room.canSend" :placeholder="room.canSend ? 'Nachricht schreiben …' : 'Dieser Unterraum ist nur lesbar'" @send="send" />
</section>
</template>