Skip to content

Commit 8f3410b

Browse files
aster-voidclaude
andcommitted
treewide: simplify useWebSocket API and fix channel button
- useWebSocket now uses $effect internally for auto-cleanup - API simplified to useWebSocket(eventType, handler) - Fix CreateChannelButton offset by using icon button 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 4942dc1 commit 8f3410b

File tree

5 files changed

+39
-114
lines changed

5 files changed

+39
-114
lines changed

apps/desktop/src/components/channels/ChannelList.svelte

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@
2929
const unreadManager = $derived(new UnreadManager(api, organizationId));
3030
let showUserSearch = $state(false);
3131
32-
// WebSocket: refresh unread counts on new messages (auto-cleanup)
33-
const ws = useWebSocket();
34-
ws.on("message:created", () => {
32+
// WebSocket: refresh unread counts on new messages
33+
useWebSocket("message:created", () => {
3534
unreadManager.fetchUnreadCounts();
3635
});
3736

apps/desktop/src/components/channels/CreateChannelButton.svelte

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import { getApiClient, unwrapResponse } from "@/lib/api.svelte";
33
import Modal, { ModalManager } from "@/lib/modal/modal.svelte";
4+
import MdiPlus from "~icons/mdi/plus";
45
56
const api = getApiClient();
67
@@ -40,12 +41,13 @@
4041
<Modal manager={modalManager} />
4142

4243
<button
43-
class="btn btn-primary btn-sm mt-2 w-full"
44+
class="btn btn-ghost btn-xs btn-square"
45+
title="新しいチャンネル"
4446
onclick={() => {
4547
modalManager.dispatch(createChannelModalContent);
4648
}}
4749
>
48-
+ 新しいチャンネル
50+
<MdiPlus class="text-muted h-4 w-4" />
4951
</button>
5052

5153
{#snippet createChannelModalContent()}

apps/desktop/src/components/chat/PinnedMessages.svelte

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
import type { Message } from "@apps/api-client";
33
import MdiPin from "@/icons/mdi-pin.svelte";
44
import { getApiClient, unwrapResponse, useQuery } from "@/lib/api.svelte";
5-
import type { WsEvent } from "@/lib/websocket";
6-
import { getWebSocket } from "@/lib/websocket";
5+
import { useWebSocket } from "@/lib/websocket";
76
87
interface Props {
98
channelId: string;
@@ -12,7 +11,6 @@
1211
let { channelId }: Props = $props();
1312
1413
const api = getApiClient();
15-
const ws = getWebSocket();
1614
1715
const pinnedMessages = useQuery<Message[]>(async () => {
1816
const response = await api.messages.pins.get({
@@ -29,35 +27,23 @@
2927
}
3028
});
3129
32-
$effect(() => {
33-
const handleMessageUpdate = (event: WsEvent) => {
34-
if (event.type !== "message:updated" || event.channelId !== channelId) {
35-
return;
36-
}
30+
useWebSocket("message:updated", (event) => {
31+
if (event.channelId !== channelId) return;
3732
38-
const updatedMessage = event.message as Message;
39-
const existingIndex = messages.findIndex(
40-
(m) => m.id === updatedMessage.id,
41-
);
33+
const updatedMessage = event.message as Message;
34+
const existingIndex = messages.findIndex((m) => m.id === updatedMessage.id);
4235
43-
if (updatedMessage.pinnedAt) {
44-
if (existingIndex !== -1) {
45-
messages[existingIndex] = updatedMessage;
46-
} else {
47-
messages = [...messages, updatedMessage];
48-
}
36+
if (updatedMessage.pinnedAt) {
37+
if (existingIndex !== -1) {
38+
messages[existingIndex] = updatedMessage;
4939
} else {
50-
if (existingIndex !== -1) {
51-
messages = messages.filter((m) => m.id !== updatedMessage.id);
52-
}
40+
messages = [...messages, updatedMessage];
5341
}
54-
};
55-
56-
ws.on("message:updated", handleMessageUpdate);
57-
58-
return () => {
59-
ws.off("message:updated", handleMessageUpdate);
60-
};
42+
} else {
43+
if (existingIndex !== -1) {
44+
messages = messages.filter((m) => m.id !== updatedMessage.id);
45+
}
46+
}
6147
});
6248
6349
function formatTime(timestamp: Date | number | string) {

apps/desktop/src/components/chat/ReactionButtons.svelte

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
let { messageId }: Props = $props();
1818
1919
const api = getApiClient();
20-
const ws = useWebSocket();
2120
2221
const reactions = useQuery<Reaction[]>(async () => {
2322
const response = await getMessage(api, messageId).reactions.get();
@@ -38,8 +37,8 @@
3837
}
3938
});
4039
41-
// WebSocket subscriptions (auto-cleanup via useWebSocket)
42-
ws.on("reaction:added", (event) => {
40+
// WebSocket subscriptions (auto-cleanup via $effect)
41+
useWebSocket("reaction:added", (event) => {
4342
if (event.messageId === messageId) {
4443
const newReaction = event.reaction as Reaction;
4544
if (!reactionsData.some((r) => r.id === newReaction.id)) {
@@ -48,7 +47,7 @@
4847
}
4948
});
5049
51-
ws.on("reaction:removed", (event) => {
50+
useWebSocket("reaction:removed", (event) => {
5251
if (event.messageId === messageId) {
5352
reactionsData = reactionsData.filter(
5453
(r) => !(r.emoji === event.emoji && r.userId === event.userId),
Lines changed: 16 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { onDestroy } from "svelte";
21
import { getWebSocket } from "./index.ts";
32
import type { WsEvent } from "./types.ts";
43

@@ -7,99 +6,39 @@ type EventHandler<T extends EventType> = (
76
event: Extract<WsEvent, { type: T }>,
87
) => void;
98

10-
interface Subscription {
11-
type: string;
12-
handler: (event: WsEvent) => void;
13-
}
14-
159
/**
16-
* Reactive WebSocket hook with automatic cleanup.
17-
* Handles event subscriptions and channel management.
10+
* Subscribe to a WebSocket event with automatic cleanup via $effect.
1811
*
1912
* @example
2013
* ```ts
21-
* const ws = useWebSocket();
22-
*
23-
* // Subscribe to events (auto-cleanup on destroy)
24-
* ws.on("message:created", (event) => {
14+
* useWebSocket("message:created", (event) => {
2515
* console.log("New message:", event.message);
2616
* });
27-
*
28-
* // Subscribe to a channel
29-
* ws.subscribe(channelId);
3017
* ```
3118
*/
32-
export function useWebSocket() {
33-
const subscriptions: Subscription[] = [];
34-
const subscribedChannels: string[] = [];
35-
19+
export function useWebSocket<T extends EventType>(
20+
type: T,
21+
handler: EventHandler<T>,
22+
): void {
3623
let ws: ReturnType<typeof getWebSocket> | null = null;
3724

3825
try {
3926
ws = getWebSocket();
4027
} catch {
4128
console.warn("WebSocket not initialized");
29+
return;
4230
}
4331

44-
/**
45-
* Subscribe to a WebSocket event with automatic cleanup.
46-
*/
47-
function on<T extends EventType>(type: T, handler: EventHandler<T>) {
48-
if (!ws) return;
49-
50-
const wrappedHandler = (event: WsEvent) => {
51-
if (event.type === type) {
52-
handler(event as Extract<WsEvent, { type: T }>);
53-
}
54-
};
55-
56-
ws.on(type, wrappedHandler);
57-
subscriptions.push({ type, handler: wrappedHandler });
58-
}
59-
60-
/**
61-
* Subscribe to a channel.
62-
*/
63-
function subscribe(channelId: string) {
64-
if (!ws) return;
65-
ws.subscribe(channelId);
66-
subscribedChannels.push(channelId);
67-
}
68-
69-
/**
70-
* Unsubscribe from a channel.
71-
*/
72-
function unsubscribe(channelId: string) {
73-
if (!ws) return;
74-
ws.unsubscribe(channelId);
75-
const index = subscribedChannels.indexOf(channelId);
76-
if (index !== -1) subscribedChannels.splice(index, 1);
77-
}
78-
79-
// Cleanup on component destroy
80-
onDestroy(() => {
81-
if (!ws) return;
82-
83-
// Remove all event listeners
84-
for (const { type, handler } of subscriptions) {
85-
ws.off(type, handler);
32+
const wrappedHandler = (event: WsEvent) => {
33+
if (event.type === type) {
34+
handler(event as Extract<WsEvent, { type: T }>);
8635
}
36+
};
8737

88-
// Unsubscribe from all channels
89-
for (const channelId of subscribedChannels) {
90-
ws.unsubscribe(channelId);
91-
}
38+
$effect(() => {
39+
ws?.on(type, wrappedHandler);
40+
return () => {
41+
ws?.off(type, wrappedHandler);
42+
};
9243
});
93-
94-
return {
95-
on,
96-
subscribe,
97-
unsubscribe,
98-
get connected() {
99-
return ws?.status === "connected";
100-
},
101-
get status() {
102-
return ws?.status ?? "disconnected";
103-
},
104-
};
10544
}

0 commit comments

Comments
 (0)