@snapie/chat-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,313 @@
1
+ # @snapie/chat-client
2
+
3
+ Hive-authenticated chat SDK for [Snapie](https://snapie.io). Supports DMs, channels, groups, real-time subscriptions (polling + FCM), and typing indicators.
4
+
5
+ Works in any JavaScript/TypeScript environment. React hooks available as a separate entry point.
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @snapie/chat-client
13
+ # or
14
+ pnpm add @snapie/chat-client
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Quick start
20
+
21
+ ```typescript
22
+ import { ChatClient } from '@snapie/chat-client';
23
+
24
+ const client = new ChatClient({ baseUrl: 'https://snapie.io' });
25
+
26
+ // Authenticate with a Hive account (posting key challenge/response)
27
+ await client.authenticate(username, async (challenge) => {
28
+ // Use Hive Keychain, Aioha, or any signing library
29
+ return await keychain.signBuffer(username, challenge, 'Posting');
30
+ });
31
+
32
+ // Get all conversations
33
+ const conversations = await client.getConversations();
34
+
35
+ // Subscribe to live messages (polls every 15s, merges new ones)
36
+ const unsub = client.subscribeToMessages(conversationId, 'dm', (messages) => {
37
+ console.log(messages);
38
+ });
39
+
40
+ // Send a message
41
+ await client.sendMessage(conversationId, 'dm', 'Hello from 3speak!');
42
+
43
+ // Stop subscribing
44
+ unsub();
45
+
46
+ // Clean up everything when done
47
+ client.destroy();
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Configuration
53
+
54
+ ```typescript
55
+ const client = new ChatClient({
56
+ baseUrl: 'https://snapie.io', // required — Snapie instance URL
57
+ pollInterval: 15000, // optional — ms between polls (default: 15000)
58
+ storage: customStorage, // optional — custom StorageAdapter (see below)
59
+ });
60
+ ```
61
+
62
+ ### Custom storage (React Native / Node)
63
+
64
+ By default the client uses `localStorage`. Override it with any object that implements `getItem`, `setItem`, `removeItem`:
65
+
66
+ ```typescript
67
+ import AsyncStorage from '@react-native-async-storage/async-storage';
68
+
69
+ const client = new ChatClient({
70
+ baseUrl: 'https://snapie.io',
71
+ storage: {
72
+ getItem: (key) => AsyncStorage.getItem(key), // note: sync wrapper needed
73
+ setItem: (key, val) => AsyncStorage.setItem(key, val),
74
+ removeItem: (key) => AsyncStorage.removeItem(key),
75
+ },
76
+ });
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Authentication
82
+
83
+ The auth flow uses Hive's posting key signature — no passwords, no OAuth.
84
+
85
+ ```typescript
86
+ // 1. Authenticate
87
+ await client.authenticate(username, async (challenge) => {
88
+ return await signingLibrary.sign(challenge);
89
+ });
90
+
91
+ // 2. Check status
92
+ client.isAuthenticated(); // boolean
93
+ client.getUsername(); // string | null
94
+
95
+ // 3. Logout
96
+ client.logout();
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Conversations
102
+
103
+ ```typescript
104
+ // List all conversations (DMs + channels + groups)
105
+ const conversations = await client.getConversations();
106
+ // Conversation { _id, name, type: 'dm'|'channel'|'group', lastMessage, unread, ... }
107
+
108
+ // Open or resume a DM with another Hive user
109
+ const conv = await client.openDm('alice');
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Messages
115
+
116
+ ```typescript
117
+ // Fetch messages (one-time)
118
+ const { messages } = await client.getMessages(conversationId, 'dm');
119
+ const { messages } = await client.getMessages(channelId, 'channel', { limit: 50 });
120
+
121
+ // Send
122
+ const { message } = await client.sendMessage(conversationId, 'dm', 'Hey!');
123
+
124
+ // Reply
125
+ const { message } = await client.sendMessage(conversationId, 'dm', 'Got it', replyToMessageId);
126
+
127
+ // Edit
128
+ const updated = await client.editMessage(conversationId, 'dm', messageId, 'Edited text');
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Real-time subscriptions
134
+
135
+ All `subscribe*` methods return an **unsubscribe function**. They fire immediately with current data, then refresh on every poll tick.
136
+
137
+ ```typescript
138
+ // Messages
139
+ const unsub = client.subscribeToMessages(conv._id, conv.type, (messages) => {
140
+ setMessages(messages); // always the full ordered list
141
+ });
142
+
143
+ // Conversations list
144
+ const unsub = client.subscribeToConversations((conversations) => {
145
+ setConversations(conversations);
146
+ });
147
+
148
+ // Unread badge count
149
+ const unsub = client.subscribeToUnreadCount((count) => {
150
+ setBadge(count);
151
+ });
152
+
153
+ // Stop any subscription
154
+ unsub();
155
+ ```
156
+
157
+ ### FCM foreground integration
158
+
159
+ When a Firebase Cloud Messaging push arrives while the app is open, trigger an immediate refresh instead of waiting for the next poll tick:
160
+
161
+ ```typescript
162
+ import { onMessage } from 'firebase/messaging';
163
+
164
+ onMessage(messaging, () => {
165
+ client.onForegroundPush();
166
+ });
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Channels & groups
172
+
173
+ ```typescript
174
+ // List public channels
175
+ const channels = await client.getChannels();
176
+
177
+ // Join / leave
178
+ await client.joinChannel(channelId);
179
+ await client.leaveChannel(channelId);
180
+
181
+ // Create a group
182
+ const group = await client.createGroup({
183
+ name: 'My Group',
184
+ description: 'Optional',
185
+ isPublic: false,
186
+ members: ['alice', 'bob'],
187
+ });
188
+
189
+ await client.addGroupMember(group._id, 'charlie');
190
+ await client.removeGroupMember(group._id, 'charlie');
191
+ ```
192
+
193
+ ---
194
+
195
+ ## Typing indicators
196
+
197
+ ```typescript
198
+ // Signal that the current user is typing
199
+ await client.setTyping(conversationId, true);
200
+ await client.setTyping(conversationId, false);
201
+
202
+ // Poll who else is typing
203
+ const { users } = await client.getTyping(conversationId);
204
+ // users: string[] — list of usernames currently typing
205
+ ```
206
+
207
+ ---
208
+
209
+ ## Preferences
210
+
211
+ ```typescript
212
+ await client.muteUser('spammer');
213
+ await client.unmuteUser('spammer');
214
+ await client.blockUser('troll');
215
+ await client.unblockUser('troll');
216
+
217
+ const prefs = await client.getPreferences();
218
+ // { mutedUsers: string[], blockedUsers: string[] }
219
+ ```
220
+
221
+ ---
222
+
223
+ ## Push notifications (FCM)
224
+
225
+ ```typescript
226
+ // Register a device token so this user receives push notifications
227
+ await client.registerDevice(fcmToken);
228
+ ```
229
+
230
+ ---
231
+
232
+ ## React hooks
233
+
234
+ ```tsx
235
+ import { ChatProvider, useConversations, useChatMessages, useUnreadCount, useTyping } from '@snapie/chat-client/react';
236
+
237
+ // Wrap once at the top level
238
+ <ChatProvider client={client}>
239
+ <App />
240
+ </ChatProvider>
241
+ ```
242
+
243
+ ### `useConversations`
244
+
245
+ ```tsx
246
+ function ConversationList() {
247
+ const { conversations, loading } = useConversations();
248
+ if (loading) return <Spinner />;
249
+ return conversations.map(conv => <ConvRow key={conv._id} conv={conv} />);
250
+ }
251
+ ```
252
+
253
+ ### `useChatMessages`
254
+
255
+ ```tsx
256
+ function MessageView({ conv }) {
257
+ const { messages, loading, sendMessage, editMessage } = useChatMessages(conv._id, conv.type);
258
+
259
+ return (
260
+ <>
261
+ {messages.map(m => <Bubble key={m._id} message={m} />)}
262
+ <Input onSend={sendMessage} />
263
+ </>
264
+ );
265
+ }
266
+ ```
267
+
268
+ ### `useUnreadCount`
269
+
270
+ ```tsx
271
+ function ChatButton() {
272
+ const { unreadCount } = useUnreadCount();
273
+ return <Button badge={unreadCount}>Chat</Button>;
274
+ }
275
+ ```
276
+
277
+ ### `useTyping`
278
+
279
+ ```tsx
280
+ function TypingIndicator({ convId }) {
281
+ const { typingUsers, setTyping } = useTyping(convId);
282
+ // setTyping(true) when user starts typing — auto-clears after 5s
283
+ return typingUsers.length > 0 ? <Text>{typingUsers.join(', ')} is typing...</Text> : null;
284
+ }
285
+ ```
286
+
287
+ ---
288
+
289
+ ## TypeScript types
290
+
291
+ All types are exported from the main entry point:
292
+
293
+ ```typescript
294
+ import type {
295
+ ChatClient,
296
+ ChatClientOptions,
297
+ Conversation,
298
+ Message,
299
+ Channel,
300
+ MessagesResult,
301
+ DmDeliveryInfo,
302
+ DmStatusInfo,
303
+ TypingStatusInfo,
304
+ ChatPreferences,
305
+ StorageAdapter,
306
+ } from '@snapie/chat-client';
307
+ ```
308
+
309
+ ---
310
+
311
+ ## Backend
312
+
313
+ The SDK communicates with the Snapie chat API at `{baseUrl}/api/chat/*`. The backend is hosted at `https://snapie.io`. Auth tokens are JWTs issued after Hive posting-key signature verification and are stored in the configured storage adapter.
@@ -0,0 +1,220 @@
1
+ interface Channel {
2
+ _id: string;
3
+ name: string;
4
+ description?: string;
5
+ type: string;
6
+ conversationKind?: 'channel' | 'group';
7
+ owner?: string;
8
+ members?: string[];
9
+ memberCount: number;
10
+ isPublic: boolean;
11
+ }
12
+ interface Message {
13
+ _id: string;
14
+ sender: string;
15
+ content: string;
16
+ replyTo?: string | null;
17
+ editedAt?: string | null;
18
+ createdAt: string;
19
+ }
20
+ interface Conversation {
21
+ _id: string;
22
+ name: string;
23
+ description?: string;
24
+ type: 'channel' | 'group' | 'dm';
25
+ isPublic: boolean;
26
+ owner?: string;
27
+ members?: string[];
28
+ memberCount?: number;
29
+ peer?: string;
30
+ lastMessage?: Message | null;
31
+ unread?: boolean;
32
+ }
33
+ interface DmDeliveryInfo {
34
+ hasFcm: boolean;
35
+ memoSuggested: boolean;
36
+ cooldownMs: number;
37
+ }
38
+ interface DmStatusInfo {
39
+ meSeenAt: string;
40
+ peerSeenAt: string | null;
41
+ peerLastSeenAt: string | null;
42
+ peerOnline: boolean;
43
+ }
44
+ interface TypingStatusInfo {
45
+ users: string[];
46
+ ttlMs: number;
47
+ }
48
+ interface ChatPreferences {
49
+ mutedUsers: string[];
50
+ blockedUsers: string[];
51
+ }
52
+ interface MessagesResult {
53
+ messages: Message[];
54
+ status?: DmStatusInfo | null;
55
+ }
56
+ interface StorageAdapter {
57
+ getItem(key: string): string | null;
58
+ setItem(key: string, value: string): void;
59
+ removeItem(key: string): void;
60
+ }
61
+ interface ChatClientOptions {
62
+ /** Base URL of the Snapie instance, e.g. "https://snapie.io" */
63
+ baseUrl: string;
64
+ /** Override default localStorage — useful for React Native or Node */
65
+ storage?: StorageAdapter;
66
+ /** How often to poll for new messages when FCM is not available (ms, default 15000) */
67
+ pollInterval?: number;
68
+ }
69
+
70
+ declare class ChatService {
71
+ private token;
72
+ private tokenUsername;
73
+ private base;
74
+ private storage;
75
+ constructor(baseUrl: string, storage: StorageAdapter);
76
+ isAuthenticated(): boolean;
77
+ getTokenUsername(): string | null;
78
+ authenticate(username: string, signMessage: (msg: string) => Promise<string>): Promise<void>;
79
+ logout(): void;
80
+ getChannels(): Promise<Channel[]>;
81
+ getConversations(): Promise<Conversation[]>;
82
+ getChannelMessages(channelId: string, opts?: {
83
+ before?: string;
84
+ after?: string;
85
+ limit?: number;
86
+ }): Promise<Message[]>;
87
+ sendChannelMessage(channelId: string, content: string, replyTo?: string): Promise<Message>;
88
+ editChannelMessage(channelId: string, messageId: string, content: string): Promise<Message>;
89
+ joinChannel(channelId: string): Promise<void>;
90
+ leaveChannel(channelId: string): Promise<void>;
91
+ getDmMessages(conversationId: string, opts?: {
92
+ before?: string;
93
+ after?: string;
94
+ limit?: number;
95
+ }): Promise<MessagesResult>;
96
+ sendDmMessage(conversationId: string, content: string, replyTo?: string): Promise<{
97
+ message: Message;
98
+ delivery?: DmDeliveryInfo;
99
+ }>;
100
+ editDmMessage(conversationId: string, messageId: string, content: string): Promise<Message>;
101
+ openDm(targetUser: string): Promise<Conversation>;
102
+ createGroup(payload: {
103
+ name: string;
104
+ description?: string;
105
+ isPublic?: boolean;
106
+ members?: string[];
107
+ }): Promise<Channel>;
108
+ getGroups(): Promise<Channel[]>;
109
+ addGroupMember(groupId: string, member: string): Promise<Channel>;
110
+ removeGroupMember(groupId: string, member: string): Promise<Channel>;
111
+ getUnreadCount(): Promise<number>;
112
+ setTyping(conversationId: string, isTyping: boolean): Promise<void>;
113
+ getTyping(conversationId: string): Promise<TypingStatusInfo>;
114
+ getPreferences(): Promise<ChatPreferences>;
115
+ muteUser(username: string): Promise<void>;
116
+ unmuteUser(username: string): Promise<void>;
117
+ blockUser(username: string): Promise<void>;
118
+ unblockUser(username: string): Promise<void>;
119
+ registerDevice(fcmToken: string): Promise<void>;
120
+ markDmMemoFallbackSent(conversationId: string): Promise<void>;
121
+ private buildQS;
122
+ private get;
123
+ private post;
124
+ request<T>(url: string, opts: RequestInit, auth: boolean): Promise<T>;
125
+ }
126
+
127
+ type ConversationsCallback = (conversations: Conversation[]) => void;
128
+ type MessagesCallback = (messages: Message[]) => void;
129
+ type UnreadCallback = (count: number) => void;
130
+ /**
131
+ * High-level Snapie chat client.
132
+ *
133
+ * Usage:
134
+ * const client = new ChatClient({ baseUrl: 'https://snapie.io' });
135
+ * await client.authenticate(username, msg => keychain.sign(msg));
136
+ * const conversations = await client.getConversations();
137
+ * const unsub = client.subscribeToMessages(convId, 'dm', msgs => setMessages(msgs));
138
+ */
139
+ declare class ChatClient {
140
+ readonly service: ChatService;
141
+ private poller;
142
+ /** Cache of messages per conversationId — avoids re-rendering unchanged data */
143
+ private messageCache;
144
+ constructor(options: ChatClientOptions);
145
+ isAuthenticated(): boolean;
146
+ getUsername(): string | null;
147
+ /**
148
+ * Authenticate with a Hive account.
149
+ * `signMessage` should call Hive Keychain or equivalent with the posting key.
150
+ */
151
+ authenticate(username: string, signMessage: (challenge: string) => Promise<string>): Promise<void>;
152
+ logout(): void;
153
+ getConversations(): Promise<Conversation[]>;
154
+ openDm(targetUser: string): Promise<Conversation>;
155
+ getChannels(): Promise<Channel[]>;
156
+ getGroups(): Promise<Channel[]>;
157
+ joinChannel(channelId: string): Promise<void>;
158
+ leaveChannel(channelId: string): Promise<void>;
159
+ createGroup(payload: {
160
+ name: string;
161
+ description?: string;
162
+ isPublic?: boolean;
163
+ members?: string[];
164
+ }): Promise<Channel>;
165
+ addGroupMember(groupId: string, member: string): Promise<Channel>;
166
+ removeGroupMember(groupId: string, member: string): Promise<Channel>;
167
+ getMessages(conversationId: string, type: 'channel' | 'dm' | 'group', opts?: {
168
+ before?: string;
169
+ after?: string;
170
+ limit?: number;
171
+ }): Promise<MessagesResult>;
172
+ sendMessage(conversationId: string, type: 'channel' | 'dm' | 'group', content: string, replyTo?: string): Promise<{
173
+ message: Message;
174
+ delivery?: DmDeliveryInfo;
175
+ }>;
176
+ editMessage(conversationId: string, type: 'channel' | 'dm' | 'group', messageId: string, content: string): Promise<Message>;
177
+ /**
178
+ * Subscribe to live updates for a conversation's message list.
179
+ * Calls `callback` immediately with the current messages, then again on every poll tick.
180
+ * Returns an unsubscribe function.
181
+ *
182
+ * @example
183
+ * const unsub = client.subscribeToMessages(conv._id, conv.type, msgs => setMessages(msgs));
184
+ * // later:
185
+ * unsub();
186
+ */
187
+ subscribeToMessages(conversationId: string, type: 'channel' | 'dm' | 'group', callback: MessagesCallback): () => void;
188
+ /**
189
+ * Subscribe to the full conversations list, refreshed on every poll tick.
190
+ * Returns an unsubscribe function.
191
+ */
192
+ subscribeToConversations(callback: ConversationsCallback): () => void;
193
+ /**
194
+ * Subscribe to the unread message count, refreshed on every poll tick.
195
+ * Returns an unsubscribe function.
196
+ */
197
+ subscribeToUnreadCount(callback: UnreadCallback): () => void;
198
+ /**
199
+ * Notify the client of an incoming FCM foreground message.
200
+ * Call this from your FCM `onMessage` handler to trigger an immediate refresh
201
+ * rather than waiting for the next poll tick.
202
+ *
203
+ * @example
204
+ * onMessage(messaging, () => client.onForegroundPush());
205
+ */
206
+ onForegroundPush(): void;
207
+ setTyping(conversationId: string, isTyping: boolean): Promise<void>;
208
+ getTyping(conversationId: string): Promise<TypingStatusInfo>;
209
+ getUnreadCount(): Promise<number>;
210
+ getPreferences(): Promise<ChatPreferences>;
211
+ muteUser(username: string): Promise<void>;
212
+ unmuteUser(username: string): Promise<void>;
213
+ blockUser(username: string): Promise<void>;
214
+ unblockUser(username: string): Promise<void>;
215
+ registerDevice(fcmToken: string): Promise<void>;
216
+ markDmMemoFallbackSent(conversationId: string): Promise<void>;
217
+ destroy(): void;
218
+ }
219
+
220
+ export { ChatClient as C, type DmDeliveryInfo as D, type Message as M, type StorageAdapter as S, type TypingStatusInfo as T, ChatService as a, type ChatClientOptions as b, type Channel as c, type Conversation as d, type MessagesResult as e, type DmStatusInfo as f, type ChatPreferences as g };