Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions apps/desktop/src/components/channels/ChannelBrowser.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<script lang="ts">
import Hash from "@lucide/svelte/icons/hash";
import Search from "@lucide/svelte/icons/search";
import Users from "@lucide/svelte/icons/users";
import type { Channel } from "@packages/api-client";

interface Props {
channels: Channel[];
isOpen: boolean;
onClose: () => void;
onJoin: (channelId: string) => void;
}

let { channels, isOpen, onClose, onJoin }: Props = $props();

let dialog: HTMLDialogElement | null = $state(null);
let searchQuery = $state("");
let showJoined = $state(false);

const filteredChannels = $derived(
channels
.filter((ch) => ch.type === "public")
.filter((ch) => (showJoined ? ch.joined : !ch.joined))
.filter((ch) =>
ch.name.toLowerCase().includes(searchQuery.toLowerCase()),
),
);

$effect(() => {
if (isOpen) {
searchQuery = "";
showJoined = false;
dialog?.showModal();
} else {
dialog?.close();
}
});

function handleJoin(channelId: string) {
onJoin(channelId);
}
</script>

<dialog bind:this={dialog} class="modal" onclose={onClose}>
<div class="modal-box max-w-lg">
<h3 class="mb-4 text-lg font-medium">Browse Channels</h3>

<div class="mb-4 flex items-center gap-2">
<div class="relative flex-1">
<Search
class="text-muted absolute top-1/2 left-3 size-4 -translate-y-1/2"
/>
<input
type="text"
placeholder="Search channels..."
class="input input-bordered w-full pl-10"
bind:value={searchQuery}
/>
</div>
</div>

<div class="mb-4">
<div class="tabs tabs-box">
<button
type="button"
class={["tab", !showJoined && "tab-active"]}
onclick={() => (showJoined = false)}
>
Available
</button>
<button
type="button"
class={["tab", showJoined && "tab-active"]}
onclick={() => (showJoined = true)}
>
Joined
</button>
</div>
</div>

