KI-AGENT: Live-Sync und Nachrichtenaktionen im Chat ergänzen
This commit is contained in:
@@ -17,6 +17,7 @@ type MatrixRoomEvent = {
|
|||||||
sender: string
|
sender: string
|
||||||
origin_server_ts: number
|
origin_server_ts: number
|
||||||
type: string
|
type: string
|
||||||
|
redacts?: string
|
||||||
content?: {
|
content?: {
|
||||||
body?: string
|
body?: string
|
||||||
msgtype?: string
|
msgtype?: string
|
||||||
@@ -60,6 +61,20 @@ type MatrixRoomSearchResponse = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MatrixSyncResponse = {
|
||||||
|
next_batch?: string
|
||||||
|
rooms?: {
|
||||||
|
join?: Record<string, {
|
||||||
|
timeline?: {
|
||||||
|
events?: MatrixRoomEvent[]
|
||||||
|
}
|
||||||
|
state?: {
|
||||||
|
events?: MatrixRoomEvent[]
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type MatrixUserSession = {
|
type MatrixUserSession = {
|
||||||
accessToken: string
|
accessToken: string
|
||||||
matrixUserId: string
|
matrixUserId: string
|
||||||
@@ -1370,6 +1385,133 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const syncTenantRoomEvents = async (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {},
|
||||||
|
since?: string,
|
||||||
|
initial = false
|
||||||
|
) => {
|
||||||
|
const room = await provisionTenantRoom(userId, tenantId, options)
|
||||||
|
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
})
|
||||||
|
const filter = {
|
||||||
|
room: {
|
||||||
|
rooms: [room.roomId],
|
||||||
|
timeline: {
|
||||||
|
limit: 30,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
presence: {
|
||||||
|
types: [],
|
||||||
|
},
|
||||||
|
account_data: {
|
||||||
|
types: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
timeout: since && !initial ? "25000" : "0",
|
||||||
|
filter: JSON.stringify(filter),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (since) params.set("since", since)
|
||||||
|
|
||||||
|
const response = await requestMatrixJson<MatrixSyncResponse>(
|
||||||
|
`/_matrix/client/v3/sync?${params.toString()}`,
|
||||||
|
session.accessToken
|
||||||
|
)
|
||||||
|
const joinedRoom = response.rooms?.join?.[room.roomId]
|
||||||
|
const timelineEvents = joinedRoom?.timeline?.events || []
|
||||||
|
const stateEvents = joinedRoom?.state?.events || []
|
||||||
|
|
||||||
|
if (initial) {
|
||||||
|
return {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
key: room.key,
|
||||||
|
name: room.name,
|
||||||
|
nextBatch: response.next_batch || since || "",
|
||||||
|
messages: [],
|
||||||
|
replacements: [],
|
||||||
|
reactions: [],
|
||||||
|
redactions: [],
|
||||||
|
membersChanged: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = timelineEvents
|
||||||
|
.filter((event) =>
|
||||||
|
event.type === "m.room.message" &&
|
||||||
|
["m.text", "m.file", "m.image"].includes(event.content?.msgtype || "") &&
|
||||||
|
event.content?.["m.relates_to"]?.rel_type !== "m.replace"
|
||||||
|
)
|
||||||
|
.map((event) => ({
|
||||||
|
id: event.event_id,
|
||||||
|
sender: event.sender,
|
||||||
|
senderDisplayName: event.sender,
|
||||||
|
body: event.content?.body || "",
|
||||||
|
attachment: attachmentFromEvent(event),
|
||||||
|
timestamp: event.origin_server_ts,
|
||||||
|
own: event.sender === session.matrixUserId,
|
||||||
|
replyToEventId: event.content?.["m.relates_to"]?.["m.in_reply_to"]?.event_id || null,
|
||||||
|
reactions: [],
|
||||||
|
}))
|
||||||
|
|
||||||
|
const replacements = timelineEvents
|
||||||
|
.filter((event) =>
|
||||||
|
event.type === "m.room.message" &&
|
||||||
|
event.content?.["m.relates_to"]?.rel_type === "m.replace" &&
|
||||||
|
Boolean(event.content?.["m.relates_to"]?.event_id)
|
||||||
|
)
|
||||||
|
.map((event) => ({
|
||||||
|
id: event.event_id,
|
||||||
|
targetEventId: event.content?.["m.relates_to"]?.event_id,
|
||||||
|
body: event.content?.["m.new_content"]?.body || event.content?.body || "",
|
||||||
|
timestamp: event.origin_server_ts,
|
||||||
|
sender: event.sender,
|
||||||
|
own: event.sender === session.matrixUserId,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const reactions = timelineEvents
|
||||||
|
.filter((event) =>
|
||||||
|
event.type === "m.reaction" &&
|
||||||
|
event.content?.["m.relates_to"]?.rel_type === "m.annotation" &&
|
||||||
|
Boolean(event.content?.["m.relates_to"]?.event_id) &&
|
||||||
|
Boolean(event.content?.["m.relates_to"]?.key)
|
||||||
|
)
|
||||||
|
.map((event) => ({
|
||||||
|
id: event.event_id,
|
||||||
|
targetEventId: event.content?.["m.relates_to"]?.event_id,
|
||||||
|
key: event.content?.["m.relates_to"]?.key,
|
||||||
|
sender: event.sender,
|
||||||
|
own: event.sender === session.matrixUserId,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const redactions = timelineEvents
|
||||||
|
.filter((event) => event.type === "m.room.redaction" && Boolean(event.redacts))
|
||||||
|
.map((event) => ({
|
||||||
|
id: event.event_id,
|
||||||
|
targetEventId: event.redacts,
|
||||||
|
sender: event.sender,
|
||||||
|
timestamp: event.origin_server_ts,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
key: room.key,
|
||||||
|
name: room.name,
|
||||||
|
nextBatch: response.next_batch || since || "",
|
||||||
|
messages,
|
||||||
|
replacements,
|
||||||
|
reactions,
|
||||||
|
redactions,
|
||||||
|
membersChanged: [...timelineEvents, ...stateEvents].some((event) => event.type === "m.room.member"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sendTenantRoomMessage = async (
|
const sendTenantRoomMessage = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
tenantId: number | null,
|
tenantId: number | null,
|
||||||
@@ -1472,6 +1614,91 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
return { success: true, eventId, key: reactionKey }
|
return { success: true, eventId, key: reactionKey }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const editTenantRoomMessage = async (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {},
|
||||||
|
eventId: string,
|
||||||
|
text: string
|
||||||
|
) => {
|
||||||
|
const message = text.trim()
|
||||||
|
if (!eventId || !message) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Nachricht und Zielnachricht sind erforderlich"),
|
||||||
|
{ statusCode: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await provisionTenantRoom(userId, tenantId, options)
|
||||||
|
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
})
|
||||||
|
const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`
|
||||||
|
const response = await requestMatrixJson<{ event_id: string }>(
|
||||||
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/send/m.room.message/${encodeURIComponent(txnId)}`,
|
||||||
|
session.accessToken,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: `* ${message}`,
|
||||||
|
"m.new_content": {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: message,
|
||||||
|
},
|
||||||
|
"m.relates_to": {
|
||||||
|
rel_type: "m.replace",
|
||||||
|
event_id: eventId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: response.event_id,
|
||||||
|
targetEventId: eventId,
|
||||||
|
body: message,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
own: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const redactTenantRoomMessage = async (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {},
|
||||||
|
eventId: string
|
||||||
|
) => {
|
||||||
|
if (!eventId) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Zielnachricht ist erforderlich"),
|
||||||
|
{ statusCode: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await provisionTenantRoom(userId, tenantId, options)
|
||||||
|
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
})
|
||||||
|
const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`
|
||||||
|
await requestMatrixJson(
|
||||||
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/redact/${encodeURIComponent(eventId)}/${encodeURIComponent(txnId)}`,
|
||||||
|
session.accessToken,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
reason: "Nachricht in FEDEO gelöscht",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return { success: true, eventId }
|
||||||
|
}
|
||||||
|
|
||||||
const markTenantRoomRead = async (
|
const markTenantRoomRead = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
tenantId: number | null,
|
tenantId: number | null,
|
||||||
@@ -1961,8 +2188,11 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
getTenantRoomMessages,
|
getTenantRoomMessages,
|
||||||
getTenantRoomMembers,
|
getTenantRoomMembers,
|
||||||
searchTenantRoomMessages,
|
searchTenantRoomMessages,
|
||||||
|
syncTenantRoomEvents,
|
||||||
sendTenantRoomMessage,
|
sendTenantRoomMessage,
|
||||||
sendTenantRoomReaction,
|
sendTenantRoomReaction,
|
||||||
|
editTenantRoomMessage,
|
||||||
|
redactTenantRoomMessage,
|
||||||
markTenantRoomRead,
|
markTenantRoomRead,
|
||||||
sendTenantRoomAttachment,
|
sendTenantRoomAttachment,
|
||||||
getMediaContent,
|
getMediaContent,
|
||||||
|
|||||||
@@ -736,6 +736,21 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.get("/communication/matrix/rooms/:roomKey/sync", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const query = req.query as { since?: string; initial?: string }
|
||||||
|
return await matrix.syncTenantRoomEvents(
|
||||||
|
req.user.user_id,
|
||||||
|
req.user.tenant_id,
|
||||||
|
roomOptionsFromRequest(req),
|
||||||
|
query.since,
|
||||||
|
query.initial === "1"
|
||||||
|
)
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix sync failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
server.get("/communication/matrix/rooms/:roomKey/members", async (req, reply) => {
|
server.get("/communication/matrix/rooms/:roomKey/members", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
return await matrix.getTenantRoomMembers(
|
return await matrix.getTenantRoomMembers(
|
||||||
@@ -851,6 +866,36 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.put("/communication/matrix/rooms/:roomKey/messages/:eventId", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const params = req.params as { eventId: string }
|
||||||
|
const body = req.body as { text?: string }
|
||||||
|
return await matrix.editTenantRoomMessage(
|
||||||
|
req.user.user_id,
|
||||||
|
req.user.tenant_id,
|
||||||
|
roomOptionsFromRequest(req),
|
||||||
|
params.eventId,
|
||||||
|
body.text || ""
|
||||||
|
)
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix message edit failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.delete("/communication/matrix/rooms/:roomKey/messages/:eventId", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const params = req.params as { eventId: string }
|
||||||
|
return await matrix.redactTenantRoomMessage(
|
||||||
|
req.user.user_id,
|
||||||
|
req.user.tenant_id,
|
||||||
|
roomOptionsFromRequest(req),
|
||||||
|
params.eventId
|
||||||
|
)
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix message delete failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
server.post("/communication/matrix/rooms/:roomKey/attachments", async (req, reply) => {
|
server.post("/communication/matrix/rooms/:roomKey/attachments", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const attachment = await uploadedAttachmentFromRequest(req)
|
const attachment = await uploadedAttachmentFromRequest(req)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const matrixMessagesViewport = ref(null)
|
|||||||
const matrixAttachmentInput = ref(null)
|
const matrixAttachmentInput = ref(null)
|
||||||
const matrixAttachmentObjectUrls = ref({})
|
const matrixAttachmentObjectUrls = ref({})
|
||||||
const matrixReplyTarget = ref(null)
|
const matrixReplyTarget = ref(null)
|
||||||
|
const matrixEditingMessage = ref(null)
|
||||||
const matrixDragActive = ref(false)
|
const matrixDragActive = ref(false)
|
||||||
const memberInviteOpen = ref(false)
|
const memberInviteOpen = ref(false)
|
||||||
const memberInviteUserId = ref("")
|
const memberInviteUserId = ref("")
|
||||||
@@ -62,13 +63,14 @@ const matrixAttachmentUploading = ref(false)
|
|||||||
const matrixAttachmentUploadCount = ref(0)
|
const matrixAttachmentUploadCount = ref(0)
|
||||||
const matrixAutoRefreshActive = ref(false)
|
const matrixAutoRefreshActive = ref(false)
|
||||||
const lastUpdated = ref(null)
|
const lastUpdated = ref(null)
|
||||||
let matrixRefreshInterval = null
|
|
||||||
let matrixCallDurationInterval = null
|
let matrixCallDurationInterval = null
|
||||||
let matrixMessagesRequestActive = false
|
let matrixMessagesRequestActive = false
|
||||||
let matrixMembersRequestActive = false
|
let matrixMembersRequestActive = false
|
||||||
let matrixLiveKitRoom = null
|
let matrixLiveKitRoom = null
|
||||||
|
let matrixLiveSyncRunId = 0
|
||||||
const matrixCallVideoElements = new Map()
|
const matrixCallVideoElements = new Map()
|
||||||
const matrixAttachmentPreviewRequests = new Set()
|
const matrixAttachmentPreviewRequests = new Set()
|
||||||
|
const matrixSyncSince = ref("")
|
||||||
|
|
||||||
const canUseMatrixChat = computed(() =>
|
const canUseMatrixChat = computed(() =>
|
||||||
Boolean(status.value?.reachable && status.value?.provisioningConfigured)
|
Boolean(status.value?.reachable && status.value?.provisioningConfigured)
|
||||||
@@ -248,6 +250,7 @@ const setActiveRoom = async (room) => {
|
|||||||
matrixSearchResults.value = []
|
matrixSearchResults.value = []
|
||||||
memberInviteOpen.value = false
|
memberInviteOpen.value = false
|
||||||
await loadRoomChat({ includeMembers: true })
|
await loadRoomChat({ includeMembers: true })
|
||||||
|
restartMatrixLiveSync()
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergeRoomIntoLists = (room) => {
|
const mergeRoomIntoLists = (room) => {
|
||||||
@@ -286,6 +289,7 @@ const provisionRoomFromList = async (room) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await loadRoomChat({ includeMembers: true })
|
await loadRoomChat({ includeMembers: true })
|
||||||
|
restartMatrixLiveSync()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({
|
toast.add({
|
||||||
title: "Chatraum konnte nicht erstellt werden",
|
title: "Chatraum konnte nicht erstellt werden",
|
||||||
@@ -311,9 +315,11 @@ const mergeMatrixMessages = (incomingMessages) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const message of incomingMessages || []) {
|
for (const message of incomingMessages || []) {
|
||||||
|
const member = matrixMembers.value.find((item) => item.matrixUserId === message.sender)
|
||||||
byId.set(message.id, {
|
byId.set(message.id, {
|
||||||
...byId.get(message.id),
|
...byId.get(message.id),
|
||||||
...message,
|
...message,
|
||||||
|
senderDisplayName: member?.displayName || message.senderDisplayName,
|
||||||
pending: false,
|
pending: false,
|
||||||
failed: false
|
failed: false
|
||||||
})
|
})
|
||||||
@@ -324,6 +330,64 @@ const mergeMatrixMessages = (incomingMessages) => {
|
|||||||
loadAttachmentPreviews()
|
loadAttachmentPreviews()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const applyMatrixReplacements = (replacements) => {
|
||||||
|
if (!replacements?.length) return
|
||||||
|
|
||||||
|
const replacementByTarget = new Map()
|
||||||
|
for (const replacement of replacements) {
|
||||||
|
if (!replacement.targetEventId) continue
|
||||||
|
const previous = replacementByTarget.get(replacement.targetEventId)
|
||||||
|
if (!previous || (replacement.timestamp || 0) >= (previous.timestamp || 0)) {
|
||||||
|
replacementByTarget.set(replacement.targetEventId, replacement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matrixMessages.value = matrixMessages.value.map((message) => {
|
||||||
|
const replacement = replacementByTarget.get(message.id)
|
||||||
|
if (!replacement) return message
|
||||||
|
|
||||||
|
return {
|
||||||
|
...message,
|
||||||
|
body: replacement.body,
|
||||||
|
edited: true,
|
||||||
|
timestamp: replacement.timestamp || message.timestamp
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyMatrixReactions = (reactions) => {
|
||||||
|
if (!reactions?.length) return
|
||||||
|
|
||||||
|
matrixMessages.value = matrixMessages.value.map((message) => {
|
||||||
|
const messageReactions = reactions.filter((reaction) => reaction.targetEventId === message.id)
|
||||||
|
if (!messageReactions.length) return message
|
||||||
|
|
||||||
|
let nextReactions = [...(message.reactions || [])]
|
||||||
|
for (const reaction of messageReactions) {
|
||||||
|
const current = nextReactions.find((item) => item.key === reaction.key)
|
||||||
|
nextReactions = current
|
||||||
|
? nextReactions.map((item) => item.key === reaction.key ? {
|
||||||
|
...item,
|
||||||
|
count: item.count + 1,
|
||||||
|
own: item.own || reaction.own
|
||||||
|
} : item)
|
||||||
|
: [...nextReactions, { key: reaction.key, count: 1, own: reaction.own }]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...message,
|
||||||
|
reactions: nextReactions
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyMatrixRedactions = (redactions) => {
|
||||||
|
const redactedIds = new Set((redactions || []).map((redaction) => redaction.targetEventId).filter(Boolean))
|
||||||
|
if (!redactedIds.size) return
|
||||||
|
|
||||||
|
matrixMessages.value = matrixMessages.value.filter((message) => !redactedIds.has(message.id))
|
||||||
|
}
|
||||||
|
|
||||||
const findMessage = (messageId) =>
|
const findMessage = (messageId) =>
|
||||||
matrixMessages.value.find((message) => message.id === messageId)
|
matrixMessages.value.find((message) => message.id === messageId)
|
||||||
|
|
||||||
@@ -333,6 +397,7 @@ const replyPreview = (message) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setReplyTarget = (message) => {
|
const setReplyTarget = (message) => {
|
||||||
|
matrixEditingMessage.value = null
|
||||||
matrixReplyTarget.value = {
|
matrixReplyTarget.value = {
|
||||||
id: message.id,
|
id: message.id,
|
||||||
senderDisplayName: message.own ? "Du" : message.senderDisplayName || message.sender,
|
senderDisplayName: message.own ? "Du" : message.senderDisplayName || message.sender,
|
||||||
@@ -344,6 +409,22 @@ const clearReplyTarget = () => {
|
|||||||
matrixReplyTarget.value = null
|
matrixReplyTarget.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const beginEditMessage = (message) => {
|
||||||
|
if (!message?.id || !message.own || message.attachment) return
|
||||||
|
|
||||||
|
matrixEditingMessage.value = {
|
||||||
|
id: message.id,
|
||||||
|
body: message.body || ""
|
||||||
|
}
|
||||||
|
matrixMessageDraft.value = message.body || ""
|
||||||
|
clearReplyTarget()
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelEditMessage = () => {
|
||||||
|
matrixEditingMessage.value = null
|
||||||
|
matrixMessageDraft.value = ""
|
||||||
|
}
|
||||||
|
|
||||||
const scrollMessagesToBottom = async () => {
|
const scrollMessagesToBottom = async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (!matrixMessagesViewport.value) return
|
if (!matrixMessagesViewport.value) return
|
||||||
@@ -485,6 +566,7 @@ const createRoom = async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await loadRoomChat({ includeMembers: true })
|
await loadRoomChat({ includeMembers: true })
|
||||||
|
restartMatrixLiveSync()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({
|
toast.add({
|
||||||
title: "Chatraum konnte nicht erstellt werden",
|
title: "Chatraum konnte nicht erstellt werden",
|
||||||
@@ -963,6 +1045,29 @@ const sendMatrixMessage = async () => {
|
|||||||
const text = matrixMessageDraft.value.trim()
|
const text = matrixMessageDraft.value.trim()
|
||||||
if (!text || !canUseMatrixChat.value || !activeRoom.value?.exists) return
|
if (!text || !canUseMatrixChat.value || !activeRoom.value?.exists) return
|
||||||
|
|
||||||
|
if (matrixEditingMessage.value?.id) {
|
||||||
|
const editingId = matrixEditingMessage.value.id
|
||||||
|
matrixMessageSending.value = true
|
||||||
|
try {
|
||||||
|
const replacement = await $api(`${activeRoomEndpoint.value}/messages/${encodeURIComponent(editingId)}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: { text }
|
||||||
|
})
|
||||||
|
|
||||||
|
applyMatrixReplacements([replacement])
|
||||||
|
matrixMessageDraft.value = ""
|
||||||
|
matrixEditingMessage.value = null
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
title: "Nachricht konnte nicht bearbeitet werden",
|
||||||
|
color: "error"
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
matrixMessageSending.value = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const optimisticId = `pending-${Date.now()}`
|
const optimisticId = `pending-${Date.now()}`
|
||||||
const optimisticMessage = {
|
const optimisticMessage = {
|
||||||
id: optimisticId,
|
id: optimisticId,
|
||||||
@@ -1150,26 +1255,92 @@ const reactToMatrixMessage = async (message, key) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteMatrixMessage = async (message) => {
|
||||||
|
if (!message?.id || !message.own || !canUseMatrixChat.value || !activeRoom.value?.exists) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $api(`${activeRoomEndpoint.value}/messages/${encodeURIComponent(message.id)}`, {
|
||||||
|
method: "DELETE"
|
||||||
|
})
|
||||||
|
matrixMessages.value = matrixMessages.value.filter((item) => item.id !== message.id)
|
||||||
|
if (matrixEditingMessage.value?.id === message.id) {
|
||||||
|
cancelEditMessage()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
title: "Nachricht konnte nicht gelöscht werden",
|
||||||
|
color: "error"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyMatrixSync = async (syncResult) => {
|
||||||
|
if (syncResult.nextBatch) {
|
||||||
|
matrixSyncSince.value = syncResult.nextBatch
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeMatrixMessages(syncResult.messages || [])
|
||||||
|
applyMatrixReplacements(syncResult.replacements)
|
||||||
|
applyMatrixReactions(syncResult.reactions)
|
||||||
|
applyMatrixRedactions(syncResult.redactions)
|
||||||
|
|
||||||
|
if ((syncResult.messages || []).length) {
|
||||||
|
await markActiveRoomRead()
|
||||||
|
await scrollMessagesToBottom()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncResult.membersChanged) {
|
||||||
|
await loadRoomMembers({ silent: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runMatrixLiveSync = async (runId) => {
|
||||||
|
while (matrixLiveSyncRunId === runId) {
|
||||||
|
if (!canUseMatrixChat.value || !activeRoom.value?.exists || document.hidden) {
|
||||||
|
await new Promise((resolve) => window.setTimeout(resolve, 3000))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (matrixSyncSince.value) {
|
||||||
|
params.set("since", matrixSyncSince.value)
|
||||||
|
} else {
|
||||||
|
params.set("initial", "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncResult = await $api(`${activeRoomEndpoint.value}/sync?${params.toString()}`)
|
||||||
|
if (matrixLiveSyncRunId !== runId) return
|
||||||
|
|
||||||
|
await applyMatrixSync(syncResult)
|
||||||
|
await loadUnreadCounts()
|
||||||
|
lastUpdated.value = new Date()
|
||||||
|
} catch (error) {
|
||||||
|
if (matrixLiveSyncRunId !== runId) return
|
||||||
|
await new Promise((resolve) => window.setTimeout(resolve, 5000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const startMatrixAutoRefresh = () => {
|
const startMatrixAutoRefresh = () => {
|
||||||
if (matrixRefreshInterval) return
|
if (matrixAutoRefreshActive.value) return
|
||||||
|
|
||||||
matrixAutoRefreshActive.value = true
|
matrixAutoRefreshActive.value = true
|
||||||
matrixRefreshInterval = window.setInterval(() => {
|
matrixLiveSyncRunId += 1
|
||||||
if (!document.hidden && canUseMatrixChat.value && activeRoom.value?.exists) {
|
runMatrixLiveSync(matrixLiveSyncRunId)
|
||||||
loadRoomChat({ silent: true })
|
|
||||||
loadUnreadCounts()
|
|
||||||
}
|
|
||||||
}, 15000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopMatrixAutoRefresh = () => {
|
const stopMatrixAutoRefresh = () => {
|
||||||
if (!matrixRefreshInterval) return
|
matrixLiveSyncRunId += 1
|
||||||
|
|
||||||
window.clearInterval(matrixRefreshInterval)
|
|
||||||
matrixRefreshInterval = null
|
|
||||||
matrixAutoRefreshActive.value = false
|
matrixAutoRefreshActive.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const restartMatrixLiveSync = () => {
|
||||||
|
matrixSyncSince.value = ""
|
||||||
|
stopMatrixAutoRefresh()
|
||||||
|
startMatrixAutoRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
const formatMessageTime = (timestamp) => {
|
const formatMessageTime = (timestamp) => {
|
||||||
if (!timestamp) return ""
|
if (!timestamp) return ""
|
||||||
|
|
||||||
@@ -1749,11 +1920,47 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
Antworten
|
Antworten
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="message.own && !message.attachment && !message.pending"
|
||||||
|
type="button"
|
||||||
|
class="rounded-full px-2 py-0.5 text-xs opacity-70 hover:opacity-100"
|
||||||
|
:class="message.own ? 'bg-white/10' : 'bg-muted'"
|
||||||
|
@click="beginEditMessage(message)"
|
||||||
|
>
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="message.own && !message.pending"
|
||||||
|
type="button"
|
||||||
|
class="rounded-full px-2 py-0.5 text-xs opacity-70 hover:opacity-100"
|
||||||
|
:class="message.own ? 'bg-white/10' : 'bg-muted'"
|
||||||
|
@click="deleteMatrixMessage(message)"
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="matrixEditingMessage"
|
||||||
|
class="flex shrink-0 items-center gap-3 border-t border-default bg-default px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<UIcon name="i-heroicons-pencil-square" class="size-4 text-muted" />
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<p class="truncate text-xs text-muted">Nachricht bearbeiten</p>
|
||||||
|
<p class="truncate text-highlighted">{{ matrixEditingMessage.body }}</p>
|
||||||
|
</div>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-x-mark"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
size="xs"
|
||||||
|
@click="cancelEditMessage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="matrixReplyTarget"
|
v-if="matrixReplyTarget"
|
||||||
class="flex shrink-0 items-center gap-3 border-t border-default bg-default px-3 py-2 text-sm"
|
class="flex shrink-0 items-center gap-3 border-t border-default bg-default px-3 py-2 text-sm"
|
||||||
@@ -1795,7 +2002,7 @@ onBeforeUnmount(() => {
|
|||||||
<UInput
|
<UInput
|
||||||
v-model="matrixMessageDraft"
|
v-model="matrixMessageDraft"
|
||||||
class="min-w-0 flex-1"
|
class="min-w-0 flex-1"
|
||||||
:placeholder="matrixAttachmentUploading ? `${matrixAttachmentUploadCount} Anhang/Anhänge werden hochgeladen` : 'Nachricht schreiben'"
|
:placeholder="matrixEditingMessage ? 'Änderung schreiben' : (matrixAttachmentUploading ? `${matrixAttachmentUploadCount} Anhang/Anhänge werden hochgeladen` : 'Nachricht schreiben')"
|
||||||
:disabled="!canSendChatInput"
|
:disabled="!canSendChatInput"
|
||||||
@keydown.enter.exact.prevent="sendMatrixMessage"
|
@keydown.enter.exact.prevent="sendMatrixMessage"
|
||||||
/>
|
/>
|
||||||
@@ -1805,7 +2012,7 @@ onBeforeUnmount(() => {
|
|||||||
:loading="matrixMessageSending"
|
:loading="matrixMessageSending"
|
||||||
:disabled="!matrixMessageDraft.trim() || !canSendChatInput"
|
:disabled="!matrixMessageDraft.trim() || !canSendChatInput"
|
||||||
>
|
>
|
||||||
Senden
|
{{ matrixEditingMessage ? "Speichern" : "Senden" }}
|
||||||
</UButton>
|
</UButton>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user