feat: room scoping (org/DM/other sections), unread badge on nav, highlight.js CSS
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user