KI-AGENT: Mitgliederverwaltung und Suche im Chat umsetzen
This commit is contained in:
@@ -14,12 +14,17 @@ const unreadRooms = ref({})
|
||||
const activeRoomKey = ref(typeof route.query.room === "string" ? route.query.room : "allgemein")
|
||||
const matrixMessages = ref([])
|
||||
const matrixMembers = ref([])
|
||||
const matrixTenantUsers = ref([])
|
||||
const matrixMessageDraft = ref("")
|
||||
const matrixMessagesViewport = ref(null)
|
||||
const matrixAttachmentInput = ref(null)
|
||||
const matrixAttachmentObjectUrls = ref({})
|
||||
const matrixReplyTarget = ref(null)
|
||||
const matrixDragActive = ref(false)
|
||||
const memberInviteOpen = ref(false)
|
||||
const memberInviteUserId = ref("")
|
||||
const matrixSearchQuery = ref("")
|
||||
const matrixSearchResults = ref([])
|
||||
const roomCreateOpen = ref(false)
|
||||
const collapsedRoomGroups = ref({})
|
||||
const matrixCallOpen = ref(false)
|
||||
@@ -49,6 +54,9 @@ const roomCreating = ref(false)
|
||||
const roomMembersSyncing = ref(false)
|
||||
const matrixMessagesLoading = ref(false)
|
||||
const matrixMembersLoading = ref(false)
|
||||
const memberInviting = ref(false)
|
||||
const memberRemovingId = ref("")
|
||||
const matrixSearchLoading = ref(false)
|
||||
const matrixMessageSending = ref(false)
|
||||
const matrixAttachmentUploading = ref(false)
|
||||
const matrixAttachmentUploadCount = ref(0)
|
||||
@@ -93,6 +101,20 @@ const canSendChatInput = computed(() =>
|
||||
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(() =>
|
||||
matrixCallMode.value === "audio" ? "Audioanruf" : "Videokonferenz"
|
||||
)
|
||||
@@ -223,6 +245,8 @@ const setActiveRoom = async (room) => {
|
||||
activeRoomKey.value = room.key
|
||||
matrixMessages.value = []
|
||||
matrixMembers.value = []
|
||||
matrixSearchResults.value = []
|
||||
memberInviteOpen.value = false
|
||||
await loadRoomChat({ includeMembers: true })
|
||||
}
|
||||
|
||||
@@ -253,6 +277,8 @@ const provisionRoomFromList = async (room) => {
|
||||
activeRoomKey.value = createdRoom.key
|
||||
matrixMessages.value = []
|
||||
matrixMembers.value = []
|
||||
matrixSearchResults.value = []
|
||||
memberInviteOpen.value = false
|
||||
|
||||
toast.add({
|
||||
title: "Chatraum ist bereit",
|
||||
@@ -359,12 +385,13 @@ const markActiveRoomRead = async () => {
|
||||
const loadChatInfo = async () => {
|
||||
loading.value = true
|
||||
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/me"),
|
||||
$api("/api/communication/matrix/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
|
||||
@@ -372,6 +399,7 @@ const loadChatInfo = async () => {
|
||||
matrixRooms.value = roomsRes.rooms || []
|
||||
matrixProjectRooms.value = projectRoomsRes.rooms || []
|
||||
matrixDirectRooms.value = directRoomsRes.rooms || []
|
||||
matrixTenantUsers.value = usersRes.users || []
|
||||
lastUpdated.value = new Date()
|
||||
await loadUnreadCounts()
|
||||
|
||||
@@ -442,6 +470,8 @@ const createRoom = async () => {
|
||||
activeRoomKey.value = room.key
|
||||
matrixMessages.value = []
|
||||
matrixMembers.value = []
|
||||
matrixSearchResults.value = []
|
||||
memberInviteOpen.value = false
|
||||
roomCreateForm.value = {
|
||||
name: "",
|
||||
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") => {
|
||||
if (matrixLiveKitRoom && matrixCallConnected.value) {
|
||||
matrixCallOpen.value = true
|
||||
@@ -1708,6 +1821,59 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<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 class="mb-2 flex items-center justify-between gap-2">
|
||||
<h4 class="text-xs font-medium uppercase text-muted">
|
||||
@@ -1715,7 +1881,7 @@ onBeforeUnmount(() => {
|
||||
</h4>
|
||||
<div class="flex items-center gap-1">
|
||||
<UButton
|
||||
icon="i-heroicons-user-plus"
|
||||
icon="i-heroicons-users"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
@@ -1723,6 +1889,14 @@ onBeforeUnmount(() => {
|
||||
:disabled="!canUseMatrixChat || !activeRoom?.exists"
|
||||
@click="syncRoomMembers"
|
||||
/>
|
||||
<UButton
|
||||
icon="i-heroicons-plus"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
:disabled="!canUseMatrixChat || !activeRoom?.exists || !availableRoomInviteUsers.length"
|
||||
@click="toggleMemberInvite"
|
||||
/>
|
||||
<UButton
|
||||
icon="i-heroicons-arrow-path"
|
||||
color="neutral"
|
||||
@@ -1735,6 +1909,36 @@ onBeforeUnmount(() => {
|
||||
</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
|
||||
v-for="member in matrixMembers"
|
||||
@@ -1752,6 +1956,17 @@ onBeforeUnmount(() => {
|
||||
{{ member.matrixUserId }}
|
||||
</p>
|
||||
</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>
|
||||
|
||||
<p
|
||||
|
||||
Reference in New Issue
Block a user