KI-AGENT: Chat Anhänge und Nachrichteninteraktionen abrunden
This commit is contained in:
@@ -25,6 +25,18 @@ type MatrixRoomEvent = {
|
|||||||
mimetype?: string
|
mimetype?: string
|
||||||
size?: number
|
size?: number
|
||||||
}
|
}
|
||||||
|
"m.new_content"?: {
|
||||||
|
body?: string
|
||||||
|
msgtype?: string
|
||||||
|
}
|
||||||
|
"m.relates_to"?: {
|
||||||
|
event_id?: string
|
||||||
|
key?: string
|
||||||
|
rel_type?: string
|
||||||
|
"m.in_reply_to"?: {
|
||||||
|
event_id?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +83,10 @@ type MatrixAttachmentInput = {
|
|||||||
size: number
|
size: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MatrixMessageOptions = {
|
||||||
|
replyToEventId?: string
|
||||||
|
}
|
||||||
|
|
||||||
type MatrixCachedValue<T = any> = {
|
type MatrixCachedValue<T = any> = {
|
||||||
exists: true
|
exists: true
|
||||||
cachedUntil: number
|
cachedUntil: number
|
||||||
@@ -1158,27 +1174,72 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
session.accessToken
|
session.accessToken
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const replacementByEventId = new Map<string, MatrixRoomEvent>()
|
||||||
|
const reactionsByEventId = new Map<string, Map<string, { key: string; count: number; own: boolean }>>()
|
||||||
|
|
||||||
|
for (const event of response.chunk) {
|
||||||
|
const relation = event.content?.["m.relates_to"]
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.type === "m.room.message" &&
|
||||||
|
relation?.rel_type === "m.replace" &&
|
||||||
|
relation.event_id
|
||||||
|
) {
|
||||||
|
replacementByEventId.set(relation.event_id, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.type === "m.reaction" &&
|
||||||
|
relation?.rel_type === "m.annotation" &&
|
||||||
|
relation.event_id &&
|
||||||
|
relation.key
|
||||||
|
) {
|
||||||
|
const eventReactions = reactionsByEventId.get(relation.event_id) || new Map()
|
||||||
|
const reaction = eventReactions.get(relation.key) || {
|
||||||
|
key: relation.key,
|
||||||
|
count: 0,
|
||||||
|
own: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
reaction.count += 1
|
||||||
|
reaction.own = reaction.own || event.sender === session.matrixUserId
|
||||||
|
eventReactions.set(relation.key, reaction)
|
||||||
|
reactionsByEventId.set(relation.event_id, eventReactions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = response.chunk
|
||||||
|
.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) => {
|
||||||
|
const replacement = replacementByEventId.get(event.event_id)
|
||||||
|
const content = replacement?.content?.["m.new_content"] || replacement?.content || event.content
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: event.event_id,
|
||||||
|
sender: event.sender,
|
||||||
|
senderDisplayName: members.joined[event.sender]?.display_name || event.sender,
|
||||||
|
body: content?.body || "",
|
||||||
|
attachment: attachmentFromEvent({ ...event, content }),
|
||||||
|
timestamp: replacement?.origin_server_ts || event.origin_server_ts,
|
||||||
|
own: event.sender === session.matrixUserId,
|
||||||
|
edited: Boolean(replacement),
|
||||||
|
replyToEventId: event.content?.["m.relates_to"]?.["m.in_reply_to"]?.event_id || null,
|
||||||
|
reactions: Array.from(reactionsByEventId.get(event.event_id)?.values() || []),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.reverse()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
alias: room.alias,
|
alias: room.alias,
|
||||||
key: room.key,
|
key: room.key,
|
||||||
name: room.name,
|
name: room.name,
|
||||||
matrixUserId: session.matrixUserId,
|
matrixUserId: session.matrixUserId,
|
||||||
messages: response.chunk
|
messages,
|
||||||
.filter((event) =>
|
|
||||||
event.type === "m.room.message" &&
|
|
||||||
["m.text", "m.file", "m.image"].includes(event.content?.msgtype || "")
|
|
||||||
)
|
|
||||||
.map((event) => ({
|
|
||||||
id: event.event_id,
|
|
||||||
sender: event.sender,
|
|
||||||
senderDisplayName: members.joined[event.sender]?.display_name || event.sender,
|
|
||||||
body: event.content?.body || "",
|
|
||||||
attachment: attachmentFromEvent(event),
|
|
||||||
timestamp: event.origin_server_ts,
|
|
||||||
own: event.sender === session.matrixUserId,
|
|
||||||
}))
|
|
||||||
.reverse(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1216,7 +1277,8 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
userId: string,
|
userId: string,
|
||||||
tenantId: number | null,
|
tenantId: number | null,
|
||||||
options: MatrixTenantRoomOptions = {},
|
options: MatrixTenantRoomOptions = {},
|
||||||
text: string
|
text: string,
|
||||||
|
messageOptions: MatrixMessageOptions = {}
|
||||||
) => {
|
) => {
|
||||||
const message = text.trim()
|
const message = text.trim()
|
||||||
|
|
||||||
@@ -1235,16 +1297,26 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
})
|
})
|
||||||
const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`
|
const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`
|
||||||
|
|
||||||
|
const content: Record<string, any> = {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageOptions.replyToEventId) {
|
||||||
|
content["m.relates_to"] = {
|
||||||
|
"m.in_reply_to": {
|
||||||
|
event_id: messageOptions.replyToEventId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const response = await requestMatrixJson<{ event_id: string }>(
|
const response = await requestMatrixJson<{ event_id: string }>(
|
||||||
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/send/m.room.message/${encodeURIComponent(txnId)}`,
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/send/m.room.message/${encodeURIComponent(txnId)}`,
|
||||||
session.accessToken,
|
session.accessToken,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(content),
|
||||||
msgtype: "m.text",
|
|
||||||
body: message,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1258,9 +1330,78 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
alias: room.alias,
|
alias: room.alias,
|
||||||
key: room.key,
|
key: room.key,
|
||||||
|
replyToEventId: messageOptions.replyToEventId || null,
|
||||||
|
reactions: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sendTenantRoomReaction = async (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {},
|
||||||
|
eventId: string,
|
||||||
|
key: string
|
||||||
|
) => {
|
||||||
|
const reactionKey = key.trim()
|
||||||
|
if (!eventId || !reactionKey) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Reaction target and key are required"),
|
||||||
|
{ 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<{ event_id: string }>(
|
||||||
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/send/m.reaction/${encodeURIComponent(txnId)}`,
|
||||||
|
session.accessToken,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
"m.relates_to": {
|
||||||
|
rel_type: "m.annotation",
|
||||||
|
event_id: eventId,
|
||||||
|
key: reactionKey,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return { success: true, eventId, key: reactionKey }
|
||||||
|
}
|
||||||
|
|
||||||
|
const markTenantRoomRead = async (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {},
|
||||||
|
eventId: string
|
||||||
|
) => {
|
||||||
|
if (!eventId) return { success: true, skipped: true }
|
||||||
|
|
||||||
|
const room = await provisionTenantRoom(userId, tenantId, options)
|
||||||
|
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
})
|
||||||
|
|
||||||
|
await requestMatrixJson(
|
||||||
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/receipt/m.read/${encodeURIComponent(eventId)}`,
|
||||||
|
session.accessToken,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return { success: true, eventId }
|
||||||
|
}
|
||||||
|
|
||||||
const sendTenantRoomAttachment = async (
|
const sendTenantRoomAttachment = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
tenantId: number | null,
|
tenantId: number | null,
|
||||||
@@ -1579,7 +1720,12 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
name: "Allgemeiner Chat",
|
name: "Allgemeiner Chat",
|
||||||
})
|
})
|
||||||
|
|
||||||
const sendGeneralRoomMessage = (userId: string, tenantId: number | null, text: string) =>
|
const sendGeneralRoomMessage = (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
text: string,
|
||||||
|
messageOptions: MatrixMessageOptions = {}
|
||||||
|
) =>
|
||||||
sendTenantRoomMessage(
|
sendTenantRoomMessage(
|
||||||
userId,
|
userId,
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -1587,7 +1733,8 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
key: "allgemein",
|
key: "allgemein",
|
||||||
name: "Allgemeiner Chat",
|
name: "Allgemeiner Chat",
|
||||||
},
|
},
|
||||||
text
|
text,
|
||||||
|
messageOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
const sendGeneralRoomAttachment = (userId: string, tenantId: number | null, attachment: MatrixAttachmentInput) =>
|
const sendGeneralRoomAttachment = (userId: string, tenantId: number | null, attachment: MatrixAttachmentInput) =>
|
||||||
@@ -1615,6 +1762,8 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
getTenantRoomMessages,
|
getTenantRoomMessages,
|
||||||
getTenantRoomMembers,
|
getTenantRoomMembers,
|
||||||
sendTenantRoomMessage,
|
sendTenantRoomMessage,
|
||||||
|
sendTenantRoomReaction,
|
||||||
|
markTenantRoomRead,
|
||||||
sendTenantRoomAttachment,
|
sendTenantRoomAttachment,
|
||||||
getMediaContent,
|
getMediaContent,
|
||||||
createElementRoomSession,
|
createElementRoomSession,
|
||||||
|
|||||||
@@ -641,8 +641,10 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
|
|
||||||
server.post("/communication/matrix/rooms/general/messages", async (req, reply) => {
|
server.post("/communication/matrix/rooms/general/messages", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const body = req.body as { text?: string }
|
const body = req.body as { text?: string; replyToEventId?: string }
|
||||||
const message = await matrix.sendGeneralRoomMessage(req.user.user_id, req.user.tenant_id, body.text || "")
|
const message = await matrix.sendGeneralRoomMessage(req.user.user_id, req.user.tenant_id, body.text || "", {
|
||||||
|
replyToEventId: body.replyToEventId,
|
||||||
|
})
|
||||||
const room = await matrix.getTenantRoomStatus(req.user.tenant_id, "allgemein", "Allgemeiner Chat")
|
const room = await matrix.getTenantRoomStatus(req.user.tenant_id, "allgemein", "Allgemeiner Chat")
|
||||||
await notifyUsersAboutChatMessage(req, room, message, body.text || "")
|
await notifyUsersAboutChatMessage(req, room, message, body.text || "")
|
||||||
return message
|
return message
|
||||||
@@ -688,7 +690,12 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
try {
|
try {
|
||||||
if (!req.user.tenant_id) return reply.code(400).send({ error: "Kein aktiver Mandant" })
|
if (!req.user.tenant_id) return reply.code(400).send({ error: "Kein aktiver Mandant" })
|
||||||
const params = req.params as { roomKey: string }
|
const params = req.params as { roomKey: string }
|
||||||
return await markRoomNotificationsRead(req.user.tenant_id, req.user.user_id, params.roomKey)
|
const body = (req.body || {}) as { eventId?: string }
|
||||||
|
const result = await markRoomNotificationsRead(req.user.tenant_id, req.user.user_id, params.roomKey)
|
||||||
|
if (body.eventId) {
|
||||||
|
await matrix.markTenantRoomRead(req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req), body.eventId)
|
||||||
|
}
|
||||||
|
return result
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return handleMatrixError(req, reply, err, "Matrix room read state failed")
|
return handleMatrixError(req, reply, err, "Matrix room read state failed")
|
||||||
}
|
}
|
||||||
@@ -759,12 +766,15 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
|
|
||||||
server.post("/communication/matrix/rooms/:roomKey/messages", async (req, reply) => {
|
server.post("/communication/matrix/rooms/:roomKey/messages", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const body = req.body as { text?: string }
|
const body = req.body as { text?: string; replyToEventId?: string }
|
||||||
const message = await matrix.sendTenantRoomMessage(
|
const message = await matrix.sendTenantRoomMessage(
|
||||||
req.user.user_id,
|
req.user.user_id,
|
||||||
req.user.tenant_id,
|
req.user.tenant_id,
|
||||||
roomOptionsFromRequest(req),
|
roomOptionsFromRequest(req),
|
||||||
body.text || ""
|
body.text || "",
|
||||||
|
{
|
||||||
|
replyToEventId: body.replyToEventId,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
const room = await matrix.getTenantRoomStatus(req.user.tenant_id, message.key)
|
const room = await matrix.getTenantRoomStatus(req.user.tenant_id, message.key)
|
||||||
await notifyUsersAboutChatMessage(req, room, message, body.text || "")
|
await notifyUsersAboutChatMessage(req, room, message, body.text || "")
|
||||||
@@ -774,6 +784,22 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.post("/communication/matrix/rooms/:roomKey/messages/:eventId/reactions", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const params = req.params as { eventId: string }
|
||||||
|
const body = req.body as { key?: string }
|
||||||
|
return await matrix.sendTenantRoomReaction(
|
||||||
|
req.user.user_id,
|
||||||
|
req.user.tenant_id,
|
||||||
|
roomOptionsFromRequest(req),
|
||||||
|
params.eventId,
|
||||||
|
body.key || ""
|
||||||
|
)
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix reaction send 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)
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ const matrixMessageDraft = ref("")
|
|||||||
const matrixMessagesViewport = ref(null)
|
const matrixMessagesViewport = ref(null)
|
||||||
const matrixAttachmentInput = ref(null)
|
const matrixAttachmentInput = ref(null)
|
||||||
const matrixAttachmentObjectUrls = ref({})
|
const matrixAttachmentObjectUrls = ref({})
|
||||||
|
const matrixReplyTarget = ref(null)
|
||||||
|
const matrixDragActive = ref(false)
|
||||||
const roomCreateOpen = ref(false)
|
const roomCreateOpen = ref(false)
|
||||||
const collapsedRoomGroups = ref({})
|
const collapsedRoomGroups = ref({})
|
||||||
const matrixCallOpen = ref(false)
|
const matrixCallOpen = ref(false)
|
||||||
@@ -49,6 +51,7 @@ const matrixMessagesLoading = ref(false)
|
|||||||
const matrixMembersLoading = ref(false)
|
const matrixMembersLoading = ref(false)
|
||||||
const matrixMessageSending = ref(false)
|
const matrixMessageSending = ref(false)
|
||||||
const matrixAttachmentUploading = ref(false)
|
const matrixAttachmentUploading = ref(false)
|
||||||
|
const matrixAttachmentUploadCount = ref(0)
|
||||||
const matrixAutoRefreshActive = ref(false)
|
const matrixAutoRefreshActive = ref(false)
|
||||||
const lastUpdated = ref(null)
|
const lastUpdated = ref(null)
|
||||||
let matrixRefreshInterval = null
|
let matrixRefreshInterval = null
|
||||||
@@ -86,6 +89,10 @@ const canStartMatrixCall = computed(() =>
|
|||||||
Boolean(canUseMatrixChat.value && activeRoom.value?.exists)
|
Boolean(canUseMatrixChat.value && activeRoom.value?.exists)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const canSendChatInput = computed(() =>
|
||||||
|
Boolean(canUseMatrixChat.value && activeRoom.value?.exists && !matrixMessageSending.value && !matrixAttachmentUploading.value)
|
||||||
|
)
|
||||||
|
|
||||||
const matrixCallTitle = computed(() =>
|
const matrixCallTitle = computed(() =>
|
||||||
matrixCallMode.value === "audio" ? "Audioanruf" : "Videokonferenz"
|
matrixCallMode.value === "audio" ? "Audioanruf" : "Videokonferenz"
|
||||||
)
|
)
|
||||||
@@ -291,6 +298,26 @@ const mergeMatrixMessages = (incomingMessages) => {
|
|||||||
loadAttachmentPreviews()
|
loadAttachmentPreviews()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const findMessage = (messageId) =>
|
||||||
|
matrixMessages.value.find((message) => message.id === messageId)
|
||||||
|
|
||||||
|
const replyPreview = (message) => {
|
||||||
|
if (!message) return ""
|
||||||
|
return message.body || message.attachment?.fileName || "Nachricht"
|
||||||
|
}
|
||||||
|
|
||||||
|
const setReplyTarget = (message) => {
|
||||||
|
matrixReplyTarget.value = {
|
||||||
|
id: message.id,
|
||||||
|
senderDisplayName: message.own ? "Du" : message.senderDisplayName || message.sender,
|
||||||
|
body: replyPreview(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearReplyTarget = () => {
|
||||||
|
matrixReplyTarget.value = null
|
||||||
|
}
|
||||||
|
|
||||||
const scrollMessagesToBottom = async () => {
|
const scrollMessagesToBottom = async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (!matrixMessagesViewport.value) return
|
if (!matrixMessagesViewport.value) return
|
||||||
@@ -313,8 +340,12 @@ const markActiveRoomRead = async () => {
|
|||||||
if (!canUseMatrixChat.value || !activeRoom.value?.exists) return
|
if (!canUseMatrixChat.value || !activeRoom.value?.exists) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const latestMessage = matrixMessages.value.at(-1)
|
||||||
await $api(`${activeRoomEndpoint.value}/read`, {
|
await $api(`${activeRoomEndpoint.value}/read`, {
|
||||||
method: "POST"
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
eventId: latestMessage?.id
|
||||||
|
}
|
||||||
})
|
})
|
||||||
unreadRooms.value = {
|
unreadRooms.value = {
|
||||||
...unreadRooms.value,
|
...unreadRooms.value,
|
||||||
@@ -825,6 +856,7 @@ const sendMatrixMessage = async () => {
|
|||||||
sender: identity.value?.matrixUserId || "Du",
|
sender: identity.value?.matrixUserId || "Du",
|
||||||
senderDisplayName: identity.value?.displayName || "Du",
|
senderDisplayName: identity.value?.displayName || "Du",
|
||||||
body: text,
|
body: text,
|
||||||
|
replyToEventId: matrixReplyTarget.value?.id || null,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
own: true,
|
own: true,
|
||||||
pending: true,
|
pending: true,
|
||||||
@@ -842,12 +874,16 @@ const sendMatrixMessage = async () => {
|
|||||||
try {
|
try {
|
||||||
const message = await $api(`${activeRoomEndpoint.value}/messages`, {
|
const message = await $api(`${activeRoomEndpoint.value}/messages`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: { text }
|
body: {
|
||||||
|
text,
|
||||||
|
replyToEventId: matrixReplyTarget.value?.id
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
matrixMessages.value = matrixMessages.value.map((item) =>
|
matrixMessages.value = matrixMessages.value.map((item) =>
|
||||||
item.id === optimisticId ? message : item
|
item.id === optimisticId ? message : item
|
||||||
)
|
)
|
||||||
|
clearReplyTarget()
|
||||||
loadAttachmentPreviews()
|
loadAttachmentPreviews()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
matrixMessages.value = matrixMessages.value.map((item) =>
|
matrixMessages.value = matrixMessages.value.map((item) =>
|
||||||
@@ -867,12 +903,18 @@ const openAttachmentPicker = () => {
|
|||||||
matrixAttachmentInput.value?.click()
|
matrixAttachmentInput.value?.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadMatrixAttachment = async (event) => {
|
const uploadSingleMatrixAttachment = async (file) => {
|
||||||
const file = event.target.files?.[0]
|
|
||||||
event.target.value = ""
|
|
||||||
|
|
||||||
if (!file || !canUseMatrixChat.value || !activeRoom.value?.exists) return
|
if (!file || !canUseMatrixChat.value || !activeRoom.value?.exists) return
|
||||||
|
|
||||||
|
if (file.size > 25 * 1024 * 1024) {
|
||||||
|
toast.add({
|
||||||
|
title: "Anhang ist zu groß",
|
||||||
|
description: "Maximal erlaubt sind 25 MB.",
|
||||||
|
color: "warning"
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const optimisticId = `attachment-${Date.now()}`
|
const optimisticId = `attachment-${Date.now()}`
|
||||||
const optimisticMessage = {
|
const optimisticMessage = {
|
||||||
id: optimisticId,
|
id: optimisticId,
|
||||||
@@ -900,7 +942,6 @@ const uploadMatrixAttachment = async (event) => {
|
|||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append("file", file)
|
formData.append("file", file)
|
||||||
|
|
||||||
matrixAttachmentUploading.value = true
|
|
||||||
try {
|
try {
|
||||||
const message = await $api(`${activeRoomEndpoint.value}/attachments`, {
|
const message = await $api(`${activeRoomEndpoint.value}/attachments`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -918,8 +959,81 @@ const uploadMatrixAttachment = async (event) => {
|
|||||||
title: "Anhang konnte nicht gesendet werden",
|
title: "Anhang konnte nicht gesendet werden",
|
||||||
color: "error"
|
color: "error"
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadMatrixFiles = async (files) => {
|
||||||
|
const fileList = Array.from(files || [])
|
||||||
|
if (!fileList.length || !canUseMatrixChat.value || !activeRoom.value?.exists) return
|
||||||
|
|
||||||
|
matrixAttachmentUploading.value = true
|
||||||
|
matrixAttachmentUploadCount.value = fileList.length
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const file of fileList) {
|
||||||
|
await uploadSingleMatrixAttachment(file)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
matrixAttachmentUploading.value = false
|
matrixAttachmentUploading.value = false
|
||||||
|
matrixAttachmentUploadCount.value = 0
|
||||||
|
matrixDragActive.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadMatrixAttachment = async (event) => {
|
||||||
|
const files = event.target.files
|
||||||
|
event.target.value = ""
|
||||||
|
await uploadMatrixFiles(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAttachmentDrop = async (event) => {
|
||||||
|
matrixDragActive.value = false
|
||||||
|
await uploadMatrixFiles(event.dataTransfer?.files)
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadMatrixAttachment = async (attachment) => {
|
||||||
|
if (!attachment?.url) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blob = await $api(matrixMediaProxyUrl(attachment), {
|
||||||
|
responseType: "blob"
|
||||||
|
})
|
||||||
|
const objectUrl = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement("a")
|
||||||
|
link.href = objectUrl
|
||||||
|
link.download = attachment.fileName || "Anhang"
|
||||||
|
link.click()
|
||||||
|
URL.revokeObjectURL(objectUrl)
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
title: "Anhang konnte nicht geladen werden",
|
||||||
|
color: "error"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reactToMatrixMessage = async (message, key) => {
|
||||||
|
if (!message?.id || !canUseMatrixChat.value || !activeRoom.value?.exists) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $api(`${activeRoomEndpoint.value}/messages/${encodeURIComponent(message.id)}/reactions`, {
|
||||||
|
method: "POST",
|
||||||
|
body: { key }
|
||||||
|
})
|
||||||
|
const existing = message.reactions || []
|
||||||
|
const current = existing.find((reaction) => reaction.key === key)
|
||||||
|
const reactions = current
|
||||||
|
? existing.map((reaction) => reaction.key === key ? { ...reaction, count: reaction.count + 1, own: true } : reaction)
|
||||||
|
: [...existing, { key, count: 1, own: true }]
|
||||||
|
|
||||||
|
matrixMessages.value = matrixMessages.value.map((item) =>
|
||||||
|
item.id === message.id ? { ...item, reactions } : item
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
title: "Reaktion konnte nicht gesendet werden",
|
||||||
|
color: "error"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1400,8 +1514,19 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
ref="matrixMessagesViewport"
|
ref="matrixMessagesViewport"
|
||||||
class="min-h-0 flex-1 space-y-3 overflow-y-auto p-4 sm:p-5"
|
class="relative min-h-0 flex-1 space-y-3 overflow-y-auto p-4 sm:p-5"
|
||||||
|
:class="matrixDragActive ? 'ring-2 ring-inset ring-primary' : ''"
|
||||||
|
@dragenter.prevent="matrixDragActive = true"
|
||||||
|
@dragover.prevent="matrixDragActive = true"
|
||||||
|
@dragleave.prevent="matrixDragActive = false"
|
||||||
|
@drop.prevent="handleAttachmentDrop"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
v-if="matrixDragActive"
|
||||||
|
class="pointer-events-none absolute inset-4 z-10 flex items-center justify-center rounded-lg border-2 border-dashed border-primary bg-primary/10 text-sm font-medium text-primary"
|
||||||
|
>
|
||||||
|
Dateien hier ablegen
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!matrixMessages.length && !matrixMessagesLoading"
|
v-if="!matrixMessages.length && !matrixMessagesLoading"
|
||||||
class="flex h-full min-h-64 items-center justify-center text-sm text-muted"
|
class="flex h-full min-h-64 items-center justify-center text-sm text-muted"
|
||||||
@@ -1430,7 +1555,17 @@ onBeforeUnmount(() => {
|
|||||||
<span>{{ formatMessageTime(message.timestamp) }}</span>
|
<span>{{ formatMessageTime(message.timestamp) }}</span>
|
||||||
<span v-if="message.pending">wird gesendet</span>
|
<span v-if="message.pending">wird gesendet</span>
|
||||||
<span v-if="message.failed">nicht gesendet</span>
|
<span v-if="message.failed">nicht gesendet</span>
|
||||||
|
<span v-if="message.edited">bearbeitet</span>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="message.replyToEventId"
|
||||||
|
type="button"
|
||||||
|
class="mb-2 block w-full rounded border px-2 py-1 text-left text-xs opacity-80"
|
||||||
|
:class="message.own ? 'border-white/20' : 'border-default'"
|
||||||
|
>
|
||||||
|
Antwort auf {{ findMessage(message.replyToEventId)?.senderDisplayName || "Nachricht" }}:
|
||||||
|
{{ replyPreview(findMessage(message.replyToEventId)) }}
|
||||||
|
</button>
|
||||||
<p class="whitespace-pre-wrap break-words text-sm">
|
<p class="whitespace-pre-wrap break-words text-sm">
|
||||||
{{ message.body }}
|
{{ message.body }}
|
||||||
</p>
|
</p>
|
||||||
@@ -1445,12 +1580,11 @@ onBeforeUnmount(() => {
|
|||||||
:alt="message.attachment.fileName || message.body"
|
:alt="message.attachment.fileName || message.body"
|
||||||
class="max-h-72 w-full object-contain"
|
class="max-h-72 w-full object-contain"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
v-if="message.attachment.url"
|
v-if="message.attachment.url"
|
||||||
:href="matrixMediaProxyUrl(message.attachment)"
|
type="button"
|
||||||
target="_blank"
|
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm"
|
||||||
rel="noopener"
|
@click="downloadMatrixAttachment(message.attachment)"
|
||||||
class="flex items-center gap-2 px-3 py-2 text-sm"
|
|
||||||
>
|
>
|
||||||
<UIcon name="i-heroicons-paper-clip" class="size-4 shrink-0" />
|
<UIcon name="i-heroicons-paper-clip" class="size-4 shrink-0" />
|
||||||
<span class="min-w-0 flex-1 truncate">
|
<span class="min-w-0 flex-1 truncate">
|
||||||
@@ -1459,7 +1593,7 @@ onBeforeUnmount(() => {
|
|||||||
<span class="shrink-0 text-xs opacity-70">
|
<span class="shrink-0 text-xs opacity-70">
|
||||||
{{ formatAttachmentSize(message.attachment.size) }}
|
{{ formatAttachmentSize(message.attachment.size) }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="flex items-center gap-2 px-3 py-2 text-sm"
|
class="flex items-center gap-2 px-3 py-2 text-sm"
|
||||||
@@ -1473,10 +1607,58 @@ onBeforeUnmount(() => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-2 flex flex-wrap items-center gap-1">
|
||||||
|
<button
|
||||||
|
v-for="reaction in message.reactions || []"
|
||||||
|
:key="reaction.key"
|
||||||
|
type="button"
|
||||||
|
class="rounded-full px-2 py-0.5 text-xs"
|
||||||
|
:class="reaction.own ? 'bg-white/20' : 'bg-muted'"
|
||||||
|
@click="reactToMatrixMessage(message, reaction.key)"
|
||||||
|
>
|
||||||
|
{{ reaction.key }} {{ reaction.count }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-for="reactionKey in ['👍', '✅', '👀']"
|
||||||
|
:key="reactionKey"
|
||||||
|
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="reactToMatrixMessage(message, reactionKey)"
|
||||||
|
>
|
||||||
|
{{ reactionKey }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
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="setReplyTarget(message)"
|
||||||
|
>
|
||||||
|
Antworten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="matrixReplyTarget"
|
||||||
|
class="flex shrink-0 items-center gap-3 border-t border-default bg-default px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<UIcon name="i-heroicons-arrow-uturn-left" class="size-4 text-muted" />
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<p class="truncate text-xs text-muted">Antwort an {{ matrixReplyTarget.senderDisplayName }}</p>
|
||||||
|
<p class="truncate text-highlighted">{{ matrixReplyTarget.body }}</p>
|
||||||
|
</div>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-x-mark"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
size="xs"
|
||||||
|
@click="clearReplyTarget"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
class="flex shrink-0 gap-2 border-t border-default bg-default p-3"
|
class="flex shrink-0 gap-2 border-t border-default bg-default p-3"
|
||||||
@submit.prevent="sendMatrixMessage"
|
@submit.prevent="sendMatrixMessage"
|
||||||
@@ -1484,6 +1666,7 @@ onBeforeUnmount(() => {
|
|||||||
<input
|
<input
|
||||||
ref="matrixAttachmentInput"
|
ref="matrixAttachmentInput"
|
||||||
type="file"
|
type="file"
|
||||||
|
multiple
|
||||||
class="hidden"
|
class="hidden"
|
||||||
@change="uploadMatrixAttachment"
|
@change="uploadMatrixAttachment"
|
||||||
>
|
>
|
||||||
@@ -1499,15 +1682,15 @@ onBeforeUnmount(() => {
|
|||||||
<UInput
|
<UInput
|
||||||
v-model="matrixMessageDraft"
|
v-model="matrixMessageDraft"
|
||||||
class="min-w-0 flex-1"
|
class="min-w-0 flex-1"
|
||||||
placeholder="Nachricht schreiben"
|
:placeholder="matrixAttachmentUploading ? `${matrixAttachmentUploadCount} Anhang/Anhänge werden hochgeladen` : 'Nachricht schreiben'"
|
||||||
:disabled="matrixMessageSending || matrixAttachmentUploading || !canUseMatrixChat || !activeRoom?.exists"
|
:disabled="!canSendChatInput"
|
||||||
@keydown.enter.exact.prevent="sendMatrixMessage"
|
@keydown.enter.exact.prevent="sendMatrixMessage"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
type="submit"
|
type="submit"
|
||||||
icon="i-heroicons-paper-airplane"
|
icon="i-heroicons-paper-airplane"
|
||||||
:loading="matrixMessageSending"
|
:loading="matrixMessageSending"
|
||||||
:disabled="!matrixMessageDraft.trim() || matrixAttachmentUploading || !canUseMatrixChat || !activeRoom?.exists"
|
:disabled="!matrixMessageDraft.trim() || !canSendChatInput"
|
||||||
>
|
>
|
||||||
Senden
|
Senden
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|||||||
Reference in New Issue
Block a user