feat: room scoping (org/DM/other sections), unread badge on nav, highlight.js CSS

This commit is contained in:
AlacrisDevs
2026-02-07 01:59:34 +02:00
parent be99a02e78
commit 3f267e3b13
4 changed files with 161 additions and 40 deletions

View File

@@ -288,6 +288,13 @@ export const roomSummaries = derived(rooms, ($rooms): RoomSummary[] => {
return summaries; return summaries;
}); });
/**
* Total unread count across all rooms (for nav badge)
*/
export const totalUnreadCount = derived(roomSummaries, ($summaries): number => {
return $summaries.reduce((sum, room) => sum + (room.isSpace ? 0 : room.unreadCount), 0);
});
// ============================================================================ // ============================================================================
// Messages // Messages
// ============================================================================ // ============================================================================

View File

@@ -10,6 +10,7 @@
import { hasPermission, type Permission } from "$lib/utils/permissions"; import { hasPermission, type Permission } from "$lib/utils/permissions";
import { setContext } from "svelte"; import { setContext } from "svelte";
import * as m from "$lib/paraglide/messages"; import * as m from "$lib/paraglide/messages";
import { totalUnreadCount } from "$lib/stores/matrix";
interface Member { interface Member {
id: string; id: string;
@@ -126,6 +127,7 @@
href: `/${data.org.slug}/chat`, href: `/${data.org.slug}/chat`,
label: "Chat", label: "Chat",
icon: "chat", icon: "chat",
badge: $totalUnreadCount > 0 ? ($totalUnreadCount > 99 ? "99+" : String($totalUnreadCount)) : null,
}, },
// Settings requires settings.view or admin role // Settings requires settings.view or admin role
...(canAccess("settings.view") ...(canAccess("settings.view")
@@ -223,6 +225,11 @@
? 'opacity-0 max-w-0 overflow-hidden' ? 'opacity-0 max-w-0 overflow-hidden'
: 'opacity-100 max-w-[200px]'}">{item.label}</span : 'opacity-100 max-w-[200px]'}">{item.label}</span
> >
{#if item.badge}
<span class="ml-auto bg-primary text-background text-xs font-bold px-1.5 py-0.5 rounded-full min-w-[18px] text-center shrink-0">
{item.badge}
</span>
{/if}
</a> </a>
{/each} {/each}
</nav> </nav>

View File

@@ -79,15 +79,39 @@
: [], : [],
); );
const filteredRooms = $derived( // All non-space rooms (exclude Space entries themselves from the list)
const allRooms = $derived(
$roomSummaries.filter((r) => !r.isSpace),
);
// Org rooms: rooms that belong to any Space
const orgRooms = $derived(
allRooms.filter((r) => r.parentSpaceId && !r.isDirect),
);
// DMs: direct messages (not tied to org)
const dmRooms = $derived(
allRooms.filter((r) => r.isDirect),
);
// Other rooms: not in a space and not a DM
const otherRooms = $derived(
allRooms.filter((r) => !r.parentSpaceId && !r.isDirect),
);
// Apply search filter across all sections
const filterBySearch = (rooms: typeof allRooms) =>
roomSearchQuery.trim() roomSearchQuery.trim()
? $roomSummaries.filter( ? rooms.filter(
(room) => (room) =>
room.name.toLowerCase().includes(roomSearchQuery.toLowerCase()) || room.name.toLowerCase().includes(roomSearchQuery.toLowerCase()) ||
room.topic?.toLowerCase().includes(roomSearchQuery.toLowerCase()), room.topic?.toLowerCase().includes(roomSearchQuery.toLowerCase()),
) )
: $roomSummaries, : rooms;
);
const filteredOrgRooms = $derived(filterBySearch(orgRooms));
const filteredDmRooms = $derived(filterBySearch(dmRooms));
const filteredOtherRooms = $derived(filterBySearch(otherRooms));
const currentMembers = $derived( const currentMembers = $derived(
$selectedRoomId ? getRoomMembers($selectedRoomId) : [], $selectedRoomId ? getRoomMembers($selectedRoomId) : [],
@@ -438,31 +462,31 @@
</div> </div>
</div> </div>
<!-- Room actions --> <!-- Room list (sectioned) -->
<div class="flex items-center justify-between px-3 py-1">
<span class="text-xs font-semibold text-light/40 uppercase tracking-wider">
Rooms {roomSearchQuery ? `(${filteredRooms.length})` : ""}
</span>
<div class="flex gap-1">
<button
class="w-6 h-6 flex items-center justify-center text-light/40 hover:text-light hover:bg-light/10 rounded transition-colors"
onclick={() => (showCreateRoomModal = true)}
title="Create room"
>
<span class="material-symbols-rounded" style="font-size: 16px;">add_circle</span>
</button>
</div>
</div>
<!-- Room list -->
<nav class="flex-1 overflow-y-auto px-2 pb-2"> <nav class="flex-1 overflow-y-auto px-2 pb-2">
{#if filteredRooms.length === 0} {#if allRooms.length === 0}
<p class="text-light/40 text-sm text-center py-8"> <p class="text-light/40 text-sm text-center py-8">
{roomSearchQuery ? "No matching rooms" : "No rooms yet"} {roomSearchQuery ? "No matching rooms" : "No rooms yet"}
</p> </p>
{:else} {:else}
<ul class="flex flex-col gap-1"> <!-- Org / Space Rooms -->
{#each filteredRooms as room (room.roomId)} {#if filteredOrgRooms.length > 0}
<div class="mb-2">
<div class="flex items-center justify-between px-2 py-1">
<span class="text-xs font-semibold text-light/40 uppercase tracking-wider">
<span class="material-symbols-rounded align-middle" style="font-size: 14px;">workspaces</span>
Organization
</span>
<button
class="w-5 h-5 flex items-center justify-center text-light/40 hover:text-light hover:bg-light/10 rounded transition-colors"
onclick={() => (showCreateRoomModal = true)}
title="Create room"
>
<span class="material-symbols-rounded" style="font-size: 14px;">add</span>
</button>
</div>
<ul class="flex flex-col gap-0.5">
{#each filteredOrgRooms as room (room.roomId)}
<li> <li>
<button <button
class="w-full flex items-center gap-2 pl-1 pr-2 py-1 rounded-[50px] transition-colors text-left class="w-full flex items-center gap-2 pl-1 pr-2 py-1 rounded-[50px] transition-colors text-left
@@ -482,6 +506,88 @@
</li> </li>
{/each} {/each}
</ul> </ul>
</div>
{/if}
<!-- Direct Messages -->
{#if filteredDmRooms.length > 0}
<div class="mb-2">
<div class="flex items-center justify-between px-2 py-1">
<span class="text-xs font-semibold text-light/40 uppercase tracking-wider">
<span class="material-symbols-rounded align-middle" style="font-size: 14px;">chat_bubble</span>
Direct Messages
</span>
<button
class="w-5 h-5 flex items-center justify-center text-light/40 hover:text-light hover:bg-light/10 rounded transition-colors"
onclick={() => (showStartDMModal = true)}
title="New DM"
>
<span class="material-symbols-rounded" style="font-size: 14px;">add</span>
</button>
</div>
<ul class="flex flex-col gap-0.5">
{#each filteredDmRooms as room (room.roomId)}
<li>
<button
class="w-full flex items-center gap-2 pl-1 pr-2 py-1 rounded-[50px] transition-colors text-left
{$selectedRoomId === room.roomId ? 'bg-primary/20' : 'hover:bg-light/5'}"
onclick={() => handleRoomSelect(room.roomId)}
>
<Avatar src={room.avatarUrl} name={room.name} size="xs" />
<div class="flex-1 min-w-0">
<span class="font-bold text-sm text-light truncate block">{room.name}</span>
</div>
{#if room.unreadCount > 0}
<span class="bg-primary text-white text-xs px-1.5 py-0.5 rounded-full min-w-[18px] text-center">
{room.unreadCount > 99 ? "99+" : room.unreadCount}
</span>
{/if}
</button>
</li>
{/each}
</ul>
</div>
{/if}
<!-- Other Rooms (not in a space, not DMs) -->
{#if filteredOtherRooms.length > 0}
<div class="mb-2">
<div class="flex items-center justify-between px-2 py-1">
<span class="text-xs font-semibold text-light/40 uppercase tracking-wider">
<span class="material-symbols-rounded align-middle" style="font-size: 14px;">tag</span>
Rooms
</span>
<button
class="w-5 h-5 flex items-center justify-center text-light/40 hover:text-light hover:bg-light/10 rounded transition-colors"
onclick={() => (showCreateRoomModal = true)}
title="Create room"
>
<span class="material-symbols-rounded" style="font-size: 14px;">add</span>
</button>
</div>
<ul class="flex flex-col gap-0.5">
{#each filteredOtherRooms as room (room.roomId)}
<li>
<button
class="w-full flex items-center gap-2 pl-1 pr-2 py-1 rounded-[50px] transition-colors text-left
{$selectedRoomId === room.roomId ? 'bg-primary/20' : 'hover:bg-light/5'}"
onclick={() => handleRoomSelect(room.roomId)}
>
<Avatar src={room.avatarUrl} name={room.name} size="xs" />
<div class="flex-1 min-w-0">
<span class="font-bold text-sm text-light truncate block">{room.name}</span>
</div>
{#if room.unreadCount > 0}
<span class="bg-primary text-white text-xs px-1.5 py-0.5 rounded-full min-w-[18px] text-center">
{room.unreadCount > 99 ? "99+" : room.unreadCount}
</span>
{/if}
</button>
</li>
{/each}
</ul>
</div>
{/if}
{/if} {/if}
</nav> </nav>

View File

@@ -1,6 +1,7 @@
@import url('https://fonts.googleapis.com/css2?family=Tilt+Warp&family=Work+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Tilt+Warp&family=Work+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,400,0,0&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,400,0,0&display=swap');
@import 'tailwindcss'; @import 'tailwindcss';
@import 'highlight.js/styles/github-dark.css';
@plugin '@tailwindcss/forms'; @plugin '@tailwindcss/forms';
@plugin '@tailwindcss/typography'; @plugin '@tailwindcss/typography';