@proveanything/smartlinks 1.2.4 → 1.3.2
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 +85 -1
- package/{API_SUMMARY.md → dist/API_SUMMARY.md} +225 -1
- package/dist/README.md +569 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +2 -0
- package/dist/api/realtime.d.ts +103 -0
- package/dist/api/realtime.js +113 -0
- package/dist/docs/API_SUMMARY.md +3230 -0
- package/dist/docs/i18n.md +287 -0
- package/dist/docs/liquid-templates.md +484 -0
- package/dist/docs/realtime.md +764 -0
- package/dist/docs/theme-defaults.md +100 -0
- package/dist/docs/theme.system.md +338 -0
- package/dist/docs/widgets.md +510 -0
- package/dist/http.js +132 -19
- package/dist/i18n.md +287 -0
- package/dist/liquid-templates.md +484 -0
- package/dist/realtime.md +764 -0
- package/dist/theme-defaults.md +100 -0
- package/dist/theme.system.md +338 -0
- package/dist/types/error.d.ts +69 -3
- package/dist/types/error.js +98 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/realtime.d.ts +44 -0
- package/dist/types/realtime.js +2 -0
- package/dist/widgets.md +510 -0
- package/docs/API_SUMMARY.md +3230 -0
- package/docs/i18n.md +287 -0
- package/docs/liquid-templates.md +484 -0
- package/docs/realtime.md +764 -0
- package/docs/theme-defaults.md +100 -0
- package/docs/theme.system.md +338 -0
- package/docs/widgets.md +510 -0
- package/package.json +4 -4
- package/dist/api/actions.d.ts +0 -32
- package/dist/api/actions.js +0 -99
- package/dist/build-docs.js +0 -61
- package/dist/types/actions.d.ts +0 -123
- package/dist/types/actions.js +0 -2
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
# Real-Time Messaging with Ably
|
|
2
|
+
|
|
3
|
+
This guide covers adding Ably real-time messaging to SmartLinks apps that need live updates, chat, or presence features.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Ably provides real-time pub/sub messaging over WebSockets. It's ideal for:
|
|
10
|
+
|
|
11
|
+
- **Live chat** between users
|
|
12
|
+
- **Real-time vote/poll results** that update instantly
|
|
13
|
+
- **Presence indicators** showing who's online
|
|
14
|
+
- **Live activity feeds** and notifications
|
|
15
|
+
- **Collaborative features** without polling
|
|
16
|
+
|
|
17
|
+
### Why It's Not in the Base Template
|
|
18
|
+
|
|
19
|
+
Ably is intentionally excluded from the base template because:
|
|
20
|
+
|
|
21
|
+
1. **Bundle size**: Ably SDK adds ~50KB+ to the bundle
|
|
22
|
+
2. **Connection overhead**: Initializing Ably opens a WebSocket connection
|
|
23
|
+
3. **Cost**: Ably charges per connection/message
|
|
24
|
+
4. **Not always needed**: Most apps (pamphlets, manuals, warranties) don't need real-time
|
|
25
|
+
|
|
26
|
+
Apps that need real-time features should add Ably on-demand.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
Add Ably to your app:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install ably
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This adds the Ably SDK with React hooks support.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
### 1. Create the Ably Client
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// src/lib/ably.ts
|
|
48
|
+
import Ably from 'ably';
|
|
49
|
+
|
|
50
|
+
export const createAblyClient = (apiKey: string) => {
|
|
51
|
+
return new Ably.Realtime({ key: apiKey });
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. Add the Provider
|
|
56
|
+
|
|
57
|
+
Wrap your app with the Ably provider:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// In PublicApp.tsx or AdminApp.tsx
|
|
61
|
+
import { AblyProvider } from 'ably/react';
|
|
62
|
+
import { createAblyClient } from '@/lib/ably';
|
|
63
|
+
|
|
64
|
+
const ablyClient = createAblyClient(import.meta.env.VITE_ABLY_API_KEY);
|
|
65
|
+
|
|
66
|
+
function App() {
|
|
67
|
+
return (
|
|
68
|
+
<AblyProvider client={ablyClient}>
|
|
69
|
+
<YourApp />
|
|
70
|
+
</AblyProvider>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. Subscribe to a Channel
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { useChannel } from 'ably/react';
|
|
79
|
+
import { useState } from 'react';
|
|
80
|
+
|
|
81
|
+
export const LiveUpdates = ({ collectionId }: { collectionId: string }) => {
|
|
82
|
+
const [updates, setUpdates] = useState<string[]>([]);
|
|
83
|
+
|
|
84
|
+
const { channel } = useChannel(
|
|
85
|
+
`collection:${collectionId}:updates`,
|
|
86
|
+
(message) => {
|
|
87
|
+
setUpdates(prev => [...prev, message.data.text]);
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const sendUpdate = (text: string) => {
|
|
92
|
+
channel.publish('update', { text });
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div>
|
|
97
|
+
{updates.map((update, i) => (
|
|
98
|
+
<p key={i}>{update}</p>
|
|
99
|
+
))}
|
|
100
|
+
<button onClick={() => sendUpdate('Hello!')}>
|
|
101
|
+
Send Update
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Setup Patterns
|
|
111
|
+
|
|
112
|
+
### Option A: Direct API Key (Development)
|
|
113
|
+
|
|
114
|
+
For development or simple apps where the API key can be in environment variables:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// src/lib/ably.ts
|
|
118
|
+
import Ably from 'ably';
|
|
119
|
+
|
|
120
|
+
export const createAblyClient = () => {
|
|
121
|
+
const apiKey = import.meta.env.VITE_ABLY_API_KEY;
|
|
122
|
+
|
|
123
|
+
if (!apiKey) {
|
|
124
|
+
console.warn('VITE_ABLY_API_KEY not set - real-time features disabled');
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return new Ably.Realtime({ key: apiKey });
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Add to your `.env`:
|
|
133
|
+
|
|
134
|
+
```env
|
|
135
|
+
VITE_ABLY_API_KEY=your-ably-api-key
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Option B: Token Auth (Production) - SmartLinks SDK
|
|
139
|
+
|
|
140
|
+
For production apps, use the SmartLinks SDK's built-in token authentication:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// src/lib/ably.ts
|
|
144
|
+
import Ably from 'ably';
|
|
145
|
+
import * as SL from '@proveanything/smartlinks';
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Create an Ably client using SmartLinks token authentication.
|
|
149
|
+
* This is the recommended approach for production apps.
|
|
150
|
+
*/
|
|
151
|
+
export const createAblyClientWithSmartLinks = (collectionId: string, appId?: string) => {
|
|
152
|
+
return new Ably.Realtime({
|
|
153
|
+
authCallback: async (tokenParams, callback) => {
|
|
154
|
+
try {
|
|
155
|
+
// Use the SmartLinks SDK to get a scoped token
|
|
156
|
+
const tokenRequest = await SL.realtime.getPublicToken({
|
|
157
|
+
collectionId,
|
|
158
|
+
appId,
|
|
159
|
+
});
|
|
160
|
+
callback(null, tokenRequest);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
callback(error as Error, null);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Create an Ably client for admin real-time features.
|
|
170
|
+
* Provides subscribe-only access to interaction channels.
|
|
171
|
+
*/
|
|
172
|
+
export const createAblyClientForAdmin = () => {
|
|
173
|
+
return new Ably.Realtime({
|
|
174
|
+
authCallback: async (tokenParams, callback) => {
|
|
175
|
+
try {
|
|
176
|
+
const tokenRequest = await SL.realtime.getAdminToken();
|
|
177
|
+
callback(null, tokenRequest);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
callback(error as Error, null);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### SmartLinks SDK Real-Time Functions
|
|
187
|
+
|
|
188
|
+
The SDK provides two token endpoints:
|
|
189
|
+
|
|
190
|
+
| Function | Use Case | Parameters |
|
|
191
|
+
|----------|----------|------------|
|
|
192
|
+
| `SL.realtime.getPublicToken()` | User-scoped real-time (chat, votes, presence) | `collectionId` (required), `appId` (optional) |
|
|
193
|
+
| `SL.realtime.getAdminToken()` | Admin real-time (interaction monitoring) | None |
|
|
194
|
+
|
|
195
|
+
Both require the user to be authenticated via the parent SmartLinks platform.
|
|
196
|
+
|
|
197
|
+
Token auth benefits:
|
|
198
|
+
- API key never exposed to client
|
|
199
|
+
- Tokens can have capability restrictions
|
|
200
|
+
- Tokens expire automatically
|
|
201
|
+
|
|
202
|
+
### Conditional Provider Setup
|
|
203
|
+
|
|
204
|
+
Handle cases where Ably isn't configured:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// src/providers/AblyProvider.tsx
|
|
208
|
+
import { AblyProvider as AblyReactProvider } from 'ably/react';
|
|
209
|
+
import { createAblyClient } from '@/lib/ably';
|
|
210
|
+
import { ReactNode, useMemo } from 'react';
|
|
211
|
+
|
|
212
|
+
interface Props {
|
|
213
|
+
children: ReactNode;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export const AblyProvider = ({ children }: Props) => {
|
|
217
|
+
const client = useMemo(() => createAblyClient(), []);
|
|
218
|
+
|
|
219
|
+
// If no client (no API key), render children without provider
|
|
220
|
+
if (!client) {
|
|
221
|
+
return <>{children}</>;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<AblyReactProvider client={client}>
|
|
226
|
+
{children}
|
|
227
|
+
</AblyReactProvider>
|
|
228
|
+
);
|
|
229
|
+
};
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Common Patterns
|
|
235
|
+
|
|
236
|
+
### Subscribing to a Channel
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import { useChannel } from 'ably/react';
|
|
240
|
+
|
|
241
|
+
const MyComponent = () => {
|
|
242
|
+
const { channel } = useChannel('my-channel', (message) => {
|
|
243
|
+
console.log('Received:', message.name, message.data);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return <div>Listening...</div>;
|
|
247
|
+
};
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Publishing Messages
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
const { channel } = useChannel('my-channel', handleMessage);
|
|
254
|
+
|
|
255
|
+
const sendMessage = (text: string) => {
|
|
256
|
+
channel.publish('message', {
|
|
257
|
+
text,
|
|
258
|
+
timestamp: Date.now(),
|
|
259
|
+
sender: userId
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Presence (Who's Online)
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
import { usePresence } from 'ably/react';
|
|
268
|
+
|
|
269
|
+
const OnlineUsers = ({ roomId }: { roomId: string }) => {
|
|
270
|
+
const { presenceData, updateStatus } = usePresence(roomId, {
|
|
271
|
+
id: currentUser.id,
|
|
272
|
+
name: currentUser.name,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<div>
|
|
277
|
+
<h3>Online ({presenceData.length})</h3>
|
|
278
|
+
<ul>
|
|
279
|
+
{presenceData.map((member) => (
|
|
280
|
+
<li key={member.clientId}>{member.data.name}</li>
|
|
281
|
+
))}
|
|
282
|
+
</ul>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
};
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Connection State
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
import { useConnectionStateListener } from 'ably/react';
|
|
292
|
+
|
|
293
|
+
const ConnectionStatus = () => {
|
|
294
|
+
const [status, setStatus] = useState('connecting');
|
|
295
|
+
|
|
296
|
+
useConnectionStateListener((stateChange) => {
|
|
297
|
+
setStatus(stateChange.current);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<div className={status === 'connected' ? 'text-green-500' : 'text-yellow-500'}>
|
|
302
|
+
{status}
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
};
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## SmartLinks Integration
|
|
311
|
+
|
|
312
|
+
### Channel Naming Conventions
|
|
313
|
+
|
|
314
|
+
Use consistent channel names that include SmartLinks context:
|
|
315
|
+
|
|
316
|
+
| Scope | Pattern | Example |
|
|
317
|
+
|-------|---------|---------|
|
|
318
|
+
| Collection | `collection:${collectionId}:${feature}` | `collection:abc123:updates` |
|
|
319
|
+
| Product | `product:${collectionId}:${productId}:${feature}` | `product:abc123:prod456:chat` |
|
|
320
|
+
| Proof | `proof:${collectionId}:${productId}:${proofId}:${feature}` | `proof:abc123:prod456:prf789:activity` |
|
|
321
|
+
|
|
322
|
+
### Helper for Channel Names
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// src/lib/ably-channels.ts
|
|
326
|
+
export const channels = {
|
|
327
|
+
collectionUpdates: (collectionId: string) =>
|
|
328
|
+
`collection:${collectionId}:updates`,
|
|
329
|
+
|
|
330
|
+
productChat: (collectionId: string, productId: string) =>
|
|
331
|
+
`product:${collectionId}:${productId}:chat`,
|
|
332
|
+
|
|
333
|
+
proofActivity: (collectionId: string, productId: string, proofId: string) =>
|
|
334
|
+
`proof:${collectionId}:${productId}:${proofId}:activity`,
|
|
335
|
+
|
|
336
|
+
voteResults: (collectionId: string, appId: string, interactionId: string) =>
|
|
337
|
+
`votes:${collectionId}:${appId}:${interactionId}`,
|
|
338
|
+
};
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Usage with SmartLinks Context
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
import { useChannel } from 'ably/react';
|
|
345
|
+
import { usePersistentQueryParams } from '@/hooks/usePersistentQueryParams';
|
|
346
|
+
import { channels } from '@/lib/ably-channels';
|
|
347
|
+
|
|
348
|
+
const LiveProductChat = () => {
|
|
349
|
+
const { persistentQueryParams } = usePersistentQueryParams();
|
|
350
|
+
const { collectionId, productId } = persistentQueryParams;
|
|
351
|
+
|
|
352
|
+
const channelName = channels.productChat(collectionId!, productId!);
|
|
353
|
+
|
|
354
|
+
const { channel } = useChannel(channelName, (message) => {
|
|
355
|
+
// Handle incoming chat message
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// ...
|
|
359
|
+
};
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Example: Live Chat Component
|
|
365
|
+
|
|
366
|
+
Full working example of a chat component:
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
// src/components/LiveChat.tsx
|
|
370
|
+
import { useChannel, usePresence } from 'ably/react';
|
|
371
|
+
import { useState, useRef, useEffect } from 'react';
|
|
372
|
+
import { Button } from '@/components/ui/button';
|
|
373
|
+
import { Input } from '@/components/ui/input';
|
|
374
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
375
|
+
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
|
376
|
+
|
|
377
|
+
interface ChatMessage {
|
|
378
|
+
id: string;
|
|
379
|
+
text: string;
|
|
380
|
+
sender: string;
|
|
381
|
+
senderName: string;
|
|
382
|
+
timestamp: number;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
interface LiveChatProps {
|
|
386
|
+
channelName: string;
|
|
387
|
+
userId: string;
|
|
388
|
+
userName: string;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export const LiveChat = ({ channelName, userId, userName }: LiveChatProps) => {
|
|
392
|
+
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
393
|
+
const [inputValue, setInputValue] = useState('');
|
|
394
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
395
|
+
|
|
396
|
+
// Subscribe to chat messages
|
|
397
|
+
const { channel } = useChannel(channelName, (message) => {
|
|
398
|
+
if (message.name === 'chat') {
|
|
399
|
+
setMessages(prev => [...prev, message.data as ChatMessage]);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Track presence
|
|
404
|
+
const { presenceData } = usePresence(channelName, {
|
|
405
|
+
id: userId,
|
|
406
|
+
name: userName,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Auto-scroll to bottom
|
|
410
|
+
useEffect(() => {
|
|
411
|
+
scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
412
|
+
}, [messages]);
|
|
413
|
+
|
|
414
|
+
const sendMessage = () => {
|
|
415
|
+
if (!inputValue.trim()) return;
|
|
416
|
+
|
|
417
|
+
const message: ChatMessage = {
|
|
418
|
+
id: `${userId}-${Date.now()}`,
|
|
419
|
+
text: inputValue.trim(),
|
|
420
|
+
sender: userId,
|
|
421
|
+
senderName: userName,
|
|
422
|
+
timestamp: Date.now(),
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
channel.publish('chat', message);
|
|
426
|
+
setInputValue('');
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
return (
|
|
430
|
+
<div className="flex flex-col h-[400px] border rounded-lg">
|
|
431
|
+
{/* Header with presence */}
|
|
432
|
+
<div className="p-3 border-b bg-muted/50">
|
|
433
|
+
<div className="flex items-center gap-2">
|
|
434
|
+
<span className="text-sm font-medium">Live Chat</span>
|
|
435
|
+
<span className="text-xs text-muted-foreground">
|
|
436
|
+
({presenceData.length} online)
|
|
437
|
+
</span>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
{/* Messages */}
|
|
442
|
+
<ScrollArea className="flex-1 p-4">
|
|
443
|
+
<div className="space-y-4">
|
|
444
|
+
{messages.map((msg) => (
|
|
445
|
+
<div
|
|
446
|
+
key={msg.id}
|
|
447
|
+
className={`flex gap-2 ${
|
|
448
|
+
msg.sender === userId ? 'flex-row-reverse' : ''
|
|
449
|
+
}`}
|
|
450
|
+
>
|
|
451
|
+
<Avatar className="h-8 w-8">
|
|
452
|
+
<AvatarFallback>
|
|
453
|
+
{msg.senderName.charAt(0).toUpperCase()}
|
|
454
|
+
</AvatarFallback>
|
|
455
|
+
</Avatar>
|
|
456
|
+
<div
|
|
457
|
+
className={`rounded-lg px-3 py-2 max-w-[70%] ${
|
|
458
|
+
msg.sender === userId
|
|
459
|
+
? 'bg-primary text-primary-foreground'
|
|
460
|
+
: 'bg-muted'
|
|
461
|
+
}`}
|
|
462
|
+
>
|
|
463
|
+
<p className="text-sm">{msg.text}</p>
|
|
464
|
+
<span className="text-xs opacity-70">
|
|
465
|
+
{new Date(msg.timestamp).toLocaleTimeString()}
|
|
466
|
+
</span>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
))}
|
|
470
|
+
<div ref={scrollRef} />
|
|
471
|
+
</div>
|
|
472
|
+
</ScrollArea>
|
|
473
|
+
|
|
474
|
+
{/* Input */}
|
|
475
|
+
<div className="p-3 border-t">
|
|
476
|
+
<form
|
|
477
|
+
onSubmit={(e) => {
|
|
478
|
+
e.preventDefault();
|
|
479
|
+
sendMessage();
|
|
480
|
+
}}
|
|
481
|
+
className="flex gap-2"
|
|
482
|
+
>
|
|
483
|
+
<Input
|
|
484
|
+
value={inputValue}
|
|
485
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
486
|
+
placeholder="Type a message..."
|
|
487
|
+
className="flex-1"
|
|
488
|
+
/>
|
|
489
|
+
<Button type="submit" size="sm">
|
|
490
|
+
Send
|
|
491
|
+
</Button>
|
|
492
|
+
</form>
|
|
493
|
+
</div>
|
|
494
|
+
</div>
|
|
495
|
+
);
|
|
496
|
+
};
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## Example: Live Vote Counter
|
|
502
|
+
|
|
503
|
+
Display real-time vote tallies:
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
// src/components/LiveVoteCounter.tsx
|
|
507
|
+
import { useChannel } from 'ably/react';
|
|
508
|
+
import { useState, useEffect } from 'react';
|
|
509
|
+
import { Progress } from '@/components/ui/progress';
|
|
510
|
+
import * as SL from '@proveanything/smartlinks';
|
|
511
|
+
|
|
512
|
+
interface VoteCounts {
|
|
513
|
+
[option: string]: number;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
interface LiveVoteCounterProps {
|
|
517
|
+
collectionId: string;
|
|
518
|
+
appId: string;
|
|
519
|
+
interactionId: string;
|
|
520
|
+
options: string[];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export const LiveVoteCounter = ({
|
|
524
|
+
collectionId,
|
|
525
|
+
appId,
|
|
526
|
+
interactionId,
|
|
527
|
+
options,
|
|
528
|
+
}: LiveVoteCounterProps) => {
|
|
529
|
+
const [votes, setVotes] = useState<VoteCounts>({});
|
|
530
|
+
const channelName = `votes:${collectionId}:${appId}:${interactionId}`;
|
|
531
|
+
|
|
532
|
+
// Fetch initial counts
|
|
533
|
+
useEffect(() => {
|
|
534
|
+
const fetchCounts = async () => {
|
|
535
|
+
const counts = await SL.interactions.countsByOutcome(collectionId, {
|
|
536
|
+
appId,
|
|
537
|
+
interactionId,
|
|
538
|
+
});
|
|
539
|
+
setVotes(counts || {});
|
|
540
|
+
};
|
|
541
|
+
fetchCounts();
|
|
542
|
+
}, [collectionId, appId, interactionId]);
|
|
543
|
+
|
|
544
|
+
// Subscribe to real-time vote updates
|
|
545
|
+
useChannel(channelName, (message) => {
|
|
546
|
+
if (message.name === 'vote') {
|
|
547
|
+
const { option } = message.data;
|
|
548
|
+
setVotes(prev => ({
|
|
549
|
+
...prev,
|
|
550
|
+
[option]: (prev[option] || 0) + 1,
|
|
551
|
+
}));
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
const totalVotes = Object.values(votes).reduce((sum, n) => sum + n, 0);
|
|
556
|
+
|
|
557
|
+
return (
|
|
558
|
+
<div className="space-y-4 p-4">
|
|
559
|
+
<h3 className="font-semibold">Live Results</h3>
|
|
560
|
+
|
|
561
|
+
{options.map((option) => {
|
|
562
|
+
const count = votes[option] || 0;
|
|
563
|
+
const percentage = totalVotes > 0 ? (count / totalVotes) * 100 : 0;
|
|
564
|
+
|
|
565
|
+
return (
|
|
566
|
+
<div key={option} className="space-y-1">
|
|
567
|
+
<div className="flex justify-between text-sm">
|
|
568
|
+
<span>{option}</span>
|
|
569
|
+
<span className="text-muted-foreground">
|
|
570
|
+
{count} ({percentage.toFixed(1)}%)
|
|
571
|
+
</span>
|
|
572
|
+
</div>
|
|
573
|
+
<Progress value={percentage} className="h-2" />
|
|
574
|
+
</div>
|
|
575
|
+
);
|
|
576
|
+
})}
|
|
577
|
+
|
|
578
|
+
<p className="text-sm text-muted-foreground text-center">
|
|
579
|
+
Total votes: {totalVotes}
|
|
580
|
+
</p>
|
|
581
|
+
</div>
|
|
582
|
+
);
|
|
583
|
+
};
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Publishing Vote Updates
|
|
587
|
+
|
|
588
|
+
When a user votes, publish to the channel:
|
|
589
|
+
|
|
590
|
+
```typescript
|
|
591
|
+
const submitVote = async (option: string) => {
|
|
592
|
+
// Record in SmartLinks
|
|
593
|
+
await SL.interactions.submitPublicEvent(collectionId, {
|
|
594
|
+
appId,
|
|
595
|
+
interactionId: 'poll',
|
|
596
|
+
outcome: option,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// Publish to Ably for real-time updates
|
|
600
|
+
channel.publish('vote', { option });
|
|
601
|
+
};
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
## Best Practices
|
|
607
|
+
|
|
608
|
+
### 1. Clean Up Subscriptions
|
|
609
|
+
|
|
610
|
+
React hooks automatically handle cleanup, but for manual subscriptions:
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
useEffect(() => {
|
|
614
|
+
const channel = ably.channels.get('my-channel');
|
|
615
|
+
|
|
616
|
+
const handler = (message: Ably.Message) => {
|
|
617
|
+
// Handle message
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
channel.subscribe('event', handler);
|
|
621
|
+
|
|
622
|
+
return () => {
|
|
623
|
+
channel.unsubscribe('event', handler);
|
|
624
|
+
};
|
|
625
|
+
}, []);
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### 2. Handle Connection States
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
const { channel } = useChannel('my-channel', handleMessage);
|
|
632
|
+
|
|
633
|
+
useConnectionStateListener((stateChange) => {
|
|
634
|
+
if (stateChange.current === 'disconnected') {
|
|
635
|
+
// Show reconnecting UI
|
|
636
|
+
}
|
|
637
|
+
if (stateChange.current === 'connected') {
|
|
638
|
+
// Clear reconnecting UI, maybe refetch missed messages
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### 3. Debounce Rapid Updates
|
|
644
|
+
|
|
645
|
+
For typing indicators or frequent updates:
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
import { useDebouncedCallback } from 'use-debounce';
|
|
649
|
+
|
|
650
|
+
const debouncedPublish = useDebouncedCallback((data) => {
|
|
651
|
+
channel.publish('typing', data);
|
|
652
|
+
}, 300);
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### 4. Message Size Limits
|
|
656
|
+
|
|
657
|
+
Ably has a 64KB message size limit. For large data:
|
|
658
|
+
- Store in SmartLinks/database
|
|
659
|
+
- Send only the ID via Ably
|
|
660
|
+
- Fetch full data on receive
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
// Publishing
|
|
664
|
+
channel.publish('update', { recordId: '123' });
|
|
665
|
+
|
|
666
|
+
// Receiving
|
|
667
|
+
useChannel(channelName, async (message) => {
|
|
668
|
+
const fullData = await SL.appConfiguration.getDataItem({
|
|
669
|
+
collectionId,
|
|
670
|
+
appId,
|
|
671
|
+
itemId: message.data.recordId,
|
|
672
|
+
});
|
|
673
|
+
// Use fullData
|
|
674
|
+
});
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### 5. Error Handling
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
try {
|
|
681
|
+
await channel.publish('event', data);
|
|
682
|
+
} catch (error) {
|
|
683
|
+
if (error instanceof Ably.ErrorInfo) {
|
|
684
|
+
console.error('Ably error:', error.code, error.message);
|
|
685
|
+
// Handle specific error codes
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## Environment Variables
|
|
693
|
+
|
|
694
|
+
For development, add to your local `.env`:
|
|
695
|
+
|
|
696
|
+
```env
|
|
697
|
+
VITE_ABLY_API_KEY=your-api-key-here
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
For production, use Lovable Cloud secrets or your deployment platform's environment variables.
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
## TypeScript Types
|
|
705
|
+
|
|
706
|
+
Ably provides full TypeScript support. Key types:
|
|
707
|
+
|
|
708
|
+
```typescript
|
|
709
|
+
import Ably from 'ably';
|
|
710
|
+
|
|
711
|
+
// Message type
|
|
712
|
+
interface CustomMessage {
|
|
713
|
+
text: string;
|
|
714
|
+
sender: string;
|
|
715
|
+
timestamp: number;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Typed channel usage
|
|
719
|
+
const { channel } = useChannel<CustomMessage>('my-channel', (message) => {
|
|
720
|
+
const data: CustomMessage = message.data;
|
|
721
|
+
console.log(data.text); // Typed!
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// Presence data type
|
|
725
|
+
interface PresenceData {
|
|
726
|
+
id: string;
|
|
727
|
+
name: string;
|
|
728
|
+
status: 'active' | 'away';
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const { presenceData } = usePresence<PresenceData>(channelName, {
|
|
732
|
+
id: userId,
|
|
733
|
+
name: userName,
|
|
734
|
+
status: 'active',
|
|
735
|
+
});
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
---
|
|
739
|
+
|
|
740
|
+
## Troubleshooting
|
|
741
|
+
|
|
742
|
+
### "Ably is not defined"
|
|
743
|
+
|
|
744
|
+
Ensure you've installed the package:
|
|
745
|
+
```bash
|
|
746
|
+
npm install ably
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### "No API key provided"
|
|
750
|
+
|
|
751
|
+
Check that `VITE_ABLY_API_KEY` is set in your environment.
|
|
752
|
+
|
|
753
|
+
### Messages not received
|
|
754
|
+
|
|
755
|
+
1. Check channel names match exactly
|
|
756
|
+
2. Verify connection state is 'connected'
|
|
757
|
+
3. Check browser console for Ably errors
|
|
758
|
+
4. Ensure you're subscribed before publishing
|
|
759
|
+
|
|
760
|
+
### Too many connections
|
|
761
|
+
|
|
762
|
+
Each browser tab opens a new connection. In development:
|
|
763
|
+
- Close unused tabs
|
|
764
|
+
- Use the same client instance across components (provider pattern)
|