mnfst 0.5.164 → 0.5.165
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.
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/* Manifest Appwrite Presences ($presence)
|
|
2
|
+
/* By Andrew Matlock under MIT license
|
|
3
|
+
/* https://github.com/andrewmatlock/Manifest
|
|
4
|
+
/*
|
|
5
|
+
/* Reactive user status/roster over the native Appwrite Presences API
|
|
6
|
+
/* Requires Alpine JS (alpinejs.dev) to operate
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
(function () {
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
/* State */
|
|
13
|
+
|
|
14
|
+
const ctx = {
|
|
15
|
+
client: null,
|
|
16
|
+
presences: null,
|
|
17
|
+
realtime: null,
|
|
18
|
+
sub: null,
|
|
19
|
+
heartbeat: null,
|
|
20
|
+
started: false,
|
|
21
|
+
status: 'online',
|
|
22
|
+
metadata: null,
|
|
23
|
+
auto: true, // focus/blur auto-flip
|
|
24
|
+
heartbeatMs: 25000, // TTL refresh cadence
|
|
25
|
+
onFocus: null,
|
|
26
|
+
onBlur: null
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/* Config */
|
|
30
|
+
|
|
31
|
+
// Resolve ${VAR} against window.env
|
|
32
|
+
function resolveEnv(v) {
|
|
33
|
+
if (typeof v !== 'string') return v;
|
|
34
|
+
return v.replace(/\$\{([^}]+)\}/g, (m, name) => {
|
|
35
|
+
if (typeof window !== 'undefined' && window.env && window.env[name] != null) return window.env[name];
|
|
36
|
+
return null;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function getConfig() {
|
|
41
|
+
try {
|
|
42
|
+
const manifest = await window.ManifestDataConfig?.ensureManifest?.();
|
|
43
|
+
const aw = manifest?.appwrite;
|
|
44
|
+
if (!aw) return null;
|
|
45
|
+
const endpoint = resolveEnv(aw.endpoint);
|
|
46
|
+
const projectId = resolveEnv(aw.projectId);
|
|
47
|
+
if (!endpoint || !projectId) return null;
|
|
48
|
+
let devKey = aw.devKey ? resolveEnv(aw.devKey) : null;
|
|
49
|
+
if (devKey && /\$\{/.test(devKey)) devKey = null;
|
|
50
|
+
return { endpoint, projectId, devKey };
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function store() {
|
|
57
|
+
return (typeof Alpine !== 'undefined') ? Alpine.store('presence') : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function authUser() {
|
|
61
|
+
const s = (typeof Alpine !== 'undefined') ? Alpine.store('auth') : null;
|
|
62
|
+
return s && s.user ? s.user : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Active team id (read-scoping)
|
|
66
|
+
function currentTeamId() {
|
|
67
|
+
const s = (typeof Alpine !== 'undefined') ? Alpine.store('auth') : null;
|
|
68
|
+
if (!s) return null;
|
|
69
|
+
if (s.currentTeam) return typeof s.currentTeam === 'string' ? s.currentTeam : (s.currentTeam.$id || s.currentTeam.id || null);
|
|
70
|
+
if (Array.isArray(s.teams) && s.teams.length) return s.teams[0].$id || s.teams[0].id || null;
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Consistent per-user color
|
|
75
|
+
function userColor(id) {
|
|
76
|
+
if (!id) return '#666';
|
|
77
|
+
let hash = 0;
|
|
78
|
+
for (let i = 0; i < id.length; i++) hash = id.charCodeAt(i) + ((hash << 5) - hash);
|
|
79
|
+
return `hsl(${Math.abs(hash) % 360}, 70%, 50%)`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Appwrite client + services — authed via the session cookie, not the dev key (which is the guests principal for user ops)
|
|
83
|
+
async function ensureServices() {
|
|
84
|
+
if (ctx.presences && ctx.realtime) return true;
|
|
85
|
+
const A = window.Appwrite;
|
|
86
|
+
if (!A || !A.Presences || !A.Realtime) return false;
|
|
87
|
+
const cfg = await getConfig();
|
|
88
|
+
if (!cfg) return false;
|
|
89
|
+
const client = new A.Client().setEndpoint(cfg.endpoint).setProject(cfg.projectId);
|
|
90
|
+
ctx.client = client;
|
|
91
|
+
ctx.presences = new A.Presences(client);
|
|
92
|
+
ctx.realtime = new A.Realtime(client);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Records */
|
|
97
|
+
|
|
98
|
+
function normalize(rec) {
|
|
99
|
+
if (!rec) return null;
|
|
100
|
+
const userId = rec.userId || rec.$userId || rec.presenceId || rec.$id;
|
|
101
|
+
if (!userId) return null;
|
|
102
|
+
let metadata = rec.metadata;
|
|
103
|
+
if (typeof metadata === 'string') { try { metadata = JSON.parse(metadata); } catch (e) { } }
|
|
104
|
+
const updatedAt = rec.$updatedAt || rec.$createdAt || null;
|
|
105
|
+
return {
|
|
106
|
+
userId,
|
|
107
|
+
status: rec.status || null,
|
|
108
|
+
metadata: metadata || null,
|
|
109
|
+
lastSeen: updatedAt ? Date.parse(updatedAt) : Date.now(),
|
|
110
|
+
expiresAt: rec.expiresAt || rec.$expiresAt || null,
|
|
111
|
+
color: userColor(userId)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Reactive map updates (reassign for Alpine tracking)
|
|
116
|
+
function putRecord(rec) {
|
|
117
|
+
const s = store(); if (!s || !rec) return;
|
|
118
|
+
s.records = { ...s.records, [rec.userId]: rec };
|
|
119
|
+
}
|
|
120
|
+
function removeRecord(userId) {
|
|
121
|
+
const s = store(); if (!s || !userId) return;
|
|
122
|
+
if (!s.records[userId]) return;
|
|
123
|
+
const next = { ...s.records }; delete next[userId]; s.records = next;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function hydrate() {
|
|
127
|
+
const s = store();
|
|
128
|
+
try {
|
|
129
|
+
const res = await ctx.presences.list();
|
|
130
|
+
const rows = res?.presences || res?.rows || res?.documents || [];
|
|
131
|
+
const map = {};
|
|
132
|
+
rows.forEach(r => { const n = normalize(r); if (n) map[n.userId] = n; });
|
|
133
|
+
if (s) s.records = map;
|
|
134
|
+
} catch (e) {
|
|
135
|
+
if (s) s.error = e?.message || String(e);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function subscribe() {
|
|
140
|
+
const A = window.Appwrite;
|
|
141
|
+
try {
|
|
142
|
+
const handle = ctx.realtime.subscribe(A.Channel.presences(), (response) => {
|
|
143
|
+
if (!response) return;
|
|
144
|
+
const events = Array.isArray(response.events) ? response.events : (response.events ? [response.events] : []);
|
|
145
|
+
const rec = normalize(response.payload || response);
|
|
146
|
+
if (!rec) return;
|
|
147
|
+
// Ignore echoes of our own presence (tracked locally; delayed echoes would clobber newer status)
|
|
148
|
+
const meId = authUser()?.$id;
|
|
149
|
+
if (meId && rec.userId === meId) return;
|
|
150
|
+
if (events.some(e => typeof e === 'string' && e.includes('delete'))) removeRecord(rec.userId);
|
|
151
|
+
else putRecord(rec);
|
|
152
|
+
});
|
|
153
|
+
// Normalise the unsubscribe handle
|
|
154
|
+
if (handle && typeof handle.then === 'function') {
|
|
155
|
+
handle.then(r => { ctx.sub = typeof r === 'function' ? r : (r && r.close ? () => r.close() : null); });
|
|
156
|
+
ctx.sub = () => { handle.then(r => { if (typeof r === 'function') r(); else if (r && r.close) r.close(); }); };
|
|
157
|
+
} else if (typeof handle === 'function') {
|
|
158
|
+
ctx.sub = handle;
|
|
159
|
+
} else if (handle && typeof handle.close === 'function') {
|
|
160
|
+
ctx.sub = () => handle.close();
|
|
161
|
+
}
|
|
162
|
+
} catch (e) {
|
|
163
|
+
const s = store(); if (s) s.error = e?.message || String(e);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Operations */
|
|
168
|
+
|
|
169
|
+
async function set(status, metadata) {
|
|
170
|
+
const A = window.Appwrite;
|
|
171
|
+
const user = authUser();
|
|
172
|
+
const s = store();
|
|
173
|
+
if (!user) { if (s) s.error = 'sign in required'; return false; }
|
|
174
|
+
if (!(await ensureServices())) { if (s) s.error = 'Appwrite Presences unavailable'; return false; }
|
|
175
|
+
ctx.status = status != null ? status : ctx.status;
|
|
176
|
+
ctx.metadata = metadata !== undefined ? metadata : ctx.metadata;
|
|
177
|
+
|
|
178
|
+
// Read-scope + owner write (owner update/delete required, else updates 401)
|
|
179
|
+
const teamId = currentTeamId();
|
|
180
|
+
const permissions = [
|
|
181
|
+
teamId ? A.Permission.read(A.Role.team(teamId)) : A.Permission.read(A.Role.users()),
|
|
182
|
+
A.Permission.update(A.Role.user(user.$id)),
|
|
183
|
+
A.Permission.delete(A.Role.user(user.$id))
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
// name/color in metadata for rosters (name null when unknown — display label is the author's)
|
|
187
|
+
const meta = Object.assign(
|
|
188
|
+
{ name: user.name || user.email || null, color: userColor(user.$id) },
|
|
189
|
+
ctx.metadata || {}
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Optimistic local update
|
|
193
|
+
putRecord(normalize({ userId: user.$id, status: ctx.status, metadata: meta, $updatedAt: new Date().toISOString() }));
|
|
194
|
+
if (s) s.me = ctx.status;
|
|
195
|
+
try {
|
|
196
|
+
await ctx.presences.upsert({ presenceId: user.$id, status: ctx.status, metadata: meta, permissions });
|
|
197
|
+
if (s) s.error = null;
|
|
198
|
+
return true;
|
|
199
|
+
} catch (e) {
|
|
200
|
+
if (s) s.error = e?.message || String(e);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function clear() {
|
|
206
|
+
const user = authUser();
|
|
207
|
+
if (!user || !ctx.presences) return;
|
|
208
|
+
try { await ctx.presences.delete({ presenceId: user.$id }); } catch (e) { }
|
|
209
|
+
removeRecord(user.$id);
|
|
210
|
+
const s = store(); if (s) s.me = null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function of(userId) {
|
|
214
|
+
const s = store();
|
|
215
|
+
return (s && s.records ? s.records[userId] : null) || null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function all() {
|
|
219
|
+
const s = store();
|
|
220
|
+
return s && s.records ? Object.values(s.records) : [];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function start(options) {
|
|
224
|
+
const opts = options || {};
|
|
225
|
+
if (typeof opts.auto === 'boolean') ctx.auto = opts.auto;
|
|
226
|
+
if (typeof opts.heartbeatMs === 'number') ctx.heartbeatMs = opts.heartbeatMs;
|
|
227
|
+
if (typeof opts.status === 'string') ctx.status = opts.status;
|
|
228
|
+
|
|
229
|
+
const s = store();
|
|
230
|
+
if (!authUser()) { if (s) s.error = 'sign in required'; return false; }
|
|
231
|
+
if (!(await ensureServices())) { if (s) s.error = 'Appwrite Presences unavailable'; return false; }
|
|
232
|
+
if (ctx.started) return true;
|
|
233
|
+
ctx.started = true;
|
|
234
|
+
|
|
235
|
+
await hydrate();
|
|
236
|
+
subscribe();
|
|
237
|
+
await set(ctx.status, ctx.metadata);
|
|
238
|
+
|
|
239
|
+
// Heartbeat (refresh TTL)
|
|
240
|
+
ctx.heartbeat = setInterval(() => { if (authUser()) set(ctx.status, ctx.metadata); }, ctx.heartbeatMs);
|
|
241
|
+
|
|
242
|
+
// Auto focus/blur flip
|
|
243
|
+
if (ctx.auto) {
|
|
244
|
+
ctx.onBlur = () => set('away');
|
|
245
|
+
ctx.onFocus = () => set('online');
|
|
246
|
+
window.addEventListener('blur', ctx.onBlur);
|
|
247
|
+
window.addEventListener('focus', ctx.onFocus);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (s) { s.ready = true; s.error = null; }
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function stop() {
|
|
255
|
+
if (ctx.heartbeat) { clearInterval(ctx.heartbeat); ctx.heartbeat = null; }
|
|
256
|
+
if (ctx.onBlur) { window.removeEventListener('blur', ctx.onBlur); ctx.onBlur = null; }
|
|
257
|
+
if (ctx.onFocus) { window.removeEventListener('focus', ctx.onFocus); ctx.onFocus = null; }
|
|
258
|
+
if (typeof ctx.sub === 'function') { try { ctx.sub(); } catch (e) { } }
|
|
259
|
+
ctx.sub = null;
|
|
260
|
+
ctx.started = false;
|
|
261
|
+
const s = store(); if (s) s.ready = false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Registration */
|
|
265
|
+
|
|
266
|
+
document.addEventListener('alpine:init', () => {
|
|
267
|
+
Alpine.store('presence', { records: {}, ready: false, error: null, me: null });
|
|
268
|
+
|
|
269
|
+
// Clear on sign-out
|
|
270
|
+
try {
|
|
271
|
+
Alpine.effect(() => {
|
|
272
|
+
if (!authUser() && ctx.started) { clear(); stop(); }
|
|
273
|
+
});
|
|
274
|
+
} catch (e) { }
|
|
275
|
+
|
|
276
|
+
const api = {
|
|
277
|
+
start, stop, set, clear, of, all,
|
|
278
|
+
get records() { return store()?.records || {}; },
|
|
279
|
+
get list() { return all(); },
|
|
280
|
+
get ready() { return !!store()?.ready; },
|
|
281
|
+
get error() { return store()?.error || null; },
|
|
282
|
+
get me() { return store()?.me || null; }
|
|
283
|
+
};
|
|
284
|
+
Alpine.magic('presence', () => api);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Clear on tab close
|
|
288
|
+
window.addEventListener('beforeunload', () => { try { if (ctx.started) clear(); } catch (e) { } });
|
|
289
|
+
|
|
290
|
+
// Non-Alpine handle
|
|
291
|
+
window.ManifestPresence = { start, stop, set, clear, of, all };
|
|
292
|
+
})();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest.appwrite.auth.js": "sha384-+4T3vdzf2NsDrSpatkIqg7cO2HUnY3tNNwSgfyXCCU0NZ7K9eqX2dzhDJIlochlz",
|
|
3
3
|
"manifest.appwrite.data.js": "sha384-KFKVo5TQESLJB7wgk+C8Zb/sAl43QAMdioWW3tQRWYq5umfdpGCZ5Hld9y9f6/Np",
|
|
4
|
+
"manifest.appwrite.presences.js": "sha384-JyVDWFT69b7Svq8J8p9tNWe2v9PZNQBY5UyhtOl1vnuHFAX0CRQ3MA9SiKq5wzNh",
|
|
4
5
|
"manifest.charts.js": "sha384-wTQlB3hJ9V9lmisCgm8l6YkpJV+nA4+Vci3dI0x1nBtWnS9TFr+eXaJelP7AusyD",
|
|
5
6
|
"manifest.chat.js": "sha384-HDyzPvNxqUOKVcLC3HNSFd5w83Flgm58LHy1BDLB9ejUsbYHAmr34G4X3FwKXokW",
|
|
6
7
|
"manifest.code.js": "sha384-G3MNfvQRWWY+gm4SGGrSsgQbDeil9MP+ON2DTk6Rcq9Nsex8woHMDt5EJHrFH0Nj",
|
|
@@ -28,5 +29,5 @@
|
|
|
28
29
|
"manifest.url.parameters.js": "sha384-FIufiClqDx1rJpU/QUc9z/D43qClQ6Qm8rBahipbJl9BDHUvhrOsUDegmTWW7Tuf",
|
|
29
30
|
"manifest.utilities.js": "sha384-MtzNaVOrgyepjlY5nz38pAJk67yiFPU1H5WoPNfcNbUZTXy+oin1lrtKCIcobEOJ",
|
|
30
31
|
"manifest.virtual.js": "sha384-c194pXD0Ld48QJ+HPVNibvbn54/ETJRuYhUOWesEBjtOIQcQMIKo0DnCyQMvDlLg",
|
|
31
|
-
"manifest.js": "sha384-
|
|
32
|
+
"manifest.js": "sha384-VshJgT9QAxsLpwNmQZfrxw/EAxoyLIklo0DsmhxLhjVmmT70fLmn5eNRuq98o6zs"
|
|
32
33
|
}
|
package/lib/manifest.js
CHANGED
|
@@ -241,13 +241,13 @@
|
|
|
241
241
|
const APPWRITE_PLUGINS = [
|
|
242
242
|
'appwrite-auth',
|
|
243
243
|
'appwrite-data',
|
|
244
|
-
'appwrite-
|
|
244
|
+
'appwrite-presences'
|
|
245
245
|
];
|
|
246
246
|
|
|
247
247
|
// Plugin dependencies: plugins that require other plugins to be loaded first
|
|
248
248
|
const PLUGIN_DEPENDENCIES = {
|
|
249
249
|
'appwrite-data': ['data'],
|
|
250
|
-
'appwrite-
|
|
250
|
+
'appwrite-presences': ['data', 'appwrite-auth']
|
|
251
251
|
};
|
|
252
252
|
|
|
253
253
|
// Derive default plugin list from manifest (only load data/localization/components when manifest needs them)
|
|
@@ -422,7 +422,7 @@
|
|
|
422
422
|
plugins.push('appwrite-data');
|
|
423
423
|
}
|
|
424
424
|
if (manifest.appwrite?.presence) {
|
|
425
|
-
plugins.push('appwrite-
|
|
425
|
+
plugins.push('appwrite-presences');
|
|
426
426
|
}
|
|
427
427
|
return plugins;
|
|
428
428
|
}
|
|
@@ -541,7 +541,7 @@
|
|
|
541
541
|
|
|
542
542
|
const MANIFEST_DEPENDENT_PLUGINS = [
|
|
543
543
|
'data', 'localization', 'components',
|
|
544
|
-
'appwrite-auth', 'appwrite-data', 'appwrite-
|
|
544
|
+
'appwrite-auth', 'appwrite-data', 'appwrite-presences', 'payments'
|
|
545
545
|
];
|
|
546
546
|
const manifestUrl = (document.querySelector('link[rel="manifest"]')?.getAttribute('href')) || '/manifest.json';
|
|
547
547
|
|