<div class="max-h-64 overflow-y-auto">
{#if filteredChannels.length === 0}
<div class="text-muted py-8 text-center text-sm">
{#if showJoined}
No joined channels
{:else}
No available channels to join
{/if}
</div>
{:else}
<ul class="flex flex-col gap-1">
{#each filteredChannels as channel (channel.id)}
<li
class="hover:bg-base-200 flex items-center justify-between rounded-lg px-3 py-2"
>
<div class="flex items-center gap-2">
<Hash class="text-muted size-4" />
<span class="font-medium">{channel.name}</span>
{#if channel.description}
<span class="text-muted text-sm">{channel.description}</span>
{/if}
</div>
<div class="flex items-center gap-3">
<span class="text-muted flex items-center gap-1 text-xs">
<Users class="size-3" />
{channel.memberCount}
</span>
{#if !channel.joined}
<button
type="button"
class="btn btn-primary btn-xs"
onclick={() => handleJoin(channel.id)}
>
Join
</button>
{:else}
<span class="text-success text-xs">Joined</span>
{/if}
</div>
</li>
{/each}
</ul>
{/if}
</div>

<div class="modal-action">
<button type="button" class="btn btn-ghost btn-sm" onclick={onClose}>
Close
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button type="submit">close</button>
</form>
</dialog>
25 changes: 24 additions & 1 deletion apps/desktop/src/components/channels/ChannelContextMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,39 @@
import ChevronRight from "@lucide/svelte/icons/chevron-right";
import FolderInput from "@lucide/svelte/icons/folder-input";
import FolderOutput from "@lucide/svelte/icons/folder-output";
import LogOut from "@lucide/svelte/icons/log-out";
import Pencil from "@lucide/svelte/icons/pencil";
import Trash2 from "@lucide/svelte/icons/trash-2";
import type { ChannelGroup } from "@packages/api-client";
import type { Channel, ChannelGroup } from "@packages/api-client";

interface Props {
x: number;
y: number;
channelType: Channel["type"];
currentGroupId: string | null;
groups: ChannelGroup[];
onEdit: () => void;
onDelete: () => void;
onLeave?: () => void;
onMoveToGroup: (groupId: string | null) => void;
onClose: () => void;
}

const {
x,
y,
channelType,
currentGroupId,
groups,
onEdit,
onDelete,
onLeave,
onMoveToGroup,
onClose,
}: Props = $props();

const canLeave = $derived(channelType !== "default");

let showMoveSubmenu = $state(false);

function handleEdit() {
Expand All @@ -40,6 +47,11 @@
onClose();
}

function handleLeave() {
onLeave?.();
onClose();
}

function handleSelect(groupId: string | null) {
onMoveToGroup(groupId);
onClose();
Expand Down Expand Up @@ -129,6 +141,17 @@
</button>
{/if}

{#if canLeave}
<button
type="button"
class="btn btn-ghost btn-sm justify-start gap-2"
onclick={handleLeave}
>
<LogOut class="size-4" />
Leave Channel
</button>
{/if}

<div class="divider my-1"></div>

<button
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/components/channels/ChannelGroup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
onDelete?: (groupId: string) => void;
onEditChannel?: (channel: Channel) => void;
onDeleteChannel?: (channelId: string) => void;
onLeaveChannel?: (channelId: string) => void;
onMoveChannelToGroup?: (channelId: string, groupId: string | null) => void;
}

Expand All @@ -50,6 +51,7 @@
onDelete,
onEditChannel,
onDeleteChannel,
onLeaveChannel,
onMoveChannelToGroup,
}: Props = $props();

Expand Down Expand Up @@ -105,6 +107,7 @@
groups={allGroups}
onEdit={onEditChannel}
onDelete={onDeleteChannel}
onLeave={onLeaveChannel}
onMoveToGroup={onMoveChannelToGroup}
/>
{/each}
Expand All @@ -128,6 +131,7 @@
{onDelete}
{onEditChannel}
{onDeleteChannel}
{onLeaveChannel}
{onMoveChannelToGroup}
/>
{/each}
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/components/channels/ChannelItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
groups?: ChannelGroup[];
onEdit?: (channel: Channel) => void;
onDelete?: (channelId: string) => void;
onLeave?: (channelId: string) => void;
onMoveToGroup?: (channelId: string, groupId: string | null) => void;
}

Expand All @@ -24,6 +25,7 @@
groups = [],
onEdit,
onDelete,
onLeave,
onMoveToGroup,
}: Props = $props();

Expand All @@ -39,10 +41,12 @@
<ChannelContextMenu
x={contextMenu.x}
y={contextMenu.y}
channelType={channel.type}
currentGroupId={channel.groupId ?? null}
{groups}
onEdit={() => onEdit?.(channel)}
onDelete={() => onDelete?.(channel.id)}
onLeave={() => onLeave?.(channel.id)}
onMoveToGroup={(groupId) => onMoveToGroup?.(channel.id, groupId)}
onClose={() => (contextMenu = null)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ export class ChannelListController {
unreadManager;
groupState;

// Filter to show only joined channels in sidebar
#joinedChannels: Channel[] = $derived.by(() =>
(this.channels.data ?? []).filter((ch) => ch.joined),
);

#organized: GroupedChannels = $derived.by(() =>
organizeChannelsIntoGroups(
this.channels.data ?? [],
this.#joinedChannels,
this.channelGroups.data ?? [],
),
);
Expand All @@ -31,6 +36,11 @@ export class ChannelListController {
return this.#organized;
}

// All channels (for channel browser)
get allChannels() {
return this.channels.data ?? [];
}

get rootGroups() {
return this.#organized.groups.filter((g) => g.parentGroupId === null);
}
Expand Down Expand Up @@ -160,4 +170,34 @@ export class ChannelListController {
refetchChannels() {
this.channels.refetch();
}

async joinChannel(channelId: string) {
try {
await this.#api.channels({ id: channelId }).join.post();
this.channels.refetch();
showToast("Joined channel", "success");
} catch (error) {
showToast("Failed to join channel", "error");
throw error;
}
}

async leaveChannel(channelId: string) {
const confirmed = await confirm({
title: "Leave Channel",
message: "Are you sure you want to leave this channel?",
confirmText: "Leave",
variant: "danger",
});
if (!confirmed) return;

try {
await this.#api.channels({ id: channelId }).leave.post();
this.channels.refetch();
showToast("Left channel", "success");
} catch (error) {
showToast("Failed to leave channel", "error");
throw error;
}
}
}
18 changes: 18 additions & 0 deletions apps/desktop/src/components/channels/ChannelList.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<script lang="ts">
import FolderPlus from "@lucide/svelte/icons/folder-plus";
import Hash from "@lucide/svelte/icons/hash";
import Plus from "@lucide/svelte/icons/plus";
import User from "@lucide/svelte/icons/user";
import type { Selection } from "$components/chat/types";
import ChannelBrowser from "./ChannelBrowser.svelte";
import ChannelGroup from "./ChannelGroup.svelte";
import ChannelItem from "./ChannelItem.svelte";
import { ChannelListController } from "./ChannelList.controller.svelte.ts";
Expand Down Expand Up @@ -31,6 +33,13 @@
Channels
</span>
<div class="flex items-center gap-1">
<button
class="btn btn-ghost btn-xs btn-square"
title="Browse channels"
onclick={() => modals.openBrowseChannels()}
>
<Hash class="text-muted size-4" />
</button>
<button
class="btn btn-ghost btn-xs btn-square"
title="New group"
Expand Down Expand Up @@ -70,6 +79,7 @@
onDelete={(id) => controller.deleteGroup(id)}
onEditChannel={(ch) => modals.openEditChannel(ch)}
onDeleteChannel={(id) => controller.deleteChannel(id)}
onLeaveChannel={(id) => controller.leaveChannel(id)}
onMoveChannelToGroup={(chId, gId) =>
controller.moveChannelToGroup(chId, gId)}
/>
Expand All @@ -89,6 +99,7 @@
groups={controller.organized.groups}
onEdit={(ch) => modals.openEditChannel(ch)}
onDelete={(id) => controller.deleteChannel(id)}
onLeave={(id) => controller.leaveChannel(id)}
onMoveToGroup={(chId, gId) =>
controller.moveChannelToGroup(chId, gId)}
/>
Expand Down Expand Up @@ -139,6 +150,13 @@
onSave={(id, name, desc) => controller.editChannel(id, name, desc)}
/>

<ChannelBrowser
channels={controller.allChannels}
isOpen={modals.browseChannels.isOpen}
onClose={() => modals.closeBrowseChannels()}
onJoin={(id) => controller.joinChannel(id)}
/>

<DMSection
{organizationId}
selectedChannelId={screenMode.type === "chat"
Expand Down
Loading