Files
root-org/src/lib/components/matrix/MemberList.svelte
AlacrisDevs d1ce5d0951 feat: integrate Matrix chat (Option 2 - credentials stored in Supabase)
- Add matrix-js-sdk, marked, highlight.js, twemoji, @tanstack/svelte-virtual deps
- Copy Matrix core layer: /matrix/, /stores/matrix.ts, /cache/, /services/
- Copy Matrix components: matrix/, message/, chat-layout/, chat-settings/
- Copy UI components: EmojiPicker, Twemoji, ImagePreviewModal, VirtualList
- Copy utils: emojiData, twemoji, twemojiGlobal
- Replace lucide-svelte with Material Symbols in SyncRecoveryBanner
- Extend Avatar with xs size and status indicator prop
- Fix ui.ts store conflict: re-export toasts from toast.svelte.ts
- Add migration 020_matrix_credentials for storing Matrix tokens per user/org
- Add /api/matrix-credentials endpoint (GET/POST/DELETE)
- Create [orgSlug]/chat page with Matrix login form + full chat UI
- Add Chat to sidebar navigation
2026-02-07 01:44:06 +02:00

103 lines
3.1 KiB
Svelte

<script lang="ts">
import { Avatar } from "$lib/components/ui";
import UserProfileModal from "./UserProfileModal.svelte";
import type { RoomMember } from "$lib/matrix/types";
import { userPresence } from "$lib/stores/matrix";
interface Props {
members: RoomMember[];
onMemberClick?: (member: RoomMember) => void;
onStartDM?: (roomId: string) => void;
}
let { members, onMemberClick, onStartDM }: Props = $props();
let selectedMember = $state<RoomMember | null>(null);
function handleMemberClick(member: RoomMember) {
if (onMemberClick) {
onMemberClick(member);
} else {
selectedMember = member;
}
}
// Sort: online first, then admins, then by name
const sortedMembers = $derived(
[...members].sort((a, b) => {
// Online status first
const aOnline = $userPresence.get(a.userId) === "online" ? 1 : 0;
const bOnline = $userPresence.get(b.userId) === "online" ? 1 : 0;
if (bOnline !== aOnline) return bOnline - aOnline;
// Power level descending
if (b.powerLevel !== a.powerLevel) {
return b.powerLevel - a.powerLevel;
}
// Then alphabetically
return a.name.localeCompare(b.name);
}),
);
function getRoleBadge(
powerLevel: number,
): { label: string; color: string } | null {
if (powerLevel >= 100) return { label: "Admin", color: "text-red-400" };
if (powerLevel >= 50) return { label: "Mod", color: "text-yellow-400" };
return null;
}
function getPresenceStatus(userId: string): "online" | "offline" | null {
const presence = $userPresence.get(userId);
if (presence === "online") return "online";
if (presence === "offline" || presence === "unavailable") return "offline";
return null;
}
</script>
<div class="flex flex-col h-full">
<header class="p-4 border-b border-light/10">
<h3 class="font-semibold text-light">Members ({members.length})</h3>
</header>
<div class="flex-1 overflow-y-auto">
{#each sortedMembers as member}
<button
class="w-full flex items-center gap-3 px-4 py-2 hover:bg-light/5 transition-colors text-left"
onclick={() => handleMemberClick(member)}
>
<Avatar
src={member.avatarUrl}
name={member.name}
size="sm"
status={getPresenceStatus(member.userId)}
/>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="text-light truncate">{member.name}</span>
{#if getRoleBadge(member.powerLevel)}
{@const badge = getRoleBadge(member.powerLevel)}
<span class="text-xs {badge?.color}">{badge?.label}</span>
{/if}
</div>
<p class="text-xs text-light/40 truncate">{member.userId}</p>
</div>
</button>
{/each}
{#if members.length === 0}
<div class="p-4 text-center text-light/40">
<p>No members</p>
</div>
{/if}
</div>
</div>
{#if selectedMember}
<UserProfileModal
member={selectedMember}
onClose={() => (selectedMember = null)}
{onStartDM}
/>
{/if}