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;
});
/**
* 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
// ============================================================================

View File

@@ -10,6 +10,7 @@
import { hasPermission, type Permission } from "$lib/utils/permissions";
import { setContext } from "svelte";
import * as m from "$lib/paraglide/messages";
import { totalUnreadCount } from "$lib/stores/matrix";
interface Member {
id: string;
@@ -126,6 +127,7 @@
href: `/${data.org.slug}/chat`,
label: "Chat",
icon: "chat",
badge: $totalUnreadCount > 0 ? ($totalUnreadCount > 99 ? "99+" : String($totalUnreadCount)) : null,
},
// Settings requires settings.view or admin role
...(canAccess("settings.view")
@@ -223,6 +225,11 @@
? 'opacity-0 max-w-0 overflow-hidden'
: '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>
{/each}
</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()
? $roomSummaries.filter(
? rooms.filter(
(room) =>
room.name.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(
$selectedRoomId ? getRoomMembers($selectedRoomId) : [],
@@ -438,50 +462,132 @@
</div>
</div>
<!-- Room actions -->
<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 -->
<!-- Room list (sectioned) -->
<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">
{roomSearchQuery ? "No matching rooms" : "No rooms yet"}
</p>
{:else}
<ul class="flex flex-col gap-1">
{#each filteredRooms as room (room.roomId)}
<li>
<!-- Org / Space Rooms -->
{#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-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)}
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"
>
<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}
<span class="material-symbols-rounded" style="font-size: 14px;">add</span>
</button>
</li>
{/each}
</ul>
</div>
<ul class="flex flex-col gap-0.5">
{#each filteredOrgRooms 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}
<!-- 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}
</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=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,400,0,0&display=swap');
@import 'tailwindcss';
@import 'highlight.js/styles/github-dark.css';
@plugin '@tailwindcss/forms';
@plugin '@tailwindcss/typography';