KI-AGENT: Anhänge im Chat über Matrix unterstützen
This commit is contained in:
@@ -20,6 +20,11 @@ type MatrixRoomEvent = {
|
|||||||
content?: {
|
content?: {
|
||||||
body?: string
|
body?: string
|
||||||
msgtype?: string
|
msgtype?: string
|
||||||
|
url?: string
|
||||||
|
info?: {
|
||||||
|
mimetype?: string
|
||||||
|
size?: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +64,13 @@ type MatrixTenantRoomOptions = {
|
|||||||
inviteUserIds?: string[]
|
inviteUserIds?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MatrixAttachmentInput = {
|
||||||
|
buffer: Buffer
|
||||||
|
filename: string
|
||||||
|
mimeType: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
type MatrixCachedValue<T = any> = {
|
type MatrixCachedValue<T = any> = {
|
||||||
exists: true
|
exists: true
|
||||||
cachedUntil: number
|
cachedUntil: number
|
||||||
@@ -404,6 +416,41 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mxcToMediaPath = (mxcUri: string) => {
|
||||||
|
const match = mxcUri.match(/^mxc:\/\/([^/]+)\/(.+)$/)
|
||||||
|
if (!match) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Ungültige Matrix-Media-URI"),
|
||||||
|
{ statusCode: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/_matrix/media/v3/download/${encodeURIComponent(match[1])}/${encodeURIComponent(match[2])}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const matrixMediaUrl = (mxcUri: string) => `${homeserverUrl()}${mxcToMediaPath(mxcUri)}`
|
||||||
|
|
||||||
|
const attachmentFromEvent = (event: MatrixRoomEvent) => {
|
||||||
|
const msgtype = event.content?.msgtype || "m.text"
|
||||||
|
|
||||||
|
if (!["m.file", "m.image"].includes(msgtype) || !event.content?.url) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const mimeType = event.content.info?.mimetype || "application/octet-stream"
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: msgtype === "m.image" ? "image" : "file",
|
||||||
|
url: event.content.url,
|
||||||
|
fileName: event.content.body || "Anhang",
|
||||||
|
mimeType,
|
||||||
|
size: event.content.info?.size || 0,
|
||||||
|
previewUrl: msgtype === "m.image" ? matrixMediaUrl(event.content.url) : null,
|
||||||
|
downloadUrl: matrixMediaUrl(event.content.url),
|
||||||
|
isImage: msgtype === "m.image" || mimeType.startsWith("image/"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createAccessTokenForUser = async (userId: string, tenantId: number | null) => {
|
const createAccessTokenForUser = async (userId: string, tenantId: number | null) => {
|
||||||
const matrixUserId = await matrixUserIdForUser(userId, tenantId)
|
const matrixUserId = await matrixUserIdForUser(userId, tenantId)
|
||||||
const cacheKey = `${tenantId || "global"}:${userId}`
|
const cacheKey = `${tenantId || "global"}:${userId}`
|
||||||
@@ -1118,12 +1165,16 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
name: room.name,
|
name: room.name,
|
||||||
matrixUserId: session.matrixUserId,
|
matrixUserId: session.matrixUserId,
|
||||||
messages: response.chunk
|
messages: response.chunk
|
||||||
.filter((event) => event.type === "m.room.message" && event.content?.msgtype === "m.text")
|
.filter((event) =>
|
||||||
|
event.type === "m.room.message" &&
|
||||||
|
["m.text", "m.file", "m.image"].includes(event.content?.msgtype || "")
|
||||||
|
)
|
||||||
.map((event) => ({
|
.map((event) => ({
|
||||||
id: event.event_id,
|
id: event.event_id,
|
||||||
sender: event.sender,
|
sender: event.sender,
|
||||||
senderDisplayName: members.joined[event.sender]?.display_name || event.sender,
|
senderDisplayName: members.joined[event.sender]?.display_name || event.sender,
|
||||||
body: event.content?.body || "",
|
body: event.content?.body || "",
|
||||||
|
attachment: attachmentFromEvent(event),
|
||||||
timestamp: event.origin_server_ts,
|
timestamp: event.origin_server_ts,
|
||||||
own: event.sender === session.matrixUserId,
|
own: event.sender === session.matrixUserId,
|
||||||
}))
|
}))
|
||||||
@@ -1210,6 +1261,103 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sendTenantRoomAttachment = async (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {},
|
||||||
|
attachment: MatrixAttachmentInput
|
||||||
|
) => {
|
||||||
|
if (!attachment.buffer?.length) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Attachment file is required"),
|
||||||
|
{ statusCode: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await provisionTenantRoom(userId, tenantId, options)
|
||||||
|
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
})
|
||||||
|
const upload = await requestMatrixJson<{ content_uri: string }>(
|
||||||
|
`/_matrix/media/v3/upload?filename=${encodeURIComponent(attachment.filename)}`,
|
||||||
|
session.accessToken,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": attachment.mimeType || "application/octet-stream" },
|
||||||
|
body: attachment.buffer as any,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`
|
||||||
|
const isImage = (attachment.mimeType || "").startsWith("image/")
|
||||||
|
const messageContent = {
|
||||||
|
msgtype: isImage ? "m.image" : "m.file",
|
||||||
|
body: attachment.filename,
|
||||||
|
url: upload.content_uri,
|
||||||
|
info: {
|
||||||
|
mimetype: attachment.mimeType || "application/octet-stream",
|
||||||
|
size: attachment.size,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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(messageContent),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: response.event_id,
|
||||||
|
sender: session.matrixUserId,
|
||||||
|
senderDisplayName: await getCurrentUserDisplayName(userId, tenantId),
|
||||||
|
body: attachment.filename,
|
||||||
|
attachment: {
|
||||||
|
type: isImage ? "image" : "file",
|
||||||
|
url: upload.content_uri,
|
||||||
|
fileName: attachment.filename,
|
||||||
|
mimeType: attachment.mimeType || "application/octet-stream",
|
||||||
|
size: attachment.size,
|
||||||
|
previewUrl: isImage ? matrixMediaUrl(upload.content_uri) : null,
|
||||||
|
downloadUrl: matrixMediaUrl(upload.content_uri),
|
||||||
|
isImage,
|
||||||
|
},
|
||||||
|
timestamp: Date.now(),
|
||||||
|
own: true,
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
key: room.key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMediaContent = async (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
mxcUri: string
|
||||||
|
) => {
|
||||||
|
const session = await createAccessTokenForUser(userId, tenantId)
|
||||||
|
const response = await fetch(matrixMediaUrl(mxcUri), {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${session.accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error(`Matrix media request failed with ${response.status}`),
|
||||||
|
{ statusCode: response.status }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
buffer: Buffer.from(await response.arrayBuffer()),
|
||||||
|
contentType: response.headers.get("content-type") || "application/octet-stream",
|
||||||
|
contentLength: response.headers.get("content-length"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createElementRoomSession = async (
|
const createElementRoomSession = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
tenantId: number | null,
|
tenantId: number | null,
|
||||||
@@ -1442,6 +1590,17 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
text
|
text
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const sendGeneralRoomAttachment = (userId: string, tenantId: number | null, attachment: MatrixAttachmentInput) =>
|
||||||
|
sendTenantRoomAttachment(
|
||||||
|
userId,
|
||||||
|
tenantId,
|
||||||
|
{
|
||||||
|
key: "allgemein",
|
||||||
|
name: "Allgemeiner Chat",
|
||||||
|
},
|
||||||
|
attachment
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getStatus,
|
getStatus,
|
||||||
matrixUserIdForUser,
|
matrixUserIdForUser,
|
||||||
@@ -1456,11 +1615,14 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
getTenantRoomMessages,
|
getTenantRoomMessages,
|
||||||
getTenantRoomMembers,
|
getTenantRoomMembers,
|
||||||
sendTenantRoomMessage,
|
sendTenantRoomMessage,
|
||||||
|
sendTenantRoomAttachment,
|
||||||
|
getMediaContent,
|
||||||
createElementRoomSession,
|
createElementRoomSession,
|
||||||
createLiveKitRoomSession,
|
createLiveKitRoomSession,
|
||||||
syncTenantRoomMembers,
|
syncTenantRoomMembers,
|
||||||
getGeneralRoomMessages,
|
getGeneralRoomMessages,
|
||||||
getGeneralRoomMembers,
|
getGeneralRoomMembers,
|
||||||
sendGeneralRoomMessage,
|
sendGeneralRoomMessage,
|
||||||
|
sendGeneralRoomAttachment,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createHash } from "node:crypto"
|
import { createHash } from "node:crypto"
|
||||||
import { FastifyInstance } from "fastify"
|
import { FastifyInstance } from "fastify"
|
||||||
|
import multipart from "@fastify/multipart"
|
||||||
import { and, eq, inArray, ne } from "drizzle-orm"
|
import { and, eq, inArray, ne } from "drizzle-orm"
|
||||||
import { authProfiles, authTenantUsers, authUsers, notificationsItems, projects } from "../../db/schema"
|
import { authProfiles, authTenantUsers, authUsers, notificationsItems, projects } from "../../db/schema"
|
||||||
import { matrixService } from "../modules/matrix.service"
|
import { matrixService } from "../modules/matrix.service"
|
||||||
@@ -24,6 +25,10 @@ type ChatRecipient = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function communicationRoutes(server: FastifyInstance) {
|
export default async function communicationRoutes(server: FastifyInstance) {
|
||||||
|
await server.register(multipart, {
|
||||||
|
limits: { fileSize: 25 * 1024 * 1024 },
|
||||||
|
})
|
||||||
|
|
||||||
const matrix = matrixService(server)
|
const matrix = matrixService(server)
|
||||||
const notifications = new NotificationService(server, getUserDirectory)
|
const notifications = new NotificationService(server, getUserDirectory)
|
||||||
const handleMatrixError = (req: any, reply: any, err: any, fallbackMessage: string) => {
|
const handleMatrixError = (req: any, reply: any, err: any, fallbackMessage: string) => {
|
||||||
@@ -226,6 +231,24 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
return { read: ids.length }
|
return { read: ids.length }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uploadedAttachmentFromRequest = async (req: any) => {
|
||||||
|
const data = await req.file()
|
||||||
|
if (!data?.file) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Keine Datei hochgeladen"),
|
||||||
|
{ statusCode: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = await data.toBuffer()
|
||||||
|
return {
|
||||||
|
buffer,
|
||||||
|
filename: data.filename || "Anhang",
|
||||||
|
mimeType: data.mimetype || "application/octet-stream",
|
||||||
|
size: buffer.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const callModeFromRequest = (req: any): "audio" | "video" => {
|
const callModeFromRequest = (req: any): "audio" | "video" => {
|
||||||
const body = (req.body || {}) as { mode?: string }
|
const body = (req.body || {}) as { mode?: string }
|
||||||
return body.mode === "audio" ? "audio" : "video"
|
return body.mode === "audio" ? "audio" : "video"
|
||||||
@@ -336,6 +359,23 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.get("/communication/matrix/media", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const query = req.query as { uri?: string; name?: string }
|
||||||
|
if (!query.uri) return reply.code(400).send({ error: "Matrix-Media-URI fehlt" })
|
||||||
|
|
||||||
|
const media = await matrix.getMediaContent(req.user.user_id, req.user.tenant_id, query.uri)
|
||||||
|
reply.header("Content-Type", media.contentType)
|
||||||
|
if (media.contentLength) reply.header("Content-Length", media.contentLength)
|
||||||
|
if (query.name) {
|
||||||
|
reply.header("Content-Disposition", `inline; filename="${query.name.replace(/"/g, "")}"`)
|
||||||
|
}
|
||||||
|
return reply.send(media.buffer)
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix media failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
server.get("/communication/matrix/project-rooms", async (req, reply) => {
|
server.get("/communication/matrix/project-rooms", async (req, reply) => {
|
||||||
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" })
|
||||||
@@ -611,6 +651,18 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.post("/communication/matrix/rooms/general/attachments", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const attachment = await uploadedAttachmentFromRequest(req)
|
||||||
|
const message = await matrix.sendGeneralRoomAttachment(req.user.user_id, req.user.tenant_id, attachment)
|
||||||
|
const room = await matrix.getTenantRoomStatus(req.user.tenant_id, "allgemein", "Allgemeiner Chat")
|
||||||
|
await notifyUsersAboutChatMessage(req, room, message, `Anhang: ${attachment.filename}`)
|
||||||
|
return message
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix attachment send failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
server.get("/communication/matrix/rooms/:roomKey", async (req, reply) => {
|
server.get("/communication/matrix/rooms/:roomKey", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const params = req.params as { roomKey: string }
|
const params = req.params as { roomKey: string }
|
||||||
@@ -721,4 +773,21 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
return handleMatrixError(req, reply, err, "Matrix message send failed")
|
return handleMatrixError(req, reply, err, "Matrix message send failed")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.post("/communication/matrix/rooms/:roomKey/attachments", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const attachment = await uploadedAttachmentFromRequest(req)
|
||||||
|
const message = await matrix.sendTenantRoomAttachment(
|
||||||
|
req.user.user_id,
|
||||||
|
req.user.tenant_id,
|
||||||
|
roomOptionsFromRequest(req),
|
||||||
|
attachment
|
||||||
|
)
|
||||||
|
const room = await matrix.getTenantRoomStatus(req.user.tenant_id, message.key)
|
||||||
|
await notifyUsersAboutChatMessage(req, room, message, `Anhang: ${attachment.filename}`)
|
||||||
|
return message
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix attachment send failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const matrixMessages = ref([])
|
|||||||
const matrixMembers = ref([])
|
const matrixMembers = ref([])
|
||||||
const matrixMessageDraft = ref("")
|
const matrixMessageDraft = ref("")
|
||||||
const matrixMessagesViewport = ref(null)
|
const matrixMessagesViewport = ref(null)
|
||||||
|
const matrixAttachmentInput = ref(null)
|
||||||
const roomCreateOpen = ref(false)
|
const roomCreateOpen = ref(false)
|
||||||
const collapsedRoomGroups = ref({})
|
const collapsedRoomGroups = ref({})
|
||||||
const matrixCallOpen = ref(false)
|
const matrixCallOpen = ref(false)
|
||||||
@@ -46,6 +47,7 @@ const roomMembersSyncing = ref(false)
|
|||||||
const matrixMessagesLoading = ref(false)
|
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 matrixAutoRefreshActive = ref(false)
|
const matrixAutoRefreshActive = ref(false)
|
||||||
const lastUpdated = ref(null)
|
const lastUpdated = ref(null)
|
||||||
let matrixRefreshInterval = null
|
let matrixRefreshInterval = null
|
||||||
@@ -856,6 +858,67 @@ const sendMatrixMessage = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openAttachmentPicker = () => {
|
||||||
|
if (!canUseMatrixChat.value || !activeRoom.value?.exists || matrixAttachmentUploading.value) return
|
||||||
|
matrixAttachmentInput.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadMatrixAttachment = async (event) => {
|
||||||
|
const file = event.target.files?.[0]
|
||||||
|
event.target.value = ""
|
||||||
|
|
||||||
|
if (!file || !canUseMatrixChat.value || !activeRoom.value?.exists) return
|
||||||
|
|
||||||
|
const optimisticId = `attachment-${Date.now()}`
|
||||||
|
const optimisticMessage = {
|
||||||
|
id: optimisticId,
|
||||||
|
sender: identity.value?.matrixUserId || "Du",
|
||||||
|
senderDisplayName: identity.value?.displayName || "Du",
|
||||||
|
body: file.name,
|
||||||
|
attachment: {
|
||||||
|
fileName: file.name,
|
||||||
|
mimeType: file.type || "application/octet-stream",
|
||||||
|
size: file.size,
|
||||||
|
isImage: file.type?.startsWith("image/"),
|
||||||
|
},
|
||||||
|
timestamp: Date.now(),
|
||||||
|
own: true,
|
||||||
|
pending: true,
|
||||||
|
failed: false
|
||||||
|
}
|
||||||
|
|
||||||
|
matrixMessages.value = [
|
||||||
|
...matrixMessages.value,
|
||||||
|
optimisticMessage
|
||||||
|
]
|
||||||
|
await scrollMessagesToBottom()
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append("file", file)
|
||||||
|
|
||||||
|
matrixAttachmentUploading.value = true
|
||||||
|
try {
|
||||||
|
const message = await $api(`${activeRoomEndpoint.value}/attachments`, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
|
||||||
|
matrixMessages.value = matrixMessages.value.map((item) =>
|
||||||
|
item.id === optimisticId ? message : item
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
matrixMessages.value = matrixMessages.value.map((item) =>
|
||||||
|
item.id === optimisticId ? { ...item, pending: false, failed: true } : item
|
||||||
|
)
|
||||||
|
toast.add({
|
||||||
|
title: "Anhang konnte nicht gesendet werden",
|
||||||
|
color: "error"
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
matrixAttachmentUploading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const startMatrixAutoRefresh = () => {
|
const startMatrixAutoRefresh = () => {
|
||||||
if (matrixRefreshInterval) return
|
if (matrixRefreshInterval) return
|
||||||
|
|
||||||
@@ -885,6 +948,25 @@ const formatMessageTime = (timestamp) => {
|
|||||||
}).format(new Date(timestamp))
|
}).format(new Date(timestamp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatAttachmentSize = (size) => {
|
||||||
|
const bytes = Number(size || 0)
|
||||||
|
if (!bytes) return ""
|
||||||
|
if (bytes < 1024) return `${bytes} B`
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||||
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||||||
|
}
|
||||||
|
|
||||||
|
const matrixMediaProxyUrl = (attachment) => {
|
||||||
|
if (!attachment?.url) return ""
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
uri: attachment.url,
|
||||||
|
name: attachment.fileName || "Anhang"
|
||||||
|
})
|
||||||
|
|
||||||
|
return `/api/communication/matrix/media?${params.toString()}`
|
||||||
|
}
|
||||||
|
|
||||||
const formatLastUpdated = computed(() => {
|
const formatLastUpdated = computed(() => {
|
||||||
if (!lastUpdated.value) return "Noch nicht aktualisiert"
|
if (!lastUpdated.value) return "Noch nicht aktualisiert"
|
||||||
|
|
||||||
@@ -1316,6 +1398,45 @@ onBeforeUnmount(() => {
|
|||||||
<p class="whitespace-pre-wrap break-words text-sm">
|
<p class="whitespace-pre-wrap break-words text-sm">
|
||||||
{{ message.body }}
|
{{ message.body }}
|
||||||
</p>
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="message.attachment"
|
||||||
|
class="mt-2 overflow-hidden rounded-md border"
|
||||||
|
:class="message.own ? 'border-white/20' : 'border-default'"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="message.attachment.isImage && message.attachment.url"
|
||||||
|
:src="matrixMediaProxyUrl(message.attachment)"
|
||||||
|
:alt="message.attachment.fileName || message.body"
|
||||||
|
class="max-h-72 w-full object-contain"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="message.attachment.url"
|
||||||
|
:href="matrixMediaProxyUrl(message.attachment)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="flex items-center gap-2 px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<UIcon name="i-heroicons-paper-clip" class="size-4 shrink-0" />
|
||||||
|
<span class="min-w-0 flex-1 truncate">
|
||||||
|
{{ message.attachment.fileName || message.body }}
|
||||||
|
</span>
|
||||||
|
<span class="shrink-0 text-xs opacity-70">
|
||||||
|
{{ formatAttachmentSize(message.attachment.size) }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex items-center gap-2 px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<UIcon name="i-heroicons-paper-clip" class="size-4 shrink-0" />
|
||||||
|
<span class="min-w-0 flex-1 truncate">
|
||||||
|
{{ message.attachment.fileName || message.body }}
|
||||||
|
</span>
|
||||||
|
<span class="shrink-0 text-xs opacity-70">
|
||||||
|
{{ formatAttachmentSize(message.attachment.size) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1324,18 +1445,33 @@ onBeforeUnmount(() => {
|
|||||||
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"
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
ref="matrixAttachmentInput"
|
||||||
|
type="file"
|
||||||
|
class="hidden"
|
||||||
|
@change="uploadMatrixAttachment"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
type="button"
|
||||||
|
icon="i-heroicons-paper-clip"
|
||||||
|
color="neutral"
|
||||||
|
variant="outline"
|
||||||
|
:loading="matrixAttachmentUploading"
|
||||||
|
:disabled="matrixAttachmentUploading || !canUseMatrixChat || !activeRoom?.exists"
|
||||||
|
@click="openAttachmentPicker"
|
||||||
|
/>
|
||||||
<UInput
|
<UInput
|
||||||
v-model="matrixMessageDraft"
|
v-model="matrixMessageDraft"
|
||||||
class="min-w-0 flex-1"
|
class="min-w-0 flex-1"
|
||||||
placeholder="Nachricht schreiben"
|
placeholder="Nachricht schreiben"
|
||||||
:disabled="matrixMessageSending || !canUseMatrixChat || !activeRoom?.exists"
|
:disabled="matrixMessageSending || matrixAttachmentUploading || !canUseMatrixChat || !activeRoom?.exists"
|
||||||
@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() || !canUseMatrixChat || !activeRoom?.exists"
|
:disabled="!matrixMessageDraft.trim() || matrixAttachmentUploading || !canUseMatrixChat || !activeRoom?.exists"
|
||||||
>
|
>
|
||||||
Senden
|
Senden
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|||||||
Reference in New Issue
Block a user