@clarityops/preferences 0.1.1
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/dist/index.d.mts +204 -0
- package/dist/index.d.ts +204 -0
- package/dist/index.js +625 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +596 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +42 -0
- package/src/PreferencesContext.tsx +309 -0
- package/src/PreferencesService.ts +443 -0
- package/src/avatar-cache.ts +55 -0
- package/src/index.ts +38 -0
- package/src/types.ts +94 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AVATAR_CACHE_TTL: () => AVATAR_CACHE_TTL,
|
|
24
|
+
PreferencesProvider: () => PreferencesProvider,
|
|
25
|
+
PreferencesService: () => PreferencesService,
|
|
26
|
+
clearAvatarCache: () => clearAvatarCache,
|
|
27
|
+
getCachedAvatar: () => getCachedAvatar,
|
|
28
|
+
hasCachedAvatar: () => hasCachedAvatar,
|
|
29
|
+
setCachedAvatar: () => setCachedAvatar,
|
|
30
|
+
useAvatar: () => useAvatar,
|
|
31
|
+
usePreferences: () => usePreferences,
|
|
32
|
+
usePreferencesSafe: () => usePreferencesSafe
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(index_exports);
|
|
35
|
+
|
|
36
|
+
// src/PreferencesContext.tsx
|
|
37
|
+
var import_react = require("react");
|
|
38
|
+
|
|
39
|
+
// src/PreferencesService.ts
|
|
40
|
+
function isValidJwtFormat(token) {
|
|
41
|
+
if (!token || typeof token !== "string") return false;
|
|
42
|
+
if (token === "null" || token === "undefined" || token.trim() === "") return false;
|
|
43
|
+
const segments = token.split(".");
|
|
44
|
+
return segments.length === 3;
|
|
45
|
+
}
|
|
46
|
+
function isTokenExpired(token) {
|
|
47
|
+
try {
|
|
48
|
+
const payload = JSON.parse(atob(token.split(".")[1]));
|
|
49
|
+
const currentTime = Math.floor(Date.now() / 1e3);
|
|
50
|
+
return payload.exp < currentTime;
|
|
51
|
+
} catch {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
var refreshInProgress = null;
|
|
56
|
+
var lastRefreshTime = 0;
|
|
57
|
+
var REFRESH_DEBOUNCE_MS = 5e3;
|
|
58
|
+
var PreferencesService = class {
|
|
59
|
+
constructor(config) {
|
|
60
|
+
this.baseUrl = config.apiBaseUrl;
|
|
61
|
+
this.brandingUrl = config.brandingUrl;
|
|
62
|
+
this.getToken = config.getToken;
|
|
63
|
+
this.onTokenInvalid = config.onTokenInvalid;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Update the config (e.g., when token changes)
|
|
67
|
+
*/
|
|
68
|
+
updateConfig(config) {
|
|
69
|
+
if (config.getToken) {
|
|
70
|
+
this.getToken = config.getToken;
|
|
71
|
+
}
|
|
72
|
+
if (config.onTokenInvalid !== void 0) {
|
|
73
|
+
this.onTokenInvalid = config.onTokenInvalid;
|
|
74
|
+
}
|
|
75
|
+
if (config.apiBaseUrl) {
|
|
76
|
+
this.baseUrl = config.apiBaseUrl;
|
|
77
|
+
}
|
|
78
|
+
if (config.brandingUrl) {
|
|
79
|
+
this.brandingUrl = config.brandingUrl;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Refresh token with debounce to prevent multiple simultaneous refresh attempts
|
|
84
|
+
*/
|
|
85
|
+
async refreshWithDebounce() {
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
if (refreshInProgress) {
|
|
88
|
+
console.log("[PreferencesService] \u23F3 Waiting for existing refresh to complete...");
|
|
89
|
+
return refreshInProgress;
|
|
90
|
+
}
|
|
91
|
+
if (now - lastRefreshTime < REFRESH_DEBOUNCE_MS) {
|
|
92
|
+
console.log("[PreferencesService] \u23F3 Recent refresh detected, using current token");
|
|
93
|
+
return this.getToken();
|
|
94
|
+
}
|
|
95
|
+
if (this.onTokenInvalid) {
|
|
96
|
+
console.log("[PreferencesService] \u{1F504} Starting token refresh...");
|
|
97
|
+
lastRefreshTime = now;
|
|
98
|
+
refreshInProgress = this.onTokenInvalid().finally(() => {
|
|
99
|
+
refreshInProgress = null;
|
|
100
|
+
});
|
|
101
|
+
return refreshInProgress;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get a valid token, refreshing if necessary with debounce protection
|
|
107
|
+
*/
|
|
108
|
+
async getValidToken() {
|
|
109
|
+
let token = this.getToken();
|
|
110
|
+
if (!isValidJwtFormat(token)) {
|
|
111
|
+
console.warn("[PreferencesService] Token is missing or malformed, attempting refresh...");
|
|
112
|
+
token = await this.refreshWithDebounce();
|
|
113
|
+
if (!isValidJwtFormat(token)) {
|
|
114
|
+
throw new Error("Authentication required - no valid token available");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (isTokenExpired(token)) {
|
|
118
|
+
console.warn("[PreferencesService] Token is expired, attempting refresh...");
|
|
119
|
+
token = await this.refreshWithDebounce();
|
|
120
|
+
if (!token || isTokenExpired(token)) {
|
|
121
|
+
throw new Error("Session expired - please log in again");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return token;
|
|
125
|
+
}
|
|
126
|
+
async request(endpoint, options = {}) {
|
|
127
|
+
const token = await this.getValidToken();
|
|
128
|
+
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
|
129
|
+
...options,
|
|
130
|
+
headers: {
|
|
131
|
+
"Authorization": `Bearer ${token}`,
|
|
132
|
+
"Content-Type": "application/json",
|
|
133
|
+
...options.headers
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
const error = await response.json().catch(() => ({ error: "Request failed" }));
|
|
138
|
+
if (response.status === 401 && this.onTokenInvalid) {
|
|
139
|
+
console.warn("[PreferencesService] Got 401, attempting token refresh and retry...");
|
|
140
|
+
const newToken = await this.onTokenInvalid();
|
|
141
|
+
if (newToken && isValidJwtFormat(newToken)) {
|
|
142
|
+
const retryResponse = await fetch(`${this.baseUrl}${endpoint}`, {
|
|
143
|
+
...options,
|
|
144
|
+
headers: {
|
|
145
|
+
"Authorization": `Bearer ${newToken}`,
|
|
146
|
+
"Content-Type": "application/json",
|
|
147
|
+
...options.headers
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
if (retryResponse.ok) {
|
|
151
|
+
return retryResponse.json();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
throw new Error(error.error || error.message || "Request failed");
|
|
156
|
+
}
|
|
157
|
+
const data = await response.json();
|
|
158
|
+
return data;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get user preferences
|
|
162
|
+
*/
|
|
163
|
+
async getPreferences() {
|
|
164
|
+
return this.request("/forge/users/me/preferences");
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Update preferences (merge update)
|
|
168
|
+
*/
|
|
169
|
+
async updatePreferences(preferences) {
|
|
170
|
+
return this.request("/forge/users/me/preferences", {
|
|
171
|
+
method: "PATCH",
|
|
172
|
+
body: JSON.stringify(preferences)
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Replace all preferences
|
|
177
|
+
*/
|
|
178
|
+
async replacePreferences(preferences) {
|
|
179
|
+
return this.request("/forge/users/me/preferences", {
|
|
180
|
+
method: "PUT",
|
|
181
|
+
body: JSON.stringify(preferences)
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Step 1: Prepare avatar upload - get signed URL for GCS
|
|
186
|
+
*/
|
|
187
|
+
async prepareAvatarUpload(filename, mimeType) {
|
|
188
|
+
const token = await this.getValidToken();
|
|
189
|
+
const response = await fetch(`${this.brandingUrl}/branding/prepare-avatar-upload`, {
|
|
190
|
+
method: "POST",
|
|
191
|
+
headers: {
|
|
192
|
+
"Authorization": `Bearer ${token}`,
|
|
193
|
+
"Content-Type": "application/json"
|
|
194
|
+
},
|
|
195
|
+
body: JSON.stringify({
|
|
196
|
+
filename,
|
|
197
|
+
mime_type: mimeType
|
|
198
|
+
})
|
|
199
|
+
});
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
const error = await response.json().catch(() => ({ error: "Failed to prepare upload" }));
|
|
202
|
+
if (response.status === 401 && this.onTokenInvalid) {
|
|
203
|
+
console.warn("[PreferencesService] Got 401 on prepareAvatarUpload, attempting refresh...");
|
|
204
|
+
const newToken = await this.onTokenInvalid();
|
|
205
|
+
if (newToken && isValidJwtFormat(newToken)) {
|
|
206
|
+
const retryResponse = await fetch(`${this.brandingUrl}/branding/prepare-avatar-upload`, {
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: {
|
|
209
|
+
"Authorization": `Bearer ${newToken}`,
|
|
210
|
+
"Content-Type": "application/json"
|
|
211
|
+
},
|
|
212
|
+
body: JSON.stringify({
|
|
213
|
+
filename,
|
|
214
|
+
mime_type: mimeType
|
|
215
|
+
})
|
|
216
|
+
});
|
|
217
|
+
if (retryResponse.ok) {
|
|
218
|
+
const data2 = await retryResponse.json();
|
|
219
|
+
if (data2.success) return data2.data;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
throw new Error(error.error || error.message || "Failed to prepare avatar upload");
|
|
224
|
+
}
|
|
225
|
+
const data = await response.json();
|
|
226
|
+
if (!data.success) {
|
|
227
|
+
throw new Error(data.error || "Failed to prepare avatar upload");
|
|
228
|
+
}
|
|
229
|
+
return data.data;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Step 2: Upload file directly to GCS
|
|
233
|
+
*/
|
|
234
|
+
async uploadToGCS(uploadUrl, file) {
|
|
235
|
+
const response = await fetch(uploadUrl, {
|
|
236
|
+
method: "PUT",
|
|
237
|
+
headers: {
|
|
238
|
+
"Content-Type": file.type
|
|
239
|
+
},
|
|
240
|
+
body: file
|
|
241
|
+
});
|
|
242
|
+
if (!response.ok) {
|
|
243
|
+
throw new Error("Failed to upload file to storage");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Step 3: Confirm avatar upload
|
|
248
|
+
*/
|
|
249
|
+
async confirmAvatarUpload(storagePath) {
|
|
250
|
+
const token = await this.getValidToken();
|
|
251
|
+
const response = await fetch(`${this.brandingUrl}/branding/confirm-avatar-upload`, {
|
|
252
|
+
method: "POST",
|
|
253
|
+
headers: {
|
|
254
|
+
"Authorization": `Bearer ${token}`,
|
|
255
|
+
"Content-Type": "application/json"
|
|
256
|
+
},
|
|
257
|
+
body: JSON.stringify({
|
|
258
|
+
storage_path: storagePath
|
|
259
|
+
})
|
|
260
|
+
});
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
const error = await response.json().catch(() => ({ error: "Failed to confirm upload" }));
|
|
263
|
+
if (response.status === 401 && this.onTokenInvalid) {
|
|
264
|
+
console.warn("[PreferencesService] Got 401 on confirmAvatarUpload, attempting refresh...");
|
|
265
|
+
const newToken = await this.onTokenInvalid();
|
|
266
|
+
if (newToken && isValidJwtFormat(newToken)) {
|
|
267
|
+
const retryResponse = await fetch(`${this.brandingUrl}/branding/confirm-avatar-upload`, {
|
|
268
|
+
method: "POST",
|
|
269
|
+
headers: {
|
|
270
|
+
"Authorization": `Bearer ${newToken}`,
|
|
271
|
+
"Content-Type": "application/json"
|
|
272
|
+
},
|
|
273
|
+
body: JSON.stringify({
|
|
274
|
+
storage_path: storagePath
|
|
275
|
+
})
|
|
276
|
+
});
|
|
277
|
+
if (retryResponse.ok) {
|
|
278
|
+
const data2 = await retryResponse.json();
|
|
279
|
+
if (data2.success) return data2.data;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
throw new Error(error.error || error.message || "Failed to confirm avatar upload");
|
|
284
|
+
}
|
|
285
|
+
const data = await response.json();
|
|
286
|
+
if (!data.success) {
|
|
287
|
+
throw new Error(data.error || "Failed to confirm avatar upload");
|
|
288
|
+
}
|
|
289
|
+
return data.data;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Get current user's avatar
|
|
293
|
+
*/
|
|
294
|
+
async getMyAvatar() {
|
|
295
|
+
const token = await this.getValidToken();
|
|
296
|
+
const response = await fetch(`${this.brandingUrl}/branding/my-avatar`, {
|
|
297
|
+
headers: {
|
|
298
|
+
"Authorization": `Bearer ${token}`
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
if (!response.ok) {
|
|
302
|
+
const error = await response.json().catch(() => ({ error: "Failed to fetch avatar" }));
|
|
303
|
+
if (response.status === 401 && this.onTokenInvalid) {
|
|
304
|
+
console.warn("[PreferencesService] Got 401 on getMyAvatar, attempting refresh...");
|
|
305
|
+
const newToken = await this.onTokenInvalid();
|
|
306
|
+
if (newToken && isValidJwtFormat(newToken)) {
|
|
307
|
+
const retryResponse = await fetch(`${this.brandingUrl}/branding/my-avatar`, {
|
|
308
|
+
headers: {
|
|
309
|
+
"Authorization": `Bearer ${newToken}`
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
if (retryResponse.ok) {
|
|
313
|
+
const data2 = await retryResponse.json();
|
|
314
|
+
if (data2.success) return data2.data;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
throw new Error(error.error || error.message || "Failed to fetch avatar");
|
|
319
|
+
}
|
|
320
|
+
const data = await response.json();
|
|
321
|
+
if (!data.success) {
|
|
322
|
+
throw new Error(data.error || "Failed to fetch avatar");
|
|
323
|
+
}
|
|
324
|
+
return data.data;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Get any user's avatar
|
|
328
|
+
*/
|
|
329
|
+
async getUserAvatar(userId) {
|
|
330
|
+
const token = await this.getValidToken();
|
|
331
|
+
const response = await fetch(`${this.brandingUrl}/branding/users/${userId}/avatar`, {
|
|
332
|
+
headers: {
|
|
333
|
+
"Authorization": `Bearer ${token}`
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
if (!response.ok) {
|
|
337
|
+
const error = await response.json().catch(() => ({ error: "Failed to fetch avatar" }));
|
|
338
|
+
if (response.status === 401 && this.onTokenInvalid) {
|
|
339
|
+
console.warn("[PreferencesService] Got 401 on getUserAvatar, attempting refresh...");
|
|
340
|
+
const newToken = await this.onTokenInvalid();
|
|
341
|
+
if (newToken && isValidJwtFormat(newToken)) {
|
|
342
|
+
const retryResponse = await fetch(`${this.brandingUrl}/branding/users/${userId}/avatar`, {
|
|
343
|
+
headers: {
|
|
344
|
+
"Authorization": `Bearer ${newToken}`
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
if (retryResponse.ok) {
|
|
348
|
+
const data2 = await retryResponse.json();
|
|
349
|
+
if (data2.success) return data2.data;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
throw new Error(error.error || error.message || "Failed to fetch avatar");
|
|
354
|
+
}
|
|
355
|
+
const data = await response.json();
|
|
356
|
+
if (!data.success) {
|
|
357
|
+
throw new Error(data.error || "Failed to fetch avatar");
|
|
358
|
+
}
|
|
359
|
+
return data.data;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Complete avatar upload flow (3-step process)
|
|
363
|
+
*/
|
|
364
|
+
async uploadAvatar(file) {
|
|
365
|
+
const validTypes = ["image/png", "image/jpeg", "image/jpg", "image/webp"];
|
|
366
|
+
if (!validTypes.includes(file.type)) {
|
|
367
|
+
throw new Error("Please select a PNG, JPEG, or WebP image");
|
|
368
|
+
}
|
|
369
|
+
const maxSize = 5 * 1024 * 1024;
|
|
370
|
+
if (file.size > maxSize) {
|
|
371
|
+
throw new Error("Image must be less than 5MB");
|
|
372
|
+
}
|
|
373
|
+
const prepareData = await this.prepareAvatarUpload(file.name, file.type);
|
|
374
|
+
await this.uploadToGCS(prepareData.upload_url, file);
|
|
375
|
+
const confirmData = await this.confirmAvatarUpload(prepareData.storage_path);
|
|
376
|
+
return confirmData.avatar_url;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// src/avatar-cache.ts
|
|
381
|
+
var AVATAR_CACHE_TTL = 24 * 60 * 60 * 1e3;
|
|
382
|
+
var avatarCache = /* @__PURE__ */ new Map();
|
|
383
|
+
function getCachedAvatar(userId) {
|
|
384
|
+
const cached = avatarCache.get(userId);
|
|
385
|
+
if (cached && Date.now() - cached.cachedAt < AVATAR_CACHE_TTL) {
|
|
386
|
+
return cached.url;
|
|
387
|
+
}
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
function setCachedAvatar(userId, url) {
|
|
391
|
+
avatarCache.set(userId, {
|
|
392
|
+
url,
|
|
393
|
+
cachedAt: Date.now()
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function clearAvatarCache(userId) {
|
|
397
|
+
if (userId) {
|
|
398
|
+
avatarCache.delete(userId);
|
|
399
|
+
} else {
|
|
400
|
+
avatarCache.clear();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function hasCachedAvatar(userId) {
|
|
404
|
+
const cached = avatarCache.get(userId);
|
|
405
|
+
return cached !== void 0 && Date.now() - cached.cachedAt < AVATAR_CACHE_TTL;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/PreferencesContext.tsx
|
|
409
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
410
|
+
var PreferencesContext = (0, import_react.createContext)(null);
|
|
411
|
+
function PreferencesProvider({
|
|
412
|
+
children,
|
|
413
|
+
config,
|
|
414
|
+
initialPreferences
|
|
415
|
+
}) {
|
|
416
|
+
const [preferences, setPreferences] = (0, import_react.useState)(initialPreferences || {});
|
|
417
|
+
const [loading, setLoading] = (0, import_react.useState)(!initialPreferences);
|
|
418
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
419
|
+
const [avatarUrl, setAvatarUrl] = (0, import_react.useState)(null);
|
|
420
|
+
const [hasAvatar, setHasAvatar] = (0, import_react.useState)(false);
|
|
421
|
+
const [avatarLoading, setAvatarLoading] = (0, import_react.useState)(true);
|
|
422
|
+
const [avatarError, setAvatarError] = (0, import_react.useState)(null);
|
|
423
|
+
const refreshAttemptRef = (0, import_react.useRef)(0);
|
|
424
|
+
const configRef = (0, import_react.useRef)(config);
|
|
425
|
+
configRef.current = config;
|
|
426
|
+
const serviceRef = (0, import_react.useRef)(null);
|
|
427
|
+
if (!serviceRef.current) {
|
|
428
|
+
serviceRef.current = new PreferencesService(config);
|
|
429
|
+
}
|
|
430
|
+
(0, import_react.useEffect)(() => {
|
|
431
|
+
if (serviceRef.current) {
|
|
432
|
+
serviceRef.current.updateConfig({
|
|
433
|
+
getToken: config.getToken,
|
|
434
|
+
onTokenInvalid: config.onTokenInvalid
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}, [config.getToken, config.onTokenInvalid]);
|
|
438
|
+
const refreshPreferences = (0, import_react.useCallback)(async () => {
|
|
439
|
+
setLoading(true);
|
|
440
|
+
setError(null);
|
|
441
|
+
try {
|
|
442
|
+
const service = serviceRef.current;
|
|
443
|
+
if (!service) throw new Error("Service not initialized");
|
|
444
|
+
const response = await service.getPreferences();
|
|
445
|
+
setPreferences(response.data.preferences);
|
|
446
|
+
} catch (err) {
|
|
447
|
+
const msg = err instanceof Error ? err.message : "Failed to load preferences";
|
|
448
|
+
setError(msg);
|
|
449
|
+
console.error("Preferences fetch error:", err);
|
|
450
|
+
} finally {
|
|
451
|
+
setLoading(false);
|
|
452
|
+
}
|
|
453
|
+
}, []);
|
|
454
|
+
const fetchAvatar = (0, import_react.useCallback)(async (skipCache = false) => {
|
|
455
|
+
const cacheKey = "me";
|
|
456
|
+
if (!skipCache) {
|
|
457
|
+
const cached = getCachedAvatar(cacheKey);
|
|
458
|
+
if (cached) {
|
|
459
|
+
setAvatarUrl(cached);
|
|
460
|
+
setHasAvatar(true);
|
|
461
|
+
setAvatarLoading(false);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
setAvatarLoading(true);
|
|
466
|
+
setAvatarError(null);
|
|
467
|
+
try {
|
|
468
|
+
const service = serviceRef.current;
|
|
469
|
+
if (!service) throw new Error("Service not initialized");
|
|
470
|
+
const data = await service.getMyAvatar();
|
|
471
|
+
if (data.has_avatar && data.avatar_url) {
|
|
472
|
+
setAvatarUrl(data.avatar_url);
|
|
473
|
+
setHasAvatar(true);
|
|
474
|
+
setCachedAvatar(cacheKey, data.avatar_url);
|
|
475
|
+
refreshAttemptRef.current = 0;
|
|
476
|
+
} else {
|
|
477
|
+
setAvatarUrl(null);
|
|
478
|
+
setHasAvatar(false);
|
|
479
|
+
clearAvatarCache(cacheKey);
|
|
480
|
+
}
|
|
481
|
+
} catch (err) {
|
|
482
|
+
const msg = err instanceof Error ? err.message : "Failed to load avatar";
|
|
483
|
+
setAvatarError(msg);
|
|
484
|
+
setAvatarUrl(null);
|
|
485
|
+
setHasAvatar(false);
|
|
486
|
+
if (!msg.includes("404") && !msg.includes("not found")) {
|
|
487
|
+
console.error("Avatar fetch error:", err);
|
|
488
|
+
}
|
|
489
|
+
} finally {
|
|
490
|
+
setAvatarLoading(false);
|
|
491
|
+
}
|
|
492
|
+
}, []);
|
|
493
|
+
const refreshAvatar = (0, import_react.useCallback)(async () => {
|
|
494
|
+
await fetchAvatar(true);
|
|
495
|
+
}, [fetchAvatar]);
|
|
496
|
+
const handleImageError = (0, import_react.useCallback)(() => {
|
|
497
|
+
if (refreshAttemptRef.current >= 2) {
|
|
498
|
+
console.warn("Avatar refresh limit reached");
|
|
499
|
+
setAvatarUrl(null);
|
|
500
|
+
setHasAvatar(false);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
refreshAttemptRef.current += 1;
|
|
504
|
+
console.log("Avatar image failed, refreshing (attempt", refreshAttemptRef.current, ")");
|
|
505
|
+
clearAvatarCache("me");
|
|
506
|
+
fetchAvatar(true);
|
|
507
|
+
}, [fetchAvatar]);
|
|
508
|
+
const updatePreferences = (0, import_react.useCallback)(async (prefs) => {
|
|
509
|
+
try {
|
|
510
|
+
const service = serviceRef.current;
|
|
511
|
+
if (!service) throw new Error("Service not initialized");
|
|
512
|
+
const response = await service.updatePreferences(prefs);
|
|
513
|
+
setPreferences(response.data.preferences);
|
|
514
|
+
} catch (err) {
|
|
515
|
+
const msg = err instanceof Error ? err.message : "Failed to update preferences";
|
|
516
|
+
setError(msg);
|
|
517
|
+
throw err;
|
|
518
|
+
}
|
|
519
|
+
}, []);
|
|
520
|
+
const uploadAvatar = (0, import_react.useCallback)(async (file) => {
|
|
521
|
+
const service = serviceRef.current;
|
|
522
|
+
if (!service) throw new Error("Service not initialized");
|
|
523
|
+
const url = await service.uploadAvatar(file);
|
|
524
|
+
clearAvatarCache("me");
|
|
525
|
+
await fetchAvatar(true);
|
|
526
|
+
return url;
|
|
527
|
+
}, [fetchAvatar]);
|
|
528
|
+
const clearAvatarCache2 = (0, import_react.useCallback)((userId) => {
|
|
529
|
+
clearAvatarCache(userId || "me");
|
|
530
|
+
}, []);
|
|
531
|
+
const applyTheme = (0, import_react.useCallback)((theme) => {
|
|
532
|
+
const root = document.documentElement;
|
|
533
|
+
if (theme === "dark") {
|
|
534
|
+
root.classList.add("dark");
|
|
535
|
+
} else if (theme === "light") {
|
|
536
|
+
root.classList.remove("dark");
|
|
537
|
+
} else {
|
|
538
|
+
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
539
|
+
if (prefersDark) {
|
|
540
|
+
root.classList.add("dark");
|
|
541
|
+
} else {
|
|
542
|
+
root.classList.remove("dark");
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}, []);
|
|
546
|
+
const syncOnLogin = (0, import_react.useCallback)(async () => {
|
|
547
|
+
try {
|
|
548
|
+
const service = serviceRef.current;
|
|
549
|
+
if (!service) return;
|
|
550
|
+
const response = await service.getPreferences();
|
|
551
|
+
const prefs = response.data.preferences;
|
|
552
|
+
setPreferences(prefs);
|
|
553
|
+
applyTheme(prefs.theme);
|
|
554
|
+
await fetchAvatar(true);
|
|
555
|
+
} catch (err) {
|
|
556
|
+
console.error("Failed to sync preferences on login:", err);
|
|
557
|
+
}
|
|
558
|
+
}, [applyTheme, fetchAvatar]);
|
|
559
|
+
(0, import_react.useEffect)(() => {
|
|
560
|
+
const hasToken = !!configRef.current.getToken();
|
|
561
|
+
if (!hasToken) {
|
|
562
|
+
setLoading(false);
|
|
563
|
+
setAvatarLoading(false);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (!initialPreferences) {
|
|
567
|
+
refreshPreferences();
|
|
568
|
+
}
|
|
569
|
+
fetchAvatar();
|
|
570
|
+
}, []);
|
|
571
|
+
(0, import_react.useEffect)(() => {
|
|
572
|
+
const hasToken = !!configRef.current.getToken();
|
|
573
|
+
if (hasToken && preferences.theme) {
|
|
574
|
+
applyTheme(preferences.theme);
|
|
575
|
+
}
|
|
576
|
+
}, [preferences.theme, applyTheme]);
|
|
577
|
+
const avatar = {
|
|
578
|
+
avatarUrl,
|
|
579
|
+
hasAvatar,
|
|
580
|
+
loading: avatarLoading,
|
|
581
|
+
error: avatarError,
|
|
582
|
+
refresh: refreshAvatar,
|
|
583
|
+
handleImageError
|
|
584
|
+
};
|
|
585
|
+
const value = {
|
|
586
|
+
preferences,
|
|
587
|
+
loading,
|
|
588
|
+
error,
|
|
589
|
+
avatar,
|
|
590
|
+
updatePreferences,
|
|
591
|
+
refreshPreferences,
|
|
592
|
+
uploadAvatar,
|
|
593
|
+
clearAvatarCache: clearAvatarCache2,
|
|
594
|
+
syncOnLogin
|
|
595
|
+
};
|
|
596
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PreferencesContext.Provider, { value, children });
|
|
597
|
+
}
|
|
598
|
+
function usePreferences() {
|
|
599
|
+
const context = (0, import_react.useContext)(PreferencesContext);
|
|
600
|
+
if (!context) {
|
|
601
|
+
throw new Error("usePreferences must be used within a PreferencesProvider");
|
|
602
|
+
}
|
|
603
|
+
return context;
|
|
604
|
+
}
|
|
605
|
+
function usePreferencesSafe() {
|
|
606
|
+
return (0, import_react.useContext)(PreferencesContext);
|
|
607
|
+
}
|
|
608
|
+
function useAvatar() {
|
|
609
|
+
const { avatar } = usePreferences();
|
|
610
|
+
return avatar;
|
|
611
|
+
}
|
|
612
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
613
|
+
0 && (module.exports = {
|
|
614
|
+
AVATAR_CACHE_TTL,
|
|
615
|
+
PreferencesProvider,
|
|
616
|
+
PreferencesService,
|
|
617
|
+
clearAvatarCache,
|
|
618
|
+
getCachedAvatar,
|
|
619
|
+
hasCachedAvatar,
|
|
620
|
+
setCachedAvatar,
|
|
621
|
+
useAvatar,
|
|
622
|
+
usePreferences,
|
|
623
|
+
usePreferencesSafe
|
|
624
|
+
});
|
|
625
|
+
//# sourceMappingURL=index.js.map
|