Skip to content

Commit 5626a41

Browse files
committed
channel list interactions
1 parent 6408851 commit 5626a41

File tree

10 files changed

+157
-103
lines changed

10 files changed

+157
-103
lines changed

src/lib/components/Channel.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
let unlisten: UnlistenFn | undefined;
2020
2121
onMount(async () => {
22+
if (!channel.joined) {
23+
await channel.join();
24+
}
25+
2226
unlisten = await listen<IrcMessage[]>("recentmessages", async (event) => {
2327
for (const message of event.payload) {
2428
await handlers.get(message.type)?.handle(message);

src/lib/components/ChannelList.svelte

Lines changed: 22 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
<script lang="ts">
2-
import { DragDropProvider } from "@dnd-kit-svelte/svelte";
3-
import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers";
4-
import { move } from "@dnd-kit/helpers";
52
import { onDestroy } from "svelte";
63
import { flip } from "svelte/animate";
74
import { app } from "$lib/app.svelte";
85
import type { Channel } from "$lib/models/channel.svelte";
96
import { settings } from "$lib/settings";
107
import Draggable from "./Draggable.svelte";
8+
import DraggableChannel from "./DraggableChannel.svelte";
119
import Droppable from "./Droppable.svelte";
1210
import { getSidebarContext } from "./Sidebar.svelte";
13-
import StreamTooltip from "./StreamTooltip.svelte";
1411
import { Separator } from "./ui/separator";
1512
1613
const sidebar = getSidebarContext();
@@ -79,38 +76,26 @@
7976
onDestroy(() => clearInterval(interval));
8077
</script>
8178

82-
<DragDropProvider
83-
modifiers={[
84-
// @ts-expect-error - type mismatch
85-
RestrictToVerticalAxis,
86-
]}
87-
onDragOver={(event) => {
88-
settings.state.pinned = move(settings.state.pinned, event);
89-
}}
90-
>
91-
{#each groups as group}
92-
{#if sidebar.collapsed}
93-
<Separator />
94-
{:else}
95-
<span
96-
class="text-muted-foreground mt-2 inline-block px-2 text-xs font-semibold uppercase"
97-
>
98-
{group.type}
99-
</span>
100-
{/if}
79+
{#each groups as group}
80+
{#if sidebar.collapsed}
81+
<Separator />
82+
{:else}
83+
<span class="text-muted-foreground mt-2 inline-block px-2 text-xs font-semibold uppercase">
84+
{group.type}
85+
</span>
86+
{/if}
10187

102-
{#if group.type === "Pinned"}
103-
<Droppable id="pinned-channels" class="space-y-1.5">
104-
{#each group.channels as channel, i (channel.user.id)}
105-
<Draggable id={channel.id} index={i} {channel} />
106-
{/each}
107-
</Droppable>
108-
{:else}
109-
{#each group.channels as channel (channel.user.id)}
110-
<div class="px-1.5" animate:flip={{ duration: 500 }}>
111-
<StreamTooltip {channel} />
112-
</div>
88+
{#if group.type === "Pinned"}
89+
<Droppable id="pinned-channels" class="space-y-1.5">
90+
{#each group.channels as channel, i (channel.user.id)}
91+
<Draggable index={i} {channel} />
11392
{/each}
114-
{/if}
115-
{/each}
116-
</DragDropProvider>
93+
</Droppable>
94+
{:else}
95+
{#each group.channels as channel (channel.user.id)}
96+
<div animate:flip={{ duration: 500 }}>
97+
<DraggableChannel {channel} />
98+
</div>
99+
{/each}
100+
{/if}
101+
{/each}

src/lib/components/Draggable.svelte

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
<script lang="ts">
22
import { useSortable } from "@dnd-kit-svelte/svelte/sortable";
3-
import type { UseSortableInput } from "@dnd-kit-svelte/svelte/sortable";
43
import ChannelListItem from "./ChannelListItem.svelte";
54
import StreamTooltip from "./StreamTooltip.svelte";
65
import type { ChannelListItemProps } from "./ChannelListItem.svelte";
76
8-
const { channel, ...rest }: UseSortableInput & ChannelListItemProps = $props();
7+
interface Props extends ChannelListItemProps {
8+
index: number;
9+
}
910
10-
const { ref, isDragging } = useSortable(rest);
11+
const { channel, index }: Props = $props();
12+
13+
const { ref, isDragging } = useSortable({
14+
id: () => channel.id,
15+
type: "channel-list-item",
16+
index,
17+
});
1118
</script>
1219

1320
<div class="relative px-1.5" {@attach ref}>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script lang="ts">
2+
import { useDraggable } from "@dnd-kit-svelte/svelte";
3+
import type { Channel } from "$lib/models/channel.svelte";
4+
import ChannelListItem from "./ChannelListItem.svelte";
5+
import StreamTooltip from "./StreamTooltip.svelte";
6+
7+
interface Props {
8+
channel: Channel;
9+
}
10+
11+
const { channel }: Props = $props();
12+
13+
const { ref, isDragging } = useDraggable({
14+
id: () => channel.id,
15+
type: "channel-list-item",
16+
});
17+
</script>
18+
19+
<div class="relative px-1.5" {@attach ref}>
20+
<div class={[isDragging.current && "invisible"]}>
21+
<StreamTooltip {channel} />
22+
</div>
23+
24+
{#if isDragging.current}
25+
<div class="absolute inset-1.5 flex items-center gap-2 px-1.5 opacity-70">
26+
<ChannelListItem {channel} />
27+
</div>
28+
{/if}
29+
</div>

src/lib/components/StreamTooltip.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<a
2525
class="absolute inset-0 z-10"
2626
href="/channels/{channel.user.username}"
27+
draggable="false"
2728
data-sveltekit-preload-data="off"
2829
aria-label="Join {channel.user.displayName}"
2930
>

src/lib/components/split/SplitHeader.svelte

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,26 @@
44
import SquareHalf from "~icons/ph/square-half-fill";
55
import X from "~icons/ph/x";
66
import { app } from "$lib/app.svelte";
7-
import type { Channel } from "$lib/models/channel.svelte";
87
import { Button } from "../ui/button";
98
109
interface Props {
11-
channel: Channel;
12-
handleRef: Attachment<Element>;
10+
id: string;
11+
handleRef?: Attachment<Element>;
1312
}
1413
15-
const { channel, handleRef }: Props = $props();
14+
const { id, handleRef }: Props = $props();
15+
16+
const channel = $derived(app.channels.get(id));
1617
</script>
1718

1819
<div class="z-20 flex items-center justify-between border-b p-1">
1920
<div
2021
class="flex h-full flex-1 cursor-grab items-center gap-x-2 overflow-hidden px-1 active:cursor-grabbing"
2122
{@attach handleRef}
2223
>
23-
<span class="truncate text-sm font-medium select-none">{channel.user.displayName}</span>
24+
{#if channel}
25+
<span class="truncate text-sm font-medium select-none">{channel.user.displayName}</span>
26+
{/if}
2427
</div>
2528

2629
<div class="flex shrink-0 items-center gap-x-1">
@@ -29,11 +32,7 @@
2932
size="icon-sm"
3033
variant="ghost"
3134
onclick={() => {
32-
app.splits.insert(channel.id, `Window ${Date.now()}`, {
33-
direction: "horizontal",
34-
first: channel.id,
35-
second: `Window ${Date.now()}`,
36-
});
35+
app.splits.insertEmpty(id, "horizontal");
3736
}}
3837
>
3938
<SquareHalf />
@@ -44,11 +43,7 @@
4443
size="icon-sm"
4544
variant="ghost"
4645
onclick={() => {
47-
app.splits.insert(channel.id, `Window ${Date.now()}`, {
48-
direction: "vertical",
49-
first: channel.id,
50-
second: `Window ${Date.now()}`,
51-
});
46+
app.splits.insertEmpty(id, "vertical");
5247
}}
5348
>
5449
<SquareHalfBottom />
@@ -59,7 +54,7 @@
5954
size="icon-sm"
6055
variant="ghost"
6156
onclick={async () => {
62-
app.splits.remove(channel.id);
57+
app.splits.remove(id);
6358
await channel?.leave();
6459
}}
6560
>

src/lib/components/split/SplitView.svelte

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@
2222
</script>
2323

2424
<div class="relative flex size-full flex-col overflow-hidden" {@attach ref}>
25-
{#if channel}
26-
<SplitHeader {channel} {handleRef} />
27-
{/if}
25+
<SplitHeader {id} {handleRef} />
2826

2927
<div class={["relative flex-1", isDragging.current && "opacity-50"]}>
3028
{#if channel}
@@ -36,10 +34,13 @@
3634
{/if}
3735

3836
{@render dropZone(dropCenter, "inset-0")}
39-
{@render dropZone(dropUp, "top-0 inset-x-0 h-1/3")}
40-
{@render dropZone(dropDown, "bottom-0 inset-x-0 h-1/3")}
41-
{@render dropZone(dropLeft, "left-0 inset-y-0 w-1/3")}
42-
{@render dropZone(dropRight, "right-0 inset-y-0 w-1/3")}
37+
38+
{#if channel}
39+
{@render dropZone(dropUp, "top-0 inset-x-0 h-1/3")}
40+
{@render dropZone(dropDown, "bottom-0 inset-x-0 h-1/3")}
41+
{@render dropZone(dropLeft, "left-0 inset-y-0 w-1/3")}
42+
{@render dropZone(dropRight, "right-0 inset-y-0 w-1/3")}
43+
{/if}
4344
</div>
4445
</div>
4546

@@ -48,7 +49,7 @@
4849
class={[
4950
"pointer-events-none absolute z-10",
5051
className,
51-
dropper.isDropTarget.current && "bg-primary/50",
52+
dropper.isDropTarget.current && "bg-primary/30",
5253
]}
5354
{@attach dropper.ref}
5455
></div>

src/lib/managers/split-manager.svelte.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ export class SplitManager {
4444
});
4545
}
4646

47+
public insertEmpty(target: string, direction: PaneGroupProps["direction"]) {
48+
const id = `split-${crypto.randomUUID()}`;
49+
50+
this.insert(target, id, {
51+
direction,
52+
first: target,
53+
second: id,
54+
});
55+
}
56+
4757
public remove(target: string) {
4858
if (!this.root) return;
4959

@@ -70,35 +80,48 @@ export class SplitManager {
7080
this.root = this.#replace(this.root, target);
7181
}
7282

83+
public replace(target: string, replacement: string) {
84+
if (!this.root || target === replacement) return;
85+
86+
const path = this.#find(this.root, target);
87+
if (!path) return;
88+
89+
this.root = this.#update(this.root, path, (node) => {
90+
if (typeof node === "string") {
91+
return replacement;
92+
}
93+
94+
return node;
95+
});
96+
}
97+
7398
public handleDragEnd(event: Parameters<DragDropEvents["dragend"]>[0]) {
7499
const { source, target } = event.operation;
75100

76101
if (source && target && source.id !== target.id) {
77102
const sourceId = source.id.toString();
78103
const [targetId, position] = target.id.toString().split(":");
79104

80-
if (!position || sourceId === targetId) {
105+
if (sourceId === targetId) return;
106+
107+
this.remove(sourceId);
108+
109+
if (position === "center") {
110+
this.replace(targetId, sourceId);
81111
return;
82112
}
83113

84-
this.remove(source.id.toString());
85-
86114
let direction: PaneGroupProps["direction"] = "horizontal";
87115
let first = targetId;
88116
let second = sourceId;
89117

90118
if (position === "up" || position === "down") {
91119
direction = "vertical";
92-
} else if (position === "left" || position === "right") {
93-
direction = "horizontal";
94120
}
95121

96122
if (position === "up" || position === "left") {
97123
first = sourceId;
98124
second = targetId;
99-
} else {
100-
first = targetId;
101-
second = sourceId;
102125
}
103126

104127
this.insert(targetId, sourceId, { direction, first, second });

src/routes/(main)/+layout.svelte

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
<script lang="ts">
2+
import { DragDropProvider, DragOverlay } from "@dnd-kit-svelte/svelte";
3+
import { move } from "@dnd-kit/helpers";
24
import { goto } from "$app/navigation";
5+
import { app } from "$lib/app.svelte";
36
import Sidebar from "$lib/components/Sidebar.svelte";
7+
import SplitHeader from "$lib/components/split/SplitHeader.svelte";
48
import * as Tooltip from "$lib/components/ui/tooltip";
59
import { settings } from "$lib/settings";
610
@@ -16,14 +20,33 @@
1620
}}
1721
/>
1822

19-
<Tooltip.Provider delayDuration={100}>
20-
<div class="flex grow overflow-hidden">
21-
{#if settings.state.user}
22-
<Sidebar />
23-
{/if}
23+
<DragDropProvider
24+
onDragOver={(event) => {
25+
if (event.operation.target?.id === "pinned-channels") {
26+
settings.state.pinned = move(settings.state.pinned, event);
27+
}
28+
}}
29+
onDragEnd={(event) => {
30+
app.splits.handleDragEnd(event);
31+
}}
32+
>
33+
<Tooltip.Provider delayDuration={100}>
34+
<div class="flex grow overflow-hidden">
35+
{#if settings.state.user}
36+
<Sidebar />
37+
{/if}
38+
39+
<main class={["bg-accent/15 grow overflow-hidden", settings.state.user && "border-l"]}>
40+
{@render children()}
41+
</main>
42+
</div>
43+
</Tooltip.Provider>
2444

25-
<main class={["bg-accent/15 grow overflow-hidden", settings.state.user && "border-l"]}>
26-
{@render children()}
27-
</main>
28-
</div>
29-
</Tooltip.Provider>
45+
<DragOverlay>
46+
{#snippet children(draggable)}
47+
{#if draggable.type !== "channel-list-item"}
48+
<SplitHeader id={draggable.id.toString()} />
49+
{/if}
50+
{/snippet}
51+
</DragOverlay>
52+
</DragDropProvider>

0 commit comments

Comments
 (0)