feat(matrix): add room invitations UI with accept/decline

- Add membership and inviter fields to SimpleRoom type
- Track room membership status in store
- Add invitedRooms derived property
- Show invites section in RoomList with accept/decline buttons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-01-29 16:13:52 +01:00
parent d37f5894dc
commit 33073ab8ac
3 changed files with 119 additions and 5 deletions

View file

@ -1,7 +1,15 @@
<script lang="ts">
import { matrixStore } from '$lib/matrix';
import RoomItem from './RoomItem.svelte';
import { MagnifyingGlass, Plus, Users, ChatCircle } from '@manacore/shared-icons';
import {
MagnifyingGlass,
Plus,
Users,
ChatCircle,
Envelope,
Check,
X,
} from '@manacore/shared-icons';
interface Props {
onCreateRoom?: () => void;
@ -22,6 +30,20 @@
room.name.toLowerCase().includes(search.toLowerCase())
)
);
let filteredInvites = $derived(
matrixStore.invitedRooms.filter((room) =>
room.name.toLowerCase().includes(search.toLowerCase())
)
);
async function acceptInvite(roomId: string) {
await matrixStore.joinRoom(roomId);
}
async function declineInvite(roomId: string) {
await matrixStore.leaveRoom(roomId);
}
</script>
<div class="flex h-full flex-col">
@ -45,6 +67,64 @@
<!-- Room List with Sections -->
<div class="chat-scrollbar flex-1 overflow-y-auto px-3">
<!-- Invites Section -->
{#if filteredInvites.length > 0}
<div class="mb-4">
<div class="flex items-center gap-2 px-2 py-2 text-xs font-semibold uppercase text-muted-foreground">
<Envelope class="h-3.5 w-3.5" />
Einladungen
<span class="px-1.5 py-0.5 rounded-full bg-gradient-to-r from-amber-500 to-orange-500 text-white text-[10px]">
{filteredInvites.length}
</span>
</div>
{#each filteredInvites as room (room.id)}
<div
class="flex items-center gap-3 px-3 py-2.5 mb-1 rounded-xl bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20"
>
<!-- Avatar -->
<div
class="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 bg-gradient-to-br from-amber-500 to-orange-500 text-white shadow-sm"
>
<span class="text-sm font-semibold">
{room.name
.split(' ')
.map((w) => w[0])
.join('')
.substring(0, 2)
.toUpperCase()}
</span>
</div>
<!-- Info -->
<div class="flex-1 min-w-0">
<p class="font-medium text-foreground truncate">{room.name}</p>
{#if room.inviter}
<p class="text-xs text-muted-foreground truncate">
Eingeladen von {room.inviter}
</p>
{/if}
</div>
<!-- Actions -->
<div class="flex gap-1.5">
<button
class="p-2 rounded-lg bg-green-500 hover:bg-green-600 text-white transition-colors"
title="Annehmen"
onclick={() => acceptInvite(room.id)}
>
<Check class="h-4 w-4" weight="bold" />
</button>
<button
class="p-2 rounded-lg bg-red-500 hover:bg-red-600 text-white transition-colors"
title="Ablehnen"
onclick={() => declineInvite(room.id)}
>
<X class="h-4 w-4" weight="bold" />
</button>
</div>
</div>
{/each}
</div>
{/if}
<!-- Direct Messages Section -->
{#if filteredDirectRooms.length > 0 || !search}
<div class="mb-2">

View file

@ -96,11 +96,17 @@ class MatrixStore {
.sort((a, b) => (b.lastMessageTime || 0) - (a.lastMessageTime || 0))
);
/** Direct message rooms */
directRooms = $derived(this.rooms.filter((r) => r.isDirect));
/** Joined rooms only */
joinedRooms = $derived(this.rooms.filter((r) => r.membership === 'join'));
/** Group rooms (non-DM) */
groupRooms = $derived(this.rooms.filter((r) => !r.isDirect));
/** Invited rooms */
invitedRooms = $derived(this.rooms.filter((r) => r.membership === 'invite'));
/** Direct message rooms (joined only) */
directRooms = $derived(this.joinedRooms.filter((r) => r.isDirect));
/** Group rooms (non-DM, joined only) */
groupRooms = $derived(this.joinedRooms.filter((r) => !r.isDirect));
/** Current selected room */
currentRoom = $derived(
@ -1173,6 +1179,25 @@ class MatrixStore {
const topicEvent = room.currentState.getStateEvents('m.room.topic', '');
const topic = (topicEvent as MatrixEvent | null)?.getContent()?.topic;
// Get membership status
const myUserId = this._client?.getUserId();
const myMember = myUserId ? room.getMember(myUserId) : null;
const membership = (myMember?.membership || 'leave') as SimpleRoom['membership'];
// Get inviter if this is an invite
let inviter: string | undefined;
if (membership === 'invite' && myMember) {
// The events array contains the invite event
const inviteEvent = room.currentState.getStateEvents('m.room.member', myUserId || '');
if (inviteEvent) {
const sender = (inviteEvent as MatrixEvent).getSender();
if (sender) {
const senderMember = room.getMember(sender);
inviter = senderMember?.name || sender;
}
}
}
return {
id: room.roomId,
name: room.name || 'Unnamed Room',
@ -1186,6 +1211,8 @@ class MatrixStore {
isDirect: this.isDirectRoom(room),
isEncrypted: room.hasEncryptionStateEvent(),
memberCount: room.getJoinedMemberCount(),
membership,
inviter,
};
}

View file

@ -57,6 +57,11 @@ export type MessageType =
| 'm.emote'
| 'm.notice';
/**
* Room membership status
*/
export type RoomMembership = 'join' | 'invite' | 'leave' | 'ban' | 'knock';
/**
* Simplified room for UI rendering
*/
@ -73,6 +78,8 @@ export interface SimpleRoom {
isDirect: boolean;
isEncrypted: boolean;
memberCount: number;
membership: RoomMembership;
inviter?: string; // User who sent the invite
}
/**