feat: room scoping (org/DM/other sections), unread badge on nav, highlight.js CSS
This commit is contained in:
@@ -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
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,50 +462,132 @@
|
|||||||
</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}
|
||||||
<li>
|
<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
|
<button
|
||||||
class="w-full flex items-center gap-2 pl-1 pr-2 py-1 rounded-[50px] transition-colors text-left
|
class="w-5 h-5 flex items-center justify-center text-light/40 hover:text-light hover:bg-light/10 rounded transition-colors"
|
||||||
{$selectedRoomId === room.roomId ? 'bg-primary/20' : 'hover:bg-light/5'}"
|
onclick={() => (showCreateRoomModal = true)}
|
||||||
onclick={() => handleRoomSelect(room.roomId)}
|
title="Create room"
|
||||||
>
|
>
|
||||||
<Avatar src={room.avatarUrl} name={room.name} size="xs" />
|
<span class="material-symbols-rounded" style="font-size: 14px;">add</span>
|
||||||
<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>
|
</button>
|
||||||
</li>
|
</div>
|
||||||
{/each}
|
<ul class="flex flex-col gap-0.5">
|
||||||
</ul>
|
{#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}
|
{/if}
|
||||||
</nav>
|
</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=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';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user