KI-AGENT: Mitgliederverwaltung und Suche im Chat umsetzen
This commit is contained in:
@@ -47,6 +47,19 @@ type MatrixJoinedMembersResponse = {
|
|||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MatrixRoomSearchResponse = {
|
||||||
|
search_categories?: {
|
||||||
|
room_events?: {
|
||||||
|
count?: number
|
||||||
|
results?: Array<{
|
||||||
|
result?: MatrixRoomEvent & {
|
||||||
|
room_id?: string
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type MatrixUserSession = {
|
type MatrixUserSession = {
|
||||||
accessToken: string
|
accessToken: string
|
||||||
matrixUserId: string
|
matrixUserId: string
|
||||||
@@ -1273,6 +1286,90 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const searchTenantRoomMessages = async (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {},
|
||||||
|
query: string
|
||||||
|
) => {
|
||||||
|
const searchTerm = query.trim()
|
||||||
|
|
||||||
|
if (searchTerm.length < 2) {
|
||||||
|
return {
|
||||||
|
roomId: "",
|
||||||
|
alias: "",
|
||||||
|
key: options.key || "allgemein",
|
||||||
|
name: options.name || options.key || "Chat",
|
||||||
|
count: 0,
|
||||||
|
results: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await provisionTenantRoom(userId, tenantId, options)
|
||||||
|
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
})
|
||||||
|
const [response, members] = await Promise.all([
|
||||||
|
requestMatrixJson<MatrixRoomSearchResponse>(
|
||||||
|
"/_matrix/client/v3/search",
|
||||||
|
session.accessToken,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
search_categories: {
|
||||||
|
room_events: {
|
||||||
|
search_term: searchTerm,
|
||||||
|
keys: ["content.body"],
|
||||||
|
order_by: "recent",
|
||||||
|
filter: {
|
||||||
|
limit: 25,
|
||||||
|
rooms: [room.roomId],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
requestMatrixJson<MatrixJoinedMembersResponse>(
|
||||||
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/joined_members`,
|
||||||
|
session.accessToken
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
const roomEvents = response.search_categories?.room_events
|
||||||
|
const results = (roomEvents?.results || [])
|
||||||
|
.map((item) => item.result)
|
||||||
|
.filter((event): event is MatrixRoomEvent & { room_id?: string } =>
|
||||||
|
Boolean(
|
||||||
|
event?.event_id &&
|
||||||
|
event.type === "m.room.message" &&
|
||||||
|
["m.text", "m.file", "m.image"].includes(event.content?.msgtype || "")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((event) => ({
|
||||||
|
id: event.event_id,
|
||||||
|
roomId: event.room_id || room.roomId,
|
||||||
|
key: room.key,
|
||||||
|
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,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
key: room.key,
|
||||||
|
name: room.name,
|
||||||
|
count: roomEvents?.count || results.length,
|
||||||
|
results,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sendTenantRoomMessage = async (
|
const sendTenantRoomMessage = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
tenantId: number | null,
|
tenantId: number | null,
|
||||||
@@ -1608,13 +1705,18 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
))
|
))
|
||||||
.where(eq(authTenantUsers.tenant_id, tenant.id))
|
.where(eq(authTenantUsers.tenant_id, tenant.id))
|
||||||
|
|
||||||
return rows
|
const users = rows
|
||||||
.filter((row) => row.profileActive !== false)
|
.filter((row) => row.profileActive !== false)
|
||||||
.map((row) => ({
|
.map((row) => ({
|
||||||
userId: row.userId,
|
userId: row.userId,
|
||||||
email: row.email,
|
email: row.email,
|
||||||
displayName: row.fullName || `${row.firstName || ""} ${row.lastName || ""}`.trim() || row.email,
|
displayName: row.fullName || `${row.firstName || ""} ${row.lastName || ""}`.trim() || row.email,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
return await Promise.all(users.map(async (user) => ({
|
||||||
|
...user,
|
||||||
|
matrixUserId: await matrixUserIdForUser(user.userId, tenant.id),
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
const inviteMatrixUserToRoom = async (
|
const inviteMatrixUserToRoom = async (
|
||||||
@@ -1708,6 +1810,102 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inviteTenantRoomMember = async (
|
||||||
|
requestingUserId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {},
|
||||||
|
targetUserId: string
|
||||||
|
) => {
|
||||||
|
if (!targetUserId) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Benutzer ist erforderlich"),
|
||||||
|
{ statusCode: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await listTenantCommunicationUsers(tenantId)
|
||||||
|
const targetUser = users.find((user) => user.userId === targetUserId)
|
||||||
|
|
||||||
|
if (!targetUser) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Benutzer gehört nicht zum aktiven Mandanten"),
|
||||||
|
{ statusCode: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await provisionTenantRoom(requestingUserId, tenantId, options)
|
||||||
|
const account = await provisionCurrentUser(targetUser.userId, tenantId)
|
||||||
|
const inviteStatus = await inviteMatrixUserToRoom(
|
||||||
|
{ roomId: room.roomId },
|
||||||
|
account.matrixUserId,
|
||||||
|
`FEDEO-Einladung: ${room.name}`
|
||||||
|
)
|
||||||
|
await ensureCurrentUserJoinedRoom(targetUser.userId, tenantId, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
key: room.key,
|
||||||
|
name: room.name,
|
||||||
|
userId: targetUser.userId,
|
||||||
|
email: targetUser.email,
|
||||||
|
displayName: targetUser.displayName,
|
||||||
|
matrixUserId: account.matrixUserId,
|
||||||
|
status: inviteStatus === "invited" ? "joined" : inviteStatus,
|
||||||
|
ok: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeTenantRoomMember = async (
|
||||||
|
requestingUserId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
options: MatrixTenantRoomOptions = {},
|
||||||
|
matrixUserId: string
|
||||||
|
) => {
|
||||||
|
if (!matrixUserId) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Matrix-Benutzer ist erforderlich"),
|
||||||
|
{ statusCode: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await provisionTenantRoom(requestingUserId, tenantId, options)
|
||||||
|
const session = await createAccessTokenForUser(requestingUserId, tenantId)
|
||||||
|
|
||||||
|
if (matrixUserId === session.matrixUserId) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Du kannst dich nicht selbst aus dem Raum entfernen"),
|
||||||
|
{ statusCode: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceLogin = await ensureServiceAccessToken()
|
||||||
|
await requestMatrixJson(
|
||||||
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/kick`,
|
||||||
|
serviceLogin.accessToken,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_id: matrixUserId,
|
||||||
|
reason: "FEDEO-Mitgliederverwaltung",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
matrixJoinedRoomCache.delete(`${matrixUserId}:${room.roomId}`)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
key: room.key,
|
||||||
|
matrixUserId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getGeneralRoomMessages = (userId: string, tenantId: number | null) =>
|
const getGeneralRoomMessages = (userId: string, tenantId: number | null) =>
|
||||||
getTenantRoomMessages(userId, tenantId, {
|
getTenantRoomMessages(userId, tenantId, {
|
||||||
key: "allgemein",
|
key: "allgemein",
|
||||||
@@ -1756,11 +1954,13 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
getTenantSpaceStatus,
|
getTenantSpaceStatus,
|
||||||
provisionCurrentTenantSpace,
|
provisionCurrentTenantSpace,
|
||||||
listTenantRooms,
|
listTenantRooms,
|
||||||
|
listTenantCommunicationUsers,
|
||||||
getTenantRoomStatus,
|
getTenantRoomStatus,
|
||||||
provisionTenantRoom,
|
provisionTenantRoom,
|
||||||
createAccessTokenForUser,
|
createAccessTokenForUser,
|
||||||
getTenantRoomMessages,
|
getTenantRoomMessages,
|
||||||
getTenantRoomMembers,
|
getTenantRoomMembers,
|
||||||
|
searchTenantRoomMessages,
|
||||||
sendTenantRoomMessage,
|
sendTenantRoomMessage,
|
||||||
sendTenantRoomReaction,
|
sendTenantRoomReaction,
|
||||||
markTenantRoomRead,
|
markTenantRoomRead,
|
||||||
@@ -1768,6 +1968,8 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
getMediaContent,
|
getMediaContent,
|
||||||
createElementRoomSession,
|
createElementRoomSession,
|
||||||
createLiveKitRoomSession,
|
createLiveKitRoomSession,
|
||||||
|
inviteTenantRoomMember,
|
||||||
|
removeTenantRoomMember,
|
||||||
syncTenantRoomMembers,
|
syncTenantRoomMembers,
|
||||||
getGeneralRoomMessages,
|
getGeneralRoomMessages,
|
||||||
getGeneralRoomMembers,
|
getGeneralRoomMembers,
|
||||||
|
|||||||
@@ -603,6 +603,15 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.get("/communication/matrix/users", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const users = await matrix.listTenantCommunicationUsers(req.user.tenant_id)
|
||||||
|
return { users }
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix users failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
server.post("/communication/matrix/rooms/general/session", async (req, reply) => {
|
server.post("/communication/matrix/rooms/general/session", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
return await matrix.createElementRoomSession(req.user.user_id, req.user.tenant_id, {
|
return await matrix.createElementRoomSession(req.user.user_id, req.user.tenant_id, {
|
||||||
@@ -713,6 +722,20 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.get("/communication/matrix/rooms/:roomKey/search", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const query = req.query as { q?: string }
|
||||||
|
return await matrix.searchTenantRoomMessages(
|
||||||
|
req.user.user_id,
|
||||||
|
req.user.tenant_id,
|
||||||
|
roomOptionsFromRequest(req),
|
||||||
|
query.q || ""
|
||||||
|
)
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix search 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(
|
||||||
@@ -725,6 +748,34 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.post("/communication/matrix/rooms/:roomKey/members/invite", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const body = req.body as { userId?: string }
|
||||||
|
return await matrix.inviteTenantRoomMember(
|
||||||
|
req.user.user_id,
|
||||||
|
req.user.tenant_id,
|
||||||
|
roomOptionsFromRequest(req),
|
||||||
|
body.userId || ""
|
||||||
|
)
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix member invite failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.delete("/communication/matrix/rooms/:roomKey/members/:matrixUserId", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const params = req.params as { matrixUserId: string }
|
||||||
|
return await matrix.removeTenantRoomMember(
|
||||||
|
req.user.user_id,
|
||||||
|
req.user.tenant_id,
|
||||||
|
roomOptionsFromRequest(req),
|
||||||
|
params.matrixUserId
|
||||||
|
)
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleMatrixError(req, reply, err, "Matrix member remove failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
server.post("/communication/matrix/rooms/:roomKey/session", async (req, reply) => {
|
server.post("/communication/matrix/rooms/:roomKey/session", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
return await matrix.createElementRoomSession(
|
return await matrix.createElementRoomSession(
|
||||||
|
|||||||
@@ -14,12 +14,17 @@ const unreadRooms = ref({})
|
|||||||
const activeRoomKey = ref(typeof route.query.room === "string" ? route.query.room : "allgemein")
|
const activeRoomKey = ref(typeof route.query.room === "string" ? route.query.room : "allgemein")
|
||||||
const matrixMessages = ref([])
|
const matrixMessages = ref([])
|
||||||
const matrixMembers = ref([])
|
const matrixMembers = ref([])
|
||||||
|
const matrixTenantUsers = ref([])
|
||||||
const matrixMessageDraft = ref("")
|
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 matrixReplyTarget = ref(null)
|
||||||
const matrixDragActive = ref(false)
|
const matrixDragActive = ref(false)
|
||||||
|
const memberInviteOpen = ref(false)
|
||||||
|
const memberInviteUserId = ref("")
|
||||||
|
const matrixSearchQuery = ref("")
|
||||||
|
const matrixSearchResults = ref([])
|
||||||
const roomCreateOpen = ref(false)
|
const roomCreateOpen = ref(false)
|
||||||
const collapsedRoomGroups = ref({})
|
const collapsedRoomGroups = ref({})
|
||||||
const matrixCallOpen = ref(false)
|
const matrixCallOpen = ref(false)
|
||||||
@@ -49,6 +54,9 @@ const roomCreating = ref(false)
|
|||||||
const roomMembersSyncing = ref(false)
|
const roomMembersSyncing = ref(false)
|
||||||
const matrixMessagesLoading = ref(false)
|
const matrixMessagesLoading = ref(false)
|
||||||
const matrixMembersLoading = ref(false)
|
const matrixMembersLoading = ref(false)
|
||||||
|
const memberInviting = ref(false)
|
||||||
|
const memberRemovingId = ref("")
|
||||||
|
const matrixSearchLoading = ref(false)
|
||||||
const matrixMessageSending = ref(false)
|
const matrixMessageSending = ref(false)
|
||||||
const matrixAttachmentUploading = ref(false)
|
const matrixAttachmentUploading = ref(false)
|
||||||
const matrixAttachmentUploadCount = ref(0)
|
const matrixAttachmentUploadCount = ref(0)
|
||||||
@@ -93,6 +101,20 @@ const canSendChatInput = computed(() =>
|
|||||||
Boolean(canUseMatrixChat.value && activeRoom.value?.exists && !matrixMessageSending.value && !matrixAttachmentUploading.value)
|
Boolean(canUseMatrixChat.value && activeRoom.value?.exists && !matrixMessageSending.value && !matrixAttachmentUploading.value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const availableRoomInviteUsers = computed(() => {
|
||||||
|
const roomMemberIds = new Set(matrixMembers.value.map((member) => member.matrixUserId))
|
||||||
|
|
||||||
|
return matrixTenantUsers.value
|
||||||
|
.filter((user) => !roomMemberIds.has(user.matrixUserId))
|
||||||
|
.sort((first, second) =>
|
||||||
|
String(first.displayName || first.email || "").localeCompare(
|
||||||
|
String(second.displayName || second.email || ""),
|
||||||
|
"de",
|
||||||
|
{ sensitivity: "base" }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const matrixCallTitle = computed(() =>
|
const matrixCallTitle = computed(() =>
|
||||||
matrixCallMode.value === "audio" ? "Audioanruf" : "Videokonferenz"
|
matrixCallMode.value === "audio" ? "Audioanruf" : "Videokonferenz"
|
||||||
)
|
)
|
||||||
@@ -223,6 +245,8 @@ const setActiveRoom = async (room) => {
|
|||||||
activeRoomKey.value = room.key
|
activeRoomKey.value = room.key
|
||||||
matrixMessages.value = []
|
matrixMessages.value = []
|
||||||
matrixMembers.value = []
|
matrixMembers.value = []
|
||||||
|
matrixSearchResults.value = []
|
||||||
|
memberInviteOpen.value = false
|
||||||
await loadRoomChat({ includeMembers: true })
|
await loadRoomChat({ includeMembers: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +277,8 @@ const provisionRoomFromList = async (room) => {
|
|||||||
activeRoomKey.value = createdRoom.key
|
activeRoomKey.value = createdRoom.key
|
||||||
matrixMessages.value = []
|
matrixMessages.value = []
|
||||||
matrixMembers.value = []
|
matrixMembers.value = []
|
||||||
|
matrixSearchResults.value = []
|
||||||
|
memberInviteOpen.value = false
|
||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
title: "Chatraum ist bereit",
|
title: "Chatraum ist bereit",
|
||||||
@@ -359,12 +385,13 @@ const markActiveRoomRead = async () => {
|
|||||||
const loadChatInfo = async () => {
|
const loadChatInfo = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const [statusRes, identityRes, roomsRes, projectRoomsRes, directRoomsRes] = await Promise.all([
|
const [statusRes, identityRes, roomsRes, projectRoomsRes, directRoomsRes, usersRes] = await Promise.all([
|
||||||
$api("/api/communication/matrix/status"),
|
$api("/api/communication/matrix/status"),
|
||||||
$api("/api/communication/matrix/me"),
|
$api("/api/communication/matrix/me"),
|
||||||
$api("/api/communication/matrix/rooms"),
|
$api("/api/communication/matrix/rooms"),
|
||||||
$api("/api/communication/matrix/project-rooms"),
|
$api("/api/communication/matrix/project-rooms"),
|
||||||
$api("/api/communication/matrix/direct-rooms")
|
$api("/api/communication/matrix/direct-rooms"),
|
||||||
|
$api("/api/communication/matrix/users")
|
||||||
])
|
])
|
||||||
|
|
||||||
status.value = statusRes
|
status.value = statusRes
|
||||||
@@ -372,6 +399,7 @@ const loadChatInfo = async () => {
|
|||||||
matrixRooms.value = roomsRes.rooms || []
|
matrixRooms.value = roomsRes.rooms || []
|
||||||
matrixProjectRooms.value = projectRoomsRes.rooms || []
|
matrixProjectRooms.value = projectRoomsRes.rooms || []
|
||||||
matrixDirectRooms.value = directRoomsRes.rooms || []
|
matrixDirectRooms.value = directRoomsRes.rooms || []
|
||||||
|
matrixTenantUsers.value = usersRes.users || []
|
||||||
lastUpdated.value = new Date()
|
lastUpdated.value = new Date()
|
||||||
await loadUnreadCounts()
|
await loadUnreadCounts()
|
||||||
|
|
||||||
@@ -442,6 +470,8 @@ const createRoom = async () => {
|
|||||||
activeRoomKey.value = room.key
|
activeRoomKey.value = room.key
|
||||||
matrixMessages.value = []
|
matrixMessages.value = []
|
||||||
matrixMembers.value = []
|
matrixMembers.value = []
|
||||||
|
matrixSearchResults.value = []
|
||||||
|
memberInviteOpen.value = false
|
||||||
roomCreateForm.value = {
|
roomCreateForm.value = {
|
||||||
name: "",
|
name: "",
|
||||||
key: "",
|
key: "",
|
||||||
@@ -545,6 +575,89 @@ const syncRoomMembers = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleMemberInvite = () => {
|
||||||
|
memberInviteOpen.value = !memberInviteOpen.value
|
||||||
|
if (memberInviteOpen.value && !memberInviteUserId.value && availableRoomInviteUsers.value[0]) {
|
||||||
|
memberInviteUserId.value = availableRoomInviteUsers.value[0].userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inviteRoomMember = async () => {
|
||||||
|
if (!memberInviteUserId.value || !canUseMatrixChat.value || !activeRoom.value?.exists) return
|
||||||
|
|
||||||
|
memberInviting.value = true
|
||||||
|
try {
|
||||||
|
const result = await $api(`${activeRoomEndpoint.value}/members/invite`, {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
userId: memberInviteUserId.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
title: "Teilnehmer eingeladen",
|
||||||
|
description: result.displayName || result.email || result.matrixUserId,
|
||||||
|
color: "success"
|
||||||
|
})
|
||||||
|
|
||||||
|
memberInviteUserId.value = ""
|
||||||
|
memberInviteOpen.value = false
|
||||||
|
await loadRoomMembers()
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
title: "Teilnehmer konnte nicht eingeladen werden",
|
||||||
|
color: "error"
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
memberInviting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeRoomMember = async (member) => {
|
||||||
|
if (!member?.matrixUserId || member.own || !canUseMatrixChat.value || !activeRoom.value?.exists) return
|
||||||
|
|
||||||
|
memberRemovingId.value = member.matrixUserId
|
||||||
|
try {
|
||||||
|
await $api(`${activeRoomEndpoint.value}/members/${encodeURIComponent(member.matrixUserId)}`, {
|
||||||
|
method: "DELETE"
|
||||||
|
})
|
||||||
|
|
||||||
|
matrixMembers.value = matrixMembers.value.filter((item) => item.matrixUserId !== member.matrixUserId)
|
||||||
|
toast.add({
|
||||||
|
title: "Teilnehmer entfernt",
|
||||||
|
color: "success"
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
title: "Teilnehmer konnte nicht entfernt werden",
|
||||||
|
color: "error"
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
memberRemovingId.value = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchRoomMessages = async () => {
|
||||||
|
const query = matrixSearchQuery.value.trim()
|
||||||
|
if (query.length < 2 || !canUseMatrixChat.value || !activeRoom.value?.exists) {
|
||||||
|
matrixSearchResults.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
matrixSearchLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await $api(`${activeRoomEndpoint.value}/search?q=${encodeURIComponent(query)}`)
|
||||||
|
matrixSearchResults.value = res.results || []
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
title: "Suche konnte nicht ausgeführt werden",
|
||||||
|
color: "error"
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
matrixSearchLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const openMatrixCall = async (mode = "video") => {
|
const openMatrixCall = async (mode = "video") => {
|
||||||
if (matrixLiveKitRoom && matrixCallConnected.value) {
|
if (matrixLiveKitRoom && matrixCallConnected.value) {
|
||||||
matrixCallOpen.value = true
|
matrixCallOpen.value = true
|
||||||
@@ -1708,6 +1821,59 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-5 p-4">
|
<div class="space-y-5 p-4">
|
||||||
|
<div>
|
||||||
|
<h4 class="mb-2 text-xs font-medium uppercase text-muted">
|
||||||
|
Suche
|
||||||
|
</h4>
|
||||||
|
<form
|
||||||
|
class="flex gap-2"
|
||||||
|
@submit.prevent="searchRoomMessages"
|
||||||
|
>
|
||||||
|
<UInput
|
||||||
|
v-model="matrixSearchQuery"
|
||||||
|
size="sm"
|
||||||
|
class="min-w-0 flex-1"
|
||||||
|
placeholder="Nachrichten suchen"
|
||||||
|
:disabled="!canUseMatrixChat || !activeRoom?.exists"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
type="submit"
|
||||||
|
icon="i-heroicons-magnifying-glass"
|
||||||
|
size="sm"
|
||||||
|
color="neutral"
|
||||||
|
variant="outline"
|
||||||
|
:loading="matrixSearchLoading"
|
||||||
|
:disabled="matrixSearchQuery.trim().length < 2 || !canUseMatrixChat || !activeRoom?.exists"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<div
|
||||||
|
v-if="matrixSearchResults.length"
|
||||||
|
class="mt-3 max-h-52 space-y-2 overflow-y-auto"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="result in matrixSearchResults"
|
||||||
|
:key="result.id"
|
||||||
|
type="button"
|
||||||
|
class="block w-full rounded-md border border-default px-3 py-2 text-left hover:bg-muted"
|
||||||
|
@click="setReplyTarget(result)"
|
||||||
|
>
|
||||||
|
<div class="mb-1 flex items-center justify-between gap-2 text-xs text-muted">
|
||||||
|
<span class="truncate">{{ result.senderDisplayName || result.sender }}</span>
|
||||||
|
<span class="shrink-0">{{ formatMessageTime(result.timestamp) }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="line-clamp-2 text-sm text-highlighted">
|
||||||
|
{{ result.body || result.attachment?.fileName || "Anhang" }}
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-else-if="matrixSearchQuery.trim().length >= 2 && !matrixSearchLoading"
|
||||||
|
class="mt-3 text-sm text-muted"
|
||||||
|
>
|
||||||
|
Keine Treffer in diesem Raum.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-2 flex items-center justify-between gap-2">
|
<div class="mb-2 flex items-center justify-between gap-2">
|
||||||
<h4 class="text-xs font-medium uppercase text-muted">
|
<h4 class="text-xs font-medium uppercase text-muted">
|
||||||
@@ -1715,7 +1881,7 @@ onBeforeUnmount(() => {
|
|||||||
</h4>
|
</h4>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-heroicons-user-plus"
|
icon="i-heroicons-users"
|
||||||
color="neutral"
|
color="neutral"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="xs"
|
size="xs"
|
||||||
@@ -1723,6 +1889,14 @@ onBeforeUnmount(() => {
|
|||||||
:disabled="!canUseMatrixChat || !activeRoom?.exists"
|
:disabled="!canUseMatrixChat || !activeRoom?.exists"
|
||||||
@click="syncRoomMembers"
|
@click="syncRoomMembers"
|
||||||
/>
|
/>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-plus"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
size="xs"
|
||||||
|
:disabled="!canUseMatrixChat || !activeRoom?.exists || !availableRoomInviteUsers.length"
|
||||||
|
@click="toggleMemberInvite"
|
||||||
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-heroicons-arrow-path"
|
icon="i-heroicons-arrow-path"
|
||||||
color="neutral"
|
color="neutral"
|
||||||
@@ -1735,6 +1909,36 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
v-if="memberInviteOpen"
|
||||||
|
class="mb-3 rounded-md border border-default p-2"
|
||||||
|
@submit.prevent="inviteRoomMember"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
v-model="memberInviteUserId"
|
||||||
|
class="mb-2 w-full rounded-md border border-default bg-default px-2 py-1.5 text-sm text-highlighted"
|
||||||
|
:disabled="memberInviting"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="user in availableRoomInviteUsers"
|
||||||
|
:key="user.userId"
|
||||||
|
:value="user.userId"
|
||||||
|
>
|
||||||
|
{{ user.displayName || user.email }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<UButton
|
||||||
|
type="submit"
|
||||||
|
icon="i-heroicons-user-plus"
|
||||||
|
size="xs"
|
||||||
|
block
|
||||||
|
:loading="memberInviting"
|
||||||
|
:disabled="!memberInviteUserId || memberInviting"
|
||||||
|
>
|
||||||
|
Einladen
|
||||||
|
</UButton>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div
|
<div
|
||||||
v-for="member in matrixMembers"
|
v-for="member in matrixMembers"
|
||||||
@@ -1752,6 +1956,17 @@ onBeforeUnmount(() => {
|
|||||||
{{ member.matrixUserId }}
|
{{ member.matrixUserId }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<UButton
|
||||||
|
v-if="!member.own"
|
||||||
|
icon="i-heroicons-x-mark"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
size="xs"
|
||||||
|
class="ml-auto"
|
||||||
|
:loading="memberRemovingId === member.matrixUserId"
|
||||||
|
:disabled="Boolean(memberRemovingId)"
|
||||||
|
@click="removeRoomMember(member)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
|
|||||||
Reference in New Issue
Block a user