chatStores.js raw
1 import { writable, derived } from 'svelte/store';
2
3 // ==================== Chat Navigation ====================
4
5 // Active chat sub-tab: "inbox" or "channels"
6 export const activeChatTab = writable(localStorage.getItem("activeChatTab") || "inbox");
7 activeChatTab.subscribe(v => localStorage.setItem("activeChatTab", v));
8
9 // ==================== Inbox (DMs) ====================
10
11 // Map of pubkey -> { messages: [], lastRead: number, unreadCount: number, protocol: "nip04"|"nip17" }
12 export const conversations = writable(new Map());
13
14 // Currently selected conversation partner pubkey (null = no conversation open)
15 export const selectedConversation = writable(null);
16
17 // Loading state for DM fetch
18 export const inboxLoading = writable(false);
19
20 // ==================== Channels (NIP-28) ====================
21
22 // Map of channelId -> { metadata: {}, messages: [], lastRead: number, unreadCount: number, joined: boolean }
23 export const channels = writable(new Map());
24
25 // Set of joined channel IDs (persisted)
26 const storedJoined = localStorage.getItem("joinedChannels");
27 export const joinedChannels = writable(new Set(storedJoined ? JSON.parse(storedJoined) : []));
28 joinedChannels.subscribe(set => localStorage.setItem("joinedChannels", JSON.stringify([...set])));
29
30 // Currently selected channel ID (null = no channel open)
31 export const selectedChannel = writable(null);
32
33 // Channel discovery loading state
34 export const channelsLoading = writable(false);
35
36 // ==================== Derived ====================
37
38 // Total unread DM count
39 export const totalUnreadDMs = derived(conversations, $convs => {
40 let count = 0;
41 for (const conv of $convs.values()) {
42 count += conv.unreadCount || 0;
43 }
44 return count;
45 });
46
47 // Total unread channel messages
48 export const totalUnreadChannels = derived(channels, $chans => {
49 let count = 0;
50 for (const chan of $chans.values()) {
51 if (chan.joined) count += chan.unreadCount || 0;
52 }
53 return count;
54 });
55
56 // ==================== Actions ====================
57
58 /**
59 * Mark a conversation as read
60 * @param {string} pubkey - Conversation partner pubkey
61 */
62 export function markConversationRead(pubkey) {
63 conversations.update(map => {
64 const conv = map.get(pubkey);
65 if (conv) {
66 conv.lastRead = Date.now();
67 conv.unreadCount = 0;
68 map.set(pubkey, conv);
69 }
70 return new Map(map);
71 });
72 }
73
74 /**
75 * Mark a channel as read
76 * @param {string} channelId - Channel ID
77 */
78 export function markChannelRead(channelId) {
79 channels.update(map => {
80 const chan = map.get(channelId);
81 if (chan) {
82 chan.lastRead = Date.now();
83 chan.unreadCount = 0;
84 map.set(channelId, chan);
85 }
86 return new Map(map);
87 });
88 // Persist last-read timestamps
89 localStorage.setItem(`channel-lastread-${channelId}`, Date.now().toString());
90 }
91
92 /**
93 * Join a channel
94 * @param {string} channelId - Channel ID
95 */
96 export function joinChannel(channelId) {
97 joinedChannels.update(set => {
98 set.add(channelId);
99 return new Set(set);
100 });
101 }
102
103 /**
104 * Leave a channel
105 * @param {string} channelId - Channel ID
106 */
107 export function leaveChannel(channelId) {
108 joinedChannels.update(set => {
109 set.delete(channelId);
110 return new Set(set);
111 });
112 selectedChannel.update(sel => sel === channelId ? null : sel);
113 }
114
115 /**
116 * Reset all chat state (on logout)
117 */
118 export function resetChatState() {
119 conversations.set(new Map());
120 selectedConversation.set(null);
121 channels.set(new Map());
122 selectedChannel.set(null);
123 inboxLoading.set(false);
124 channelsLoading.set(false);
125 }
126