Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
301 lines
11 KiB
TypeScript
301 lines
11 KiB
TypeScript
import type { DmConversation, DmGroup, DmMessage, DmReactionEvent, DmUser } from '~~/shared/utils/messages'
|
|
|
|
interface DmEvent {
|
|
message: DmMessage
|
|
sender: DmUser
|
|
recipient?: DmUser | null
|
|
group?: DmGroup | null
|
|
}
|
|
|
|
interface DmReadEvent {
|
|
reader: number
|
|
partner: number
|
|
at: string
|
|
}
|
|
|
|
interface DmGroupReadEvent {
|
|
reader: number
|
|
groupId: number
|
|
at: string
|
|
}
|
|
|
|
export interface FloatingChatTab {
|
|
user: DmUser
|
|
minimized: boolean
|
|
}
|
|
|
|
type MessageListener = (msg: DmMessage, other: DmUser | null, group: DmGroup | null) => void
|
|
type ReadListener = (partnerId: number, at: string) => void
|
|
type ReactionListener = (e: DmReactionEvent) => void
|
|
|
|
// Direkt-Callbacks der offenen Chat-Ansicht - Modul-Singletons, weil
|
|
// Plugin (WS-Event) und Page (Registrierung) verschiedene Composable-Aufrufe nutzen
|
|
const liveMessageHandler = shallowRef<MessageListener | null>(null)
|
|
const liveReadHandler = shallowRef<ReadListener | null>(null)
|
|
const liveReactionHandler = shallowRef<ReactionListener | null>(null)
|
|
const liveGroupHandler = shallowRef<((group: DmGroup) => void) | null>(null)
|
|
|
|
// Zusätzliche Listener für schwebende Messenger-Fenster (mehrere gleichzeitig möglich)
|
|
const floatingMessageListeners = new Set<MessageListener>()
|
|
const floatingReadListeners = new Set<ReadListener>()
|
|
const floatingReactionListeners = new Set<ReactionListener>()
|
|
|
|
const MAX_FLOATING_CHATS = 3
|
|
|
|
export function useDirectMessages() {
|
|
const { user } = useAuth()
|
|
const conversations = useState<DmConversation[]>('dm:conversations', () => [])
|
|
const unreadTotal = useState<number>('dm:unread', () => 0)
|
|
// Aktuell geöffnete Konversation (messages-Seite setzt/löscht das)
|
|
const openPartnerId = useState<number | null>('dm:open', () => null)
|
|
const openGroupId = useState<number | null>('dm:open-group', () => null)
|
|
// Schwebende 1:1-Chat-Fenster (Messenger-Stil)
|
|
const floatingChats = useState<FloatingChatTab[]>('dm:floating', () => [])
|
|
|
|
function isPartnerOpen(partnerId: number): boolean {
|
|
if (openPartnerId.value === partnerId) return true
|
|
return floatingChats.value.some(c => c.user.id === partnerId && !c.minimized)
|
|
}
|
|
|
|
function openFloatingChat(partner: DmUser, minimized = false) {
|
|
if (!user.value || partner.id === user.value.id) return
|
|
const existing = floatingChats.value.find(c => c.user.id === partner.id)
|
|
if (existing) {
|
|
existing.user = { ...existing.user, ...partner }
|
|
existing.minimized = minimized
|
|
return
|
|
}
|
|
const next = [...floatingChats.value, { user: partner, minimized }]
|
|
if (next.length > MAX_FLOATING_CHATS) next.shift()
|
|
floatingChats.value = next
|
|
}
|
|
|
|
function toggleMinimizeFloatingChat(userId: number) {
|
|
const tab = floatingChats.value.find(c => c.user.id === userId)
|
|
if (tab) tab.minimized = !tab.minimized
|
|
}
|
|
|
|
function closeFloatingChat(userId: number) {
|
|
floatingChats.value = floatingChats.value.filter(c => c.user.id !== userId)
|
|
}
|
|
|
|
function subscribeFloating(handlers: {
|
|
onMessage?: MessageListener
|
|
onRead?: ReadListener
|
|
onReaction?: ReactionListener
|
|
}) {
|
|
if (handlers.onMessage) floatingMessageListeners.add(handlers.onMessage)
|
|
if (handlers.onRead) floatingReadListeners.add(handlers.onRead)
|
|
if (handlers.onReaction) floatingReactionListeners.add(handlers.onReaction)
|
|
return () => {
|
|
if (handlers.onMessage) floatingMessageListeners.delete(handlers.onMessage)
|
|
if (handlers.onRead) floatingReadListeners.delete(handlers.onRead)
|
|
if (handlers.onReaction) floatingReactionListeners.delete(handlers.onReaction)
|
|
}
|
|
}
|
|
|
|
function notifyMessageListeners(msg: DmMessage, other: DmUser | null, group: DmGroup | null) {
|
|
liveMessageHandler.value?.(msg, other, group)
|
|
for (const fn of floatingMessageListeners) fn(msg, other, group)
|
|
}
|
|
|
|
function notifyReadListeners(partnerId: number, at: string) {
|
|
liveReadHandler.value?.(partnerId, at)
|
|
for (const fn of floatingReadListeners) fn(partnerId, at)
|
|
}
|
|
|
|
function notifyReactionListeners(e: DmReactionEvent) {
|
|
liveReactionHandler.value?.(e)
|
|
for (const fn of floatingReactionListeners) fn(e)
|
|
}
|
|
|
|
async function fetchConversations() {
|
|
const res = await $fetch<{ conversations: DmConversation[], unreadTotal: number }>('/api/messages')
|
|
conversations.value = res.conversations
|
|
unreadTotal.value = res.unreadTotal
|
|
}
|
|
|
|
function upsertDm(other: DmUser, msg: DmMessage, unreadDelta: number) {
|
|
const existing = conversations.value.find(c => c.kind === 'dm' && c.user?.id === other.id)
|
|
conversations.value = [{
|
|
kind: 'dm',
|
|
user: existing?.user ?? other,
|
|
group: null,
|
|
lastMessage: msg,
|
|
unread: Math.max(0, (existing?.unread ?? 0) + unreadDelta),
|
|
}, ...conversations.value.filter(c => !(c.kind === 'dm' && c.user?.id === other.id))]
|
|
}
|
|
|
|
function upsertGroup(group: DmGroup, msg: DmMessage | null, unreadDelta: number) {
|
|
const existing = conversations.value.find(c => c.kind === 'group' && c.group?.id === group.id)
|
|
conversations.value = [{
|
|
kind: 'group',
|
|
user: null,
|
|
group,
|
|
lastMessage: msg ?? existing?.lastMessage ?? null,
|
|
unread: Math.max(0, (existing?.unread ?? 0) + unreadDelta),
|
|
}, ...conversations.value.filter(c => !(c.kind === 'group' && c.group?.id === group.id))]
|
|
}
|
|
|
|
// Eingehende WS-Nachricht - Unread-Badge erhöht sich nur außerhalb der offenen Konversation
|
|
function handleIncoming(event: DmEvent) {
|
|
const { message: msg } = event
|
|
|
|
if (event.group) {
|
|
const g = event.group
|
|
const isMine = msg.senderId === user.value?.id
|
|
const isOpen = openGroupId.value === g.id
|
|
upsertGroup(g, msg, isMine || isOpen ? 0 : 1)
|
|
if (!isMine && !isOpen) unreadTotal.value += 1
|
|
if (isOpen) notifyMessageListeners(msg, null, g)
|
|
return
|
|
}
|
|
|
|
const isForMe = msg.recipientId === user.value?.id
|
|
const other = isForMe ? event.sender : event.recipient!
|
|
|
|
if (isForMe) {
|
|
// Wie beim Messenger: eingehende 1:1-Nachricht poppt automatisch als
|
|
// Floating-Tab auf, sofern man nicht schon in /messages mit dem User ist
|
|
if (openPartnerId.value !== msg.senderId && !floatingChats.value.some(c => c.user.id === other.id)) {
|
|
openFloatingChat(other, false)
|
|
}
|
|
const isOpen = isPartnerOpen(msg.senderId)
|
|
upsertDm(other, msg, isOpen ? 0 : 1)
|
|
if (!isOpen) unreadTotal.value += 1
|
|
notifyMessageListeners(msg, other, null)
|
|
}
|
|
else {
|
|
// Eigene Nachricht aus anderem Tab/Fenster synchronisieren
|
|
upsertDm(other, msg, 0)
|
|
notifyMessageListeners(msg, other, null)
|
|
}
|
|
}
|
|
|
|
// Eingehende Reaction - offene Konversation direkt updaten, sonst
|
|
// Unread-Badge der Konversation (Reaction = Aktivität des Gegenübers)
|
|
function handleReaction(e: DmReactionEvent) {
|
|
const isMine = e.actorId === user.value?.id
|
|
if (e.group) {
|
|
const gid = e.group.id
|
|
if (openGroupId.value === gid) {
|
|
notifyReactionListeners(e)
|
|
return
|
|
}
|
|
if (isMine) return
|
|
const conv = conversations.value.find(c => c.kind === 'group' && c.group?.id === gid)
|
|
const delta = e.active ? 1 : -1
|
|
if (conv) conv.unread = Math.max(0, conv.unread + delta)
|
|
unreadTotal.value = Math.max(0, unreadTotal.value + delta)
|
|
return
|
|
}
|
|
const partnerId = isMine ? e.other!.id : e.actorId
|
|
notifyReactionListeners(e)
|
|
if (isPartnerOpen(partnerId) || isMine) return
|
|
const conv = conversations.value.find(c => c.kind === 'dm' && c.user?.id === partnerId)
|
|
const delta = e.active ? 1 : -1
|
|
if (conv) conv.unread = Math.max(0, conv.unread + delta)
|
|
unreadTotal.value = Math.max(0, unreadTotal.value + delta)
|
|
}
|
|
|
|
// Lesebestätigung 1:1: reader=wer gelesen hat, partner=die andere Seite
|
|
function handleRead(event: DmReadEvent) {
|
|
if (event.reader === user.value?.id) {
|
|
// Ich habe woanders gelesen → Unread dieser Konversation weg
|
|
const conv = conversations.value.find(c => c.kind === 'dm' && c.user?.id === event.partner)
|
|
const delta = conv?.unread ?? 0
|
|
if (conv) conv.unread = 0
|
|
unreadTotal.value = Math.max(0, unreadTotal.value - delta)
|
|
}
|
|
else if (event.partner === user.value?.id) {
|
|
// Gegenüber hat meine Nachrichten gelesen
|
|
notifyReadListeners(event.reader, event.at)
|
|
const conv = conversations.value.find(c => c.kind === 'dm' && c.user?.id === event.reader)
|
|
if (conv?.lastMessage && conv.lastMessage.senderId === user.value?.id && !conv.lastMessage.readAt) {
|
|
conv.lastMessage.readAt = event.at
|
|
}
|
|
}
|
|
}
|
|
|
|
// Gruppen-Lesebestätigung - nur für eigene andere Tabs relevant
|
|
function handleGroupRead(event: DmGroupReadEvent) {
|
|
if (event.reader !== user.value?.id) return
|
|
const conv = conversations.value.find(c => c.kind === 'group' && c.group?.id === event.groupId)
|
|
const delta = conv?.unread ?? 0
|
|
if (conv) conv.unread = 0
|
|
unreadTotal.value = Math.max(0, unreadTotal.value - delta)
|
|
}
|
|
|
|
// Neue Gruppe bzw. geänderte Mitgliederliste - Konversation anlegen/updaten
|
|
// und eine geöffnete Ansicht informieren
|
|
function handleGroupUpdate(group: DmGroup) {
|
|
upsertGroup(group, null, 0)
|
|
if (openGroupId.value === group.id) liveGroupHandler.value?.(group)
|
|
}
|
|
|
|
// Optimistisch: eigene gesendete Nachricht sofort in die Liste
|
|
function addOwnMessage(partner: DmUser, msg: DmMessage) {
|
|
upsertDm(partner, msg, 0)
|
|
notifyMessageListeners(msg, partner, null)
|
|
}
|
|
|
|
function addOwnGroupMessage(group: DmGroup, msg: DmMessage) {
|
|
upsertGroup(group, msg, 0)
|
|
}
|
|
|
|
function markConversationRead(partnerId: number) {
|
|
const conv = conversations.value.find(c => c.kind === 'dm' && c.user?.id === partnerId)
|
|
const delta = conv?.unread ?? 0
|
|
if (!delta) return
|
|
conv!.unread = 0
|
|
unreadTotal.value = Math.max(0, unreadTotal.value - delta)
|
|
}
|
|
|
|
function markGroupRead(groupId: number) {
|
|
const conv = conversations.value.find(c => c.kind === 'group' && c.group?.id === groupId)
|
|
const delta = conv?.unread ?? 0
|
|
if (!delta) return
|
|
conv!.unread = 0
|
|
unreadTotal.value = Math.max(0, unreadTotal.value - delta)
|
|
}
|
|
|
|
function reset() {
|
|
conversations.value = []
|
|
unreadTotal.value = 0
|
|
openPartnerId.value = null
|
|
openGroupId.value = null
|
|
floatingChats.value = []
|
|
liveMessageHandler.value = null
|
|
liveReadHandler.value = null
|
|
liveReactionHandler.value = null
|
|
liveGroupHandler.value = null
|
|
}
|
|
|
|
return {
|
|
conversations,
|
|
unreadTotal,
|
|
openPartnerId,
|
|
openGroupId,
|
|
floatingChats,
|
|
openFloatingChat,
|
|
toggleMinimizeFloatingChat,
|
|
closeFloatingChat,
|
|
subscribeFloating,
|
|
fetchConversations,
|
|
handleIncoming,
|
|
handleReaction,
|
|
handleRead,
|
|
handleGroupRead,
|
|
handleGroupUpdate,
|
|
addOwnMessage,
|
|
addOwnGroupMessage,
|
|
markConversationRead,
|
|
markGroupRead,
|
|
liveMessageHandler,
|
|
liveReadHandler,
|
|
liveReactionHandler,
|
|
liveGroupHandler,
|
|
reset,
|
|
}
|
|
}
|