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
This commit is contained in:
102
src/lib/components/matrix/MemberList.svelte
Normal file
102
src/lib/components/matrix/MemberList.svelte
Normal file
@@ -0,0 +1,102 @@
|
||||
<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}
|
||||
Reference in New Issue
Block a user