@oxyhq/core 9.2.4 → 10.0.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.
Files changed (64) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/i18n/index.js +22 -2
  3. package/dist/cjs/i18n/locales/en-US.json +4 -1
  4. package/dist/cjs/i18n/locales/es-ES.json +4 -1
  5. package/dist/cjs/i18n/locales/locales/en-US.json +4 -1
  6. package/dist/cjs/i18n/locales/locales/es-ES.json +4 -1
  7. package/dist/cjs/index.js +10 -5
  8. package/dist/cjs/mixins/OxyServices.assets.js +10 -6
  9. package/dist/cjs/mixins/OxyServices.language.js +11 -10
  10. package/dist/cjs/mixins/OxyServices.privacy.js +6 -0
  11. package/dist/cjs/mixins/OxyServices.user.js +40 -0
  12. package/dist/cjs/server/safeFetch.js +3 -3
  13. package/dist/cjs/session/SessionClient.js +1 -1
  14. package/dist/cjs/shared/utils/colorUtils.js +9 -9
  15. package/dist/cjs/utils/languageUtils.js +195 -158
  16. package/dist/cjs/utils/platform.js +9 -2
  17. package/dist/esm/.tsbuildinfo +1 -1
  18. package/dist/esm/i18n/index.js +22 -2
  19. package/dist/esm/i18n/locales/en-US.json +4 -1
  20. package/dist/esm/i18n/locales/es-ES.json +4 -1
  21. package/dist/esm/i18n/locales/locales/en-US.json +4 -1
  22. package/dist/esm/i18n/locales/locales/es-ES.json +4 -1
  23. package/dist/esm/index.js +1 -1
  24. package/dist/esm/mixins/OxyServices.assets.js +10 -6
  25. package/dist/esm/mixins/OxyServices.language.js +12 -11
  26. package/dist/esm/mixins/OxyServices.privacy.js +6 -0
  27. package/dist/esm/mixins/OxyServices.user.js +40 -0
  28. package/dist/esm/server/safeFetch.js +3 -3
  29. package/dist/esm/session/SessionClient.js +1 -1
  30. package/dist/esm/shared/utils/colorUtils.js +9 -9
  31. package/dist/esm/utils/languageUtils.js +189 -156
  32. package/dist/esm/utils/platform.js +9 -2
  33. package/dist/types/.tsbuildinfo +1 -1
  34. package/dist/types/index.d.ts +3 -3
  35. package/dist/types/mixins/OxyServices.language.d.ts +4 -4
  36. package/dist/types/mixins/OxyServices.user.d.ts +30 -0
  37. package/dist/types/models/interfaces.d.ts +8 -0
  38. package/dist/types/session/accountDialogController.d.ts +1 -1
  39. package/dist/types/utils/languageUtils.d.ts +86 -24
  40. package/package.json +2 -2
  41. package/src/HttpService.ts +2 -2
  42. package/src/crypto/keyManager.ts +3 -3
  43. package/src/crypto/recoveryPhrase.ts +1 -1
  44. package/src/i18n/index.ts +21 -2
  45. package/src/i18n/locales/en-US.json +4 -1
  46. package/src/i18n/locales/es-ES.json +4 -1
  47. package/src/index.ts +8 -2
  48. package/src/mixins/OxyServices.assets.ts +15 -11
  49. package/src/mixins/OxyServices.language.ts +31 -17
  50. package/src/mixins/OxyServices.privacy.ts +6 -0
  51. package/src/mixins/OxyServices.security.ts +1 -1
  52. package/src/mixins/OxyServices.user.ts +57 -0
  53. package/src/models/interfaces.ts +8 -0
  54. package/src/server/safeFetch.ts +3 -3
  55. package/src/session/SessionClient.ts +1 -1
  56. package/src/session/accountDialogController.ts +1 -1
  57. package/src/shared/utils/colorUtils.ts +11 -11
  58. package/src/shared/utils/errorUtils.ts +1 -1
  59. package/src/utils/__tests__/languageUtils.test.ts +219 -0
  60. package/src/utils/avatarUtils.ts +1 -1
  61. package/src/utils/languageUtils.ts +223 -162
  62. package/src/utils/platform.ts +21 -3
  63. package/src/utils/requestUtils.ts +3 -3
  64. package/src/utils/sessionUtils.ts +2 -2
@@ -9,6 +9,7 @@ import jaJP from './locales/ja-JP.json' with { type: "json" };
9
9
  import koKR from './locales/ko-KR.json' with { type: "json" };
10
10
  import zhCN from './locales/zh-CN.json' with { type: "json" };
11
11
  import arSA from './locales/ar-SA.json' with { type: "json" };
12
+ import { getBaseLanguage } from '../utils/languageUtils.js';
12
13
  const DICTS = {
13
14
  'en': enUS,
14
15
  'en-US': enUS,
@@ -34,11 +35,30 @@ const DICTS = {
34
35
  'ar-SA': arSA,
35
36
  };
36
37
  const FALLBACK = 'en-US';
38
+ /**
39
+ * Resolve a locale tag to the key of the dictionary that should serve it.
40
+ *
41
+ * Account locales are full BCP-47 tags (e.g. `es-MX`, `pt-BR`, `fr-CA`), but a
42
+ * translation dictionary is shipped per base language. Resolution order:
43
+ * 1. Exact dictionary for the tag (e.g. `es-ES`).
44
+ * 2. Dictionary for the base subtag (e.g. `es-MX` → `es`).
45
+ * 3. The English fallback.
46
+ */
47
+ function resolveLang(locale) {
48
+ if (locale) {
49
+ if (DICTS[locale])
50
+ return locale;
51
+ const base = getBaseLanguage(locale);
52
+ if (DICTS[base])
53
+ return base;
54
+ }
55
+ return FALLBACK;
56
+ }
37
57
  function getNested(obj, path) {
38
58
  return path.split('.').reduce((acc, key) => (acc && acc[key] != null ? acc[key] : undefined), obj);
39
59
  }
40
60
  export function translate(locale, key, vars) {
41
- const lang = locale && DICTS[locale] ? locale : FALLBACK;
61
+ const lang = resolveLang(locale);
42
62
  const dict = DICTS[lang] || DICTS[FALLBACK];
43
63
  let val = getNested(dict, key);
44
64
  // Per-key fallback to the English dictionary when a key is missing from the
@@ -58,6 +78,6 @@ export function translate(locale, key, vars) {
58
78
  return val;
59
79
  }
60
80
  export function hasKey(locale, key) {
61
- const lang = locale && DICTS[locale] ? locale : FALLBACK;
81
+ const lang = resolveLang(locale);
62
82
  return getNested(DICTS[lang], key) != null || getNested(DICTS[FALLBACK], key) != null;
63
83
  }
@@ -169,9 +169,12 @@
169
169
  },
170
170
  "language": {
171
171
  "title": "Language",
172
- "subtitle": "Choose your preferred language",
172
+ "subtitle": "Choose the languages you use. The first is your primary interface language.",
173
173
  "current": "Current Language",
174
+ "selected": "Your languages",
174
175
  "available": "Available Languages",
176
+ "primary": "Primary",
177
+ "remove": "Remove",
175
178
  "changed": "Language changed to {{lang}}",
176
179
  "saveFailed": "Failed to save language preference",
177
180
  "search": "Search languages"
@@ -123,9 +123,12 @@
123
123
  },
124
124
  "language": {
125
125
  "title": "Idioma",
126
- "subtitle": "Elige tu idioma preferido",
126
+ "subtitle": "Elige los idiomas que usas. El primero es tu idioma de interfaz principal.",
127
127
  "current": "Idioma actual",
128
+ "selected": "Tus idiomas",
128
129
  "available": "Idiomas disponibles",
130
+ "primary": "Principal",
131
+ "remove": "Quitar",
129
132
  "changed": "Idioma cambiado a {{lang}}",
130
133
  "saveFailed": "Error al guardar la preferencia de idioma",
131
134
  "search": "Buscar idiomas"
@@ -169,9 +169,12 @@
169
169
  },
170
170
  "language": {
171
171
  "title": "Language",
172
- "subtitle": "Choose your preferred language",
172
+ "subtitle": "Choose the languages you use. The first is your primary interface language.",
173
173
  "current": "Current Language",
174
+ "selected": "Your languages",
174
175
  "available": "Available Languages",
176
+ "primary": "Primary",
177
+ "remove": "Remove",
175
178
  "changed": "Language changed to {{lang}}",
176
179
  "saveFailed": "Failed to save language preference",
177
180
  "search": "Search languages"
@@ -123,9 +123,12 @@
123
123
  },
124
124
  "language": {
125
125
  "title": "Idioma",
126
- "subtitle": "Elige tu idioma preferido",
126
+ "subtitle": "Elige los idiomas que usas. El primero es tu idioma de interfaz principal.",
127
127
  "current": "Idioma actual",
128
+ "selected": "Tus idiomas",
128
129
  "available": "Idiomas disponibles",
130
+ "primary": "Principal",
131
+ "remove": "Quitar",
129
132
  "changed": "Idioma cambiado a {{lang}}",
130
133
  "saveFailed": "Error al guardar la preferencia de idioma",
131
134
  "search": "Buscar idiomas"
package/dist/esm/index.js CHANGED
@@ -75,7 +75,7 @@ export { TopicType, TopicSource } from './models/Topic.js';
75
75
  // ---------------------------------------------------------------------------
76
76
  // Languages
77
77
  // ---------------------------------------------------------------------------
78
- export { SUPPORTED_LANGUAGES, getLanguageMetadata, getLanguageName, getNativeLanguageName, normalizeLanguageCode, isRTLLocale, } from './utils/languageUtils.js';
78
+ export { SUPPORTED_LANGUAGES, FALLBACK_LOCALE, getBaseLanguage, normalizeLocale, isSupportedLocale, getLanguageMetadata, getLanguageName, getNativeLanguageName, isRTLLocale, getUserLanguages, getPrimaryLanguage, } from './utils/languageUtils.js';
79
79
  // ---------------------------------------------------------------------------
80
80
  // Platform detection
81
81
  // ---------------------------------------------------------------------------
@@ -365,14 +365,18 @@ export function OxyServicesAssetsMixin(Base) {
365
365
  errorMessage = error.message || errorMessage;
366
366
  }
367
367
  else if (error && typeof error === 'object') {
368
- if ('message' in error) {
369
- errorMessage = String(error.message) || errorMessage;
368
+ const errObj = error;
369
+ if ('message' in errObj) {
370
+ errorMessage = String(errObj.message) || errorMessage;
370
371
  }
371
- else if ('error' in error && typeof error.error === 'string') {
372
- errorMessage = error.error;
372
+ else if (typeof errObj.error === 'string') {
373
+ errorMessage = errObj.error;
373
374
  }
374
- else if ('data' in error && error.data?.message) {
375
- errorMessage = String(error.data.message);
375
+ else if (errObj.data && typeof errObj.data === 'object') {
376
+ const dataObj = errObj.data;
377
+ if (dataObj.message) {
378
+ errorMessage = String(dataObj.message);
379
+ }
376
380
  }
377
381
  }
378
382
  else if (error) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Language Methods Mixin
3
3
  */
4
- import { normalizeLanguageCode, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils.js';
4
+ import { normalizeLocale, getPrimaryLanguage, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils.js';
5
5
  import { loadAsyncStorage } from '@oxyhq/protocol';
6
6
  import { isDev } from '../shared/utils/debugUtils.js';
7
7
  export function OxyServicesLanguageMixin(Base) {
@@ -55,29 +55,30 @@ export function OxyServicesLanguageMixin(Base) {
55
55
  }
56
56
  }
57
57
  /**
58
- * Get the current language from storage or user profile
58
+ * Get the current locale from the user profile or local storage.
59
59
  * @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
60
- * @returns The current language code (e.g., 'en-US') or null if not set
60
+ * @returns The current BCP-47 locale (e.g., 'en-US') or null if not set
61
61
  */
62
62
  async getCurrentLanguage(storageKeyPrefix = 'oxy_session') {
63
63
  try {
64
- // First try to get from user profile if authenticated
64
+ // First try the authenticated user's primary account locale.
65
65
  try {
66
66
  const user = await this.getCurrentUser();
67
- const userLanguage = user?.language;
68
- if (userLanguage) {
69
- return normalizeLanguageCode(userLanguage) || userLanguage;
67
+ const primary = getPrimaryLanguage(user);
68
+ if (primary) {
69
+ return primary;
70
70
  }
71
71
  }
72
- catch (e) {
73
- // User not authenticated or error, continue to storage
72
+ catch {
73
+ // Not authenticated or the profile fetch failed — fall through to the
74
+ // locally stored preference below.
74
75
  }
75
- // Fall back to storage
76
+ // Fall back to the locally stored locale preference.
76
77
  const storage = await this.getStorage();
77
78
  const storageKey = `${storageKeyPrefix}_language`;
78
79
  const storedLanguage = await storage.getItem(storageKey);
79
80
  if (storedLanguage) {
80
- return normalizeLanguageCode(storedLanguage) || storedLanguage;
81
+ return normalizeLocale(storedLanguage) ?? storedLanguage;
81
82
  }
82
83
  return null;
83
84
  }
@@ -70,6 +70,9 @@ export function OxyServicesPrivacyMixin(Base) {
70
70
  cache: false,
71
71
  });
72
72
  this.clearCacheEntry('GET:/privacy/blocked');
73
+ // The block changed the viewer's graph (`blockedIds`) — bust the cached
74
+ // consolidated `GET /users/me/graph` so the next read reflects it.
75
+ this.clearCacheEntry('GET:/users/me/graph');
73
76
  return result;
74
77
  }
75
78
  catch (error) {
@@ -93,6 +96,9 @@ export function OxyServicesPrivacyMixin(Base) {
93
96
  cache: false,
94
97
  });
95
98
  this.clearCacheEntry('GET:/privacy/blocked');
99
+ // Symmetric to blockUser: the unblock changed the viewer's `blockedIds`,
100
+ // so bust the consolidated `GET /users/me/graph` cache.
101
+ this.clearCacheEntry('GET:/users/me/graph');
96
102
  return result;
97
103
  }
98
104
  catch (error) {
@@ -509,6 +509,10 @@ export function OxyServicesUserMixin(Base) {
509
509
  try {
510
510
  const result = await this.makeRequest('POST', `/users/${userId}/follow`, undefined, { cache: false });
511
511
  this.clearCacheEntry(`GET:/users/${userId}/follow-status`);
512
+ // The follow changed the viewer's graph — bust the cached consolidated
513
+ // `GET /users/me/graph` so the next read reflects the new following/
514
+ // mutual set instead of the stale pre-write snapshot.
515
+ this.clearCacheEntry('GET:/users/me/graph');
512
516
  return result;
513
517
  }
514
518
  catch (error) {
@@ -533,6 +537,8 @@ export function OxyServicesUserMixin(Base) {
533
537
  for (const id of userIds) {
534
538
  this.clearCacheEntry(`GET:/users/${id}/follow-status`);
535
539
  }
540
+ // The batch changed the viewer's graph — bust the consolidated cache.
541
+ this.clearCacheEntry('GET:/users/me/graph');
536
542
  return result;
537
543
  }
538
544
  catch (error) {
@@ -557,6 +563,8 @@ export function OxyServicesUserMixin(Base) {
557
563
  for (const id of userIds) {
558
564
  this.clearCacheEntry(`GET:/users/${id}/follow-status`);
559
565
  }
566
+ // The batch changed the viewer's graph — bust the consolidated cache.
567
+ this.clearCacheEntry('GET:/users/me/graph');
560
568
  return result;
561
569
  }
562
570
  catch (error) {
@@ -571,6 +579,8 @@ export function OxyServicesUserMixin(Base) {
571
579
  const result = await this.makeRequest('DELETE', `/users/${userId}/follow`, undefined, { cache: false });
572
580
  // Bust the cached follow-status so a remount reads fresh truth (see `followUser`).
573
581
  this.clearCacheEntry(`GET:/users/${userId}/follow-status`);
582
+ // The unfollow changed the viewer's graph — bust the consolidated cache.
583
+ this.clearCacheEntry('GET:/users/me/graph');
574
584
  return result;
575
585
  }
576
586
  catch (error) {
@@ -702,6 +712,36 @@ export function OxyServicesUserMixin(Base) {
702
712
  throw this.handleError(error);
703
713
  }
704
714
  }
715
+ /**
716
+ * Get the authenticated VIEWER's OWN social graph — the accounts they follow,
717
+ * the subset who follow back (mutuals), and the accounts they have blocked —
718
+ * as ONE ids-only payload. The viewer is derived server-side from the SDK's
719
+ * auth token (never a param).
720
+ *
721
+ * Consolidates what were three separate round trips (`getUserFollowing` /
722
+ * `getMutualUserIds` / `getBlockedUsers`) into a single request so a consumer
723
+ * can prime its whole viewer graph at once. Mirrors {@link getMutualUserIds}'s
724
+ * caching posture (2-minute identity-scoped cache); the follow/unfollow/block/
725
+ * unblock write methods bust this entry so a local mutation is reflected
726
+ * immediately. An anonymous caller resolves to empty lists.
727
+ */
728
+ async getViewerGraph() {
729
+ try {
730
+ const response = await this.makeRequest('GET', '/users/me/graph', undefined, {
731
+ cache: true,
732
+ cacheTTL: 2 * 60 * 1000, // 2 minutes cache
733
+ });
734
+ const graph = response.data;
735
+ return {
736
+ followingIds: graph?.followingIds || [],
737
+ mutualIds: graph?.mutualIds || [],
738
+ blockedIds: graph?.blockedIds || [],
739
+ };
740
+ }
741
+ catch (error) {
742
+ throw this.handleError(error);
743
+ }
744
+ }
705
745
  /**
706
746
  * Get notifications
707
747
  */
@@ -155,7 +155,7 @@ function ipv6ToGroups(ip) {
155
155
  for (const part of segment.split(':')) {
156
156
  if (!/^[0-9a-fA-F]{1,4}$/.test(part))
157
157
  return null;
158
- groups.push(parseInt(part, 16));
158
+ groups.push(Number.parseInt(part, 16));
159
159
  }
160
160
  return groups;
161
161
  };
@@ -205,8 +205,8 @@ function extractEmbeddedIpv4(ip) {
205
205
  // Hex form "::ffff:0102:0304" → 1.2.3.4
206
206
  const hexMapped = lower.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
207
207
  if (hexMapped) {
208
- const hi = parseInt(hexMapped[1], 16);
209
- const lo = parseInt(hexMapped[2], 16);
208
+ const hi = Number.parseInt(hexMapped[1], 16);
209
+ const lo = Number.parseInt(hexMapped[2], 16);
210
210
  return `${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`;
211
211
  }
212
212
  return null;
@@ -274,7 +274,7 @@ export class SessionClient {
274
274
  transports: ['websocket'],
275
275
  autoConnect: true,
276
276
  reconnection: true,
277
- reconnectionAttempts: Infinity,
277
+ reconnectionAttempts: Number.POSITIVE_INFINITY,
278
278
  reconnectionDelay: 1000,
279
279
  reconnectionDelayMax: 10000,
280
280
  auth: (cb) => {
@@ -21,9 +21,9 @@
21
21
  */
22
22
  export const darkenColor = (color, factor = 0.6) => {
23
23
  const hex = color.replace('#', '');
24
- const r = parseInt(hex.substring(0, 2), 16);
25
- const g = parseInt(hex.substring(2, 4), 16);
26
- const b = parseInt(hex.substring(4, 6), 16);
24
+ const r = Number.parseInt(hex.substring(0, 2), 16);
25
+ const g = Number.parseInt(hex.substring(2, 4), 16);
26
+ const b = Number.parseInt(hex.substring(4, 6), 16);
27
27
  const newR = Math.max(0, Math.round(r * (1 - factor)));
28
28
  const newG = Math.max(0, Math.round(g * (1 - factor)));
29
29
  const newB = Math.max(0, Math.round(b * (1 - factor)));
@@ -43,9 +43,9 @@ export const darkenColor = (color, factor = 0.6) => {
43
43
  */
44
44
  export const lightenColor = (color, factor = 0.3) => {
45
45
  const hex = color.replace('#', '');
46
- const r = parseInt(hex.substring(0, 2), 16);
47
- const g = parseInt(hex.substring(2, 4), 16);
48
- const b = parseInt(hex.substring(4, 6), 16);
46
+ const r = Number.parseInt(hex.substring(0, 2), 16);
47
+ const g = Number.parseInt(hex.substring(2, 4), 16);
48
+ const b = Number.parseInt(hex.substring(4, 6), 16);
49
49
  const newR = Math.min(255, Math.round(r + (255 - r) * factor));
50
50
  const newG = Math.min(255, Math.round(g + (255 - g) * factor));
51
51
  const newB = Math.min(255, Math.round(b + (255 - b) * factor));
@@ -66,9 +66,9 @@ export const hexToRgb = (hex) => {
66
66
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
67
67
  return result
68
68
  ? {
69
- r: parseInt(result[1], 16),
70
- g: parseInt(result[2], 16),
71
- b: parseInt(result[3], 16),
69
+ r: Number.parseInt(result[1], 16),
70
+ g: Number.parseInt(result[2], 16),
71
+ b: Number.parseInt(result[3], 16),
72
72
  }
73
73
  : null;
74
74
  };