@oxyhq/core 9.2.4 → 10.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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/i18n/index.js +22 -2
- package/dist/cjs/i18n/locales/en-US.json +4 -1
- package/dist/cjs/i18n/locales/es-ES.json +4 -1
- package/dist/cjs/i18n/locales/locales/en-US.json +4 -1
- package/dist/cjs/i18n/locales/locales/es-ES.json +4 -1
- package/dist/cjs/index.js +16 -5
- package/dist/cjs/mixins/OxyServices.assets.js +10 -6
- package/dist/cjs/mixins/OxyServices.language.js +11 -10
- package/dist/cjs/mixins/OxyServices.privacy.js +6 -0
- package/dist/cjs/mixins/OxyServices.user.js +40 -0
- package/dist/cjs/server/safeFetch.js +3 -3
- package/dist/cjs/session/SessionClient.js +1 -1
- package/dist/cjs/shared/utils/colorUtils.js +9 -9
- package/dist/cjs/utils/languageUtils.js +195 -158
- package/dist/cjs/utils/platform.js +9 -2
- package/dist/cjs/utils/textNormalization.js +168 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/i18n/index.js +22 -2
- package/dist/esm/i18n/locales/en-US.json +4 -1
- package/dist/esm/i18n/locales/es-ES.json +4 -1
- package/dist/esm/i18n/locales/locales/en-US.json +4 -1
- package/dist/esm/i18n/locales/locales/es-ES.json +4 -1
- package/dist/esm/index.js +5 -1
- package/dist/esm/mixins/OxyServices.assets.js +10 -6
- package/dist/esm/mixins/OxyServices.language.js +12 -11
- package/dist/esm/mixins/OxyServices.privacy.js +6 -0
- package/dist/esm/mixins/OxyServices.user.js +40 -0
- package/dist/esm/server/safeFetch.js +3 -3
- package/dist/esm/session/SessionClient.js +1 -1
- package/dist/esm/shared/utils/colorUtils.js +9 -9
- package/dist/esm/utils/languageUtils.js +189 -156
- package/dist/esm/utils/platform.js +9 -2
- package/dist/esm/utils/textNormalization.js +164 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +4 -3
- package/dist/types/mixins/OxyServices.language.d.ts +4 -4
- package/dist/types/mixins/OxyServices.user.d.ts +30 -0
- package/dist/types/models/interfaces.d.ts +14 -0
- package/dist/types/session/accountDialogController.d.ts +1 -1
- package/dist/types/utils/languageUtils.d.ts +86 -24
- package/dist/types/utils/textNormalization.d.ts +100 -0
- package/package.json +3 -3
- package/src/HttpService.ts +2 -2
- package/src/crypto/keyManager.ts +3 -3
- package/src/crypto/recoveryPhrase.ts +1 -1
- package/src/i18n/index.ts +21 -2
- package/src/i18n/locales/en-US.json +4 -1
- package/src/i18n/locales/es-ES.json +4 -1
- package/src/index.ts +16 -2
- package/src/mixins/OxyServices.assets.ts +15 -11
- package/src/mixins/OxyServices.language.ts +31 -17
- package/src/mixins/OxyServices.privacy.ts +6 -0
- package/src/mixins/OxyServices.security.ts +1 -1
- package/src/mixins/OxyServices.user.ts +57 -0
- package/src/models/interfaces.ts +14 -0
- package/src/server/safeFetch.ts +3 -3
- package/src/session/SessionClient.ts +1 -1
- package/src/session/accountDialogController.ts +1 -1
- package/src/shared/utils/colorUtils.ts +11 -11
- package/src/shared/utils/errorUtils.ts +1 -1
- package/src/utils/__tests__/languageUtils.test.ts +219 -0
- package/src/utils/__tests__/textNormalization.test.ts +196 -0
- package/src/utils/avatarUtils.ts +1 -1
- package/src/utils/languageUtils.ts +223 -162
- package/src/utils/platform.ts +21 -3
- package/src/utils/requestUtils.ts +3 -3
- package/src/utils/sessionUtils.ts +2 -2
- package/src/utils/textNormalization.ts +173 -0
package/dist/types/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type { ServiceTokenResponse } from './mixins/OxyServices.auth';
|
|
|
26
26
|
export type { CommonsSignInHandle, CommonsSignInStatus, CommonsApprovalInfo, CommonsSignInActionResult, } from './mixins/OxyServices.auth';
|
|
27
27
|
export type { ServiceApp, ServiceActingAsVerification } from './mixins/OxyServices.utility';
|
|
28
28
|
export type { ContactDiscoveryMatch, ContactDiscoveryResponse, } from './mixins/OxyServices.contacts';
|
|
29
|
-
export type { BulkFollowEntry, BulkFollowResult, BulkUnfollowEntry, BulkUnfollowResult, } from './mixins/OxyServices.user';
|
|
29
|
+
export type { BulkFollowEntry, BulkFollowResult, BulkUnfollowEntry, BulkUnfollowResult, ViewerGraph, } from './mixins/OxyServices.user';
|
|
30
30
|
export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData';
|
|
31
31
|
export { getNormalizedUserId, normalizeUserIdentity, normalizeUserIdentityOrNull, } from './utils/userIdentity';
|
|
32
32
|
export { getCanonicalUserHandle, getNormalizedUserHandle, } from './utils/userHandle';
|
|
@@ -58,8 +58,8 @@ export type { OxyConfig, PrivacySettings, NotificationPreferences, UserPreferenc
|
|
|
58
58
|
export { SECURITY_EVENT_SEVERITY_MAP } from './models/interfaces';
|
|
59
59
|
export { TopicType, TopicSource } from './models/Topic';
|
|
60
60
|
export type { TopicData, TopicTranslation } from './models/Topic';
|
|
61
|
-
export { SUPPORTED_LANGUAGES, getLanguageMetadata, getLanguageName, getNativeLanguageName,
|
|
62
|
-
export type {
|
|
61
|
+
export { SUPPORTED_LANGUAGES, FALLBACK_LOCALE, getBaseLanguage, normalizeLocale, isSupportedLocale, getLanguageMetadata, getLanguageName, getNativeLanguageName, isRTLLocale, getUserLanguages, getPrimaryLanguage, } from './utils/languageUtils';
|
|
62
|
+
export type { SupportedLanguage } from './utils/languageUtils';
|
|
63
63
|
export { getPlatformOS, setPlatformOS, isWeb, isNative, isIOS, isAndroid, isWebBrowser, } from './utils/platform';
|
|
64
64
|
export type { PlatformOS } from './utils/platform';
|
|
65
65
|
export { darkenColor, lightenColor, hexToRgb, rgbToHex, withOpacity, isLightColor, getContrastTextColor, } from './shared/utils/colorUtils';
|
|
@@ -75,6 +75,7 @@ export type { PaginationParams, ApiResponse, ErrorResponse, } from './utils/apiU
|
|
|
75
75
|
export { ErrorCodes, createApiError, handleHttpError, validateRequiredFields, } from './utils/errorUtils';
|
|
76
76
|
export { retryAsync } from './utils/asyncUtils';
|
|
77
77
|
export { EMAIL_REGEX, USERNAME_REGEX, PASSWORD_REGEX, isValidEmail, isValidUsername, isValidPassword, isValidDisplayName, isRequiredString, isRequiredNumber, isRequiredBoolean, isValidArray, isValidObject, isValidUUID, isValidURL, isValidDate, isValidFileSize, isValidFileType, sanitizeString, sanitizeHTML, isValidObjectId, validateAndSanitizeUserInput, } from './utils/validationUtils';
|
|
78
|
+
export { normalizeInlineText, normalizeMultilineText, } from './utils/textNormalization';
|
|
78
79
|
export { logger, LogLevel, logAuth, logApi, logSession, logUser, logDevice, logPayment, logPerformance, } from './utils/loggerUtils';
|
|
79
80
|
export type { LogContext } from './utils/loggerUtils';
|
|
80
81
|
export { updateAvatarVisibility } from './utils/avatarUtils';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SupportedLanguage } from '../utils/languageUtils';
|
|
2
2
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
3
3
|
export declare function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base: T): {
|
|
4
4
|
new (...args: any[]): {
|
|
@@ -11,9 +11,9 @@ export declare function OxyServicesLanguageMixin<T extends typeof OxyServicesBas
|
|
|
11
11
|
removeItem: (key: string) => Promise<void>;
|
|
12
12
|
}>;
|
|
13
13
|
/**
|
|
14
|
-
* Get the current
|
|
14
|
+
* Get the current locale from the user profile or local storage.
|
|
15
15
|
* @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
|
|
16
|
-
* @returns The current
|
|
16
|
+
* @returns The current BCP-47 locale (e.g., 'en-US') or null if not set
|
|
17
17
|
*/
|
|
18
18
|
getCurrentLanguage(storageKeyPrefix?: string): Promise<string | null>;
|
|
19
19
|
/**
|
|
@@ -21,7 +21,7 @@ export declare function OxyServicesLanguageMixin<T extends typeof OxyServicesBas
|
|
|
21
21
|
* @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
|
|
22
22
|
* @returns Language metadata object or null if not set
|
|
23
23
|
*/
|
|
24
|
-
getCurrentLanguageMetadata(storageKeyPrefix?: string): Promise<
|
|
24
|
+
getCurrentLanguageMetadata(storageKeyPrefix?: string): Promise<SupportedLanguage | null>;
|
|
25
25
|
/**
|
|
26
26
|
* Get the current language name (e.g., 'English')
|
|
27
27
|
* @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
|
|
@@ -21,6 +21,22 @@ export interface BulkFollowResult {
|
|
|
21
21
|
/** Number of users newly followed by this request. */
|
|
22
22
|
followedCount: number;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* The authenticated viewer's OWN social graph, ids-only — the response of
|
|
26
|
+
* `GET /users/me/graph`. Consolidates the accounts the viewer follows, the
|
|
27
|
+
* subset who follow back (mutuals), and the accounts the viewer has blocked
|
|
28
|
+
* into one payload so a consumer can fetch its whole viewer graph in a single
|
|
29
|
+
* round trip instead of three. Each list is server-bounded; bare ids only (no
|
|
30
|
+
* hydrated DTOs) because the consumer hydrates/ranks itself.
|
|
31
|
+
*/
|
|
32
|
+
export interface ViewerGraph {
|
|
33
|
+
/** Accounts the viewer follows (most-recent first, bounded). */
|
|
34
|
+
followingIds: string[];
|
|
35
|
+
/** Accounts the viewer follows that ALSO follow the viewer back (bounded). */
|
|
36
|
+
mutualIds: string[];
|
|
37
|
+
/** Accounts the viewer has blocked (bounded). */
|
|
38
|
+
blockedIds: string[];
|
|
39
|
+
}
|
|
24
40
|
/** Per-user outcome returned by `POST /users/unfollow/bulk`. */
|
|
25
41
|
export interface BulkUnfollowEntry {
|
|
26
42
|
/** The user ID that was processed. */
|
|
@@ -383,6 +399,20 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
|
|
|
383
399
|
getFollowsOfFollowsIds(params?: {
|
|
384
400
|
limit?: number;
|
|
385
401
|
}): Promise<string[]>;
|
|
402
|
+
/**
|
|
403
|
+
* Get the authenticated VIEWER's OWN social graph — the accounts they follow,
|
|
404
|
+
* the subset who follow back (mutuals), and the accounts they have blocked —
|
|
405
|
+
* as ONE ids-only payload. The viewer is derived server-side from the SDK's
|
|
406
|
+
* auth token (never a param).
|
|
407
|
+
*
|
|
408
|
+
* Consolidates what were three separate round trips (`getUserFollowing` /
|
|
409
|
+
* `getMutualUserIds` / `getBlockedUsers`) into a single request so a consumer
|
|
410
|
+
* can prime its whole viewer graph at once. Mirrors {@link getMutualUserIds}'s
|
|
411
|
+
* caching posture (2-minute identity-scoped cache); the follow/unfollow/block/
|
|
412
|
+
* unblock write methods bust this entry so a local mutation is reflected
|
|
413
|
+
* immediately. An anonymous caller resolves to empty lists.
|
|
414
|
+
*/
|
|
415
|
+
getViewerGraph(): Promise<ViewerGraph>;
|
|
386
416
|
/**
|
|
387
417
|
* Get notifications
|
|
388
418
|
*/
|
|
@@ -93,6 +93,12 @@ export interface User {
|
|
|
93
93
|
*/
|
|
94
94
|
name: UserNameResponse;
|
|
95
95
|
bio?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Longer public profile text. Emitted alongside `bio` by every user DTO the
|
|
98
|
+
* API produces (single profile, `getUsersByIds`, follower/following/mutual
|
|
99
|
+
* lists, profile search) — the two are separate fields on the User document.
|
|
100
|
+
*/
|
|
101
|
+
description?: string;
|
|
96
102
|
phone?: string;
|
|
97
103
|
address?: string;
|
|
98
104
|
birthday?: string;
|
|
@@ -131,6 +137,14 @@ export interface User {
|
|
|
131
137
|
managedBy?: string;
|
|
132
138
|
/** Real-estate taxonomy when this user is a `kind: 'organization'` account. */
|
|
133
139
|
organizationCategory?: OrganizationCategory;
|
|
140
|
+
/**
|
|
141
|
+
* The account's languages as full BCP-47 locales (`language-REGION`, e.g.
|
|
142
|
+
* `en-US`, `es-MX`, `pt-BR`), ordered with the PRIMARY (UI) locale first.
|
|
143
|
+
* `languages[0]` is the primary locale — there is no singular `language`
|
|
144
|
+
* field. Resolve via `getUserLanguages` / `getPrimaryLanguage`, which
|
|
145
|
+
* normalize, validate against the supported catalog, and de-duplicate.
|
|
146
|
+
*/
|
|
147
|
+
languages?: string[];
|
|
134
148
|
notificationPreferences?: NotificationPreferences;
|
|
135
149
|
userPreferences?: UserPreferences;
|
|
136
150
|
[key: string]: unknown;
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import type { OxyServices } from '../OxyServices';
|
|
29
29
|
import type { SessionLoginResponse, MinimalUserData } from '../models/session';
|
|
30
|
-
import { SessionClient } from './SessionClient';
|
|
30
|
+
import type { SessionClient } from './SessionClient';
|
|
31
31
|
import { type SwitchableAccount } from './accountProjection';
|
|
32
32
|
/** The dialog's top-level view. */
|
|
33
33
|
export type AccountDialogView = 'accounts' | 'signin' | 'qr' | 'add';
|
|
@@ -1,38 +1,100 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Locale utilities for OxyServices.
|
|
3
|
+
*
|
|
4
|
+
* The Oxy platform models account languages as full BCP-47 locales
|
|
5
|
+
* (`language-REGION`, e.g. `es-ES`, `es-MX`, `pt-BR`). Region is significant:
|
|
6
|
+
* "Spanish (Spain)" is a different locale than "Spanish (Mexico)". A user's
|
|
7
|
+
* account locales live on `User.languages` as an ordered list with the PRIMARY
|
|
8
|
+
* (UI) locale first — there is no singular `language` field.
|
|
9
|
+
*
|
|
10
|
+
* This module is the single source of truth for the supported-locale catalog
|
|
11
|
+
* and every locale operation the SDK exposes: parsing base subtags, canonical
|
|
12
|
+
* normalization, validation, metadata lookup, and resolving a user's locales.
|
|
13
|
+
* It is pure and side-effect free.
|
|
4
14
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
15
|
+
import type { User } from '../models/interfaces';
|
|
16
|
+
/**
|
|
17
|
+
* A supported BCP-47 locale in the Oxy catalog.
|
|
18
|
+
*/
|
|
19
|
+
export interface SupportedLanguage {
|
|
20
|
+
/** Canonical BCP-47 locale tag, `language-REGION` (e.g. `'es-ES'`). */
|
|
21
|
+
code: string;
|
|
22
|
+
/** ISO 639-1 base language subtag, lowercased (e.g. `'es'`). */
|
|
23
|
+
language: string;
|
|
24
|
+
/** ISO 3166-1 alpha-2 region subtag, uppercased (e.g. `'ES'`). */
|
|
25
|
+
region: string;
|
|
26
|
+
/** English display name, `'Language (Region)'` (e.g. `'Spanish (Spain)'`). */
|
|
7
27
|
name: string;
|
|
28
|
+
/** Endonym — the name as written in the locale itself (e.g. `'Español (España)'`). */
|
|
8
29
|
nativeName: string;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
color: string;
|
|
30
|
+
/** `true` when the locale is written right-to-left. Omitted for LTR locales. */
|
|
31
|
+
rtl?: boolean;
|
|
12
32
|
}
|
|
13
|
-
export declare const SUPPORTED_LANGUAGES: LanguageMetadata[];
|
|
14
33
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
34
|
+
* The supported-locale catalog. Ordered by prominence for display; the order
|
|
35
|
+
* is not otherwise significant. Every entry is a full `language-REGION` tag,
|
|
36
|
+
* with the base subtag lowercased and the region uppercased.
|
|
37
|
+
*/
|
|
38
|
+
export declare const SUPPORTED_LANGUAGES: readonly SupportedLanguage[];
|
|
39
|
+
/** Canonical fallback locale used when a display value cannot be resolved. */
|
|
40
|
+
export declare const FALLBACK_LOCALE = "en-US";
|
|
41
|
+
/**
|
|
42
|
+
* Extract the base language subtag from a locale, lowercased.
|
|
43
|
+
*
|
|
44
|
+
* Tolerant of bare base subtags and of extra subtags (script/variant):
|
|
45
|
+
* `'es-ES'` → `'es'`, `'es'` → `'es'`, `'zh-Hant-TW'` → `'zh'`. Returns an
|
|
46
|
+
* empty string for empty input.
|
|
47
|
+
*/
|
|
48
|
+
export declare function getBaseLanguage(locale: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Normalize a locale string to its canonical `language-REGION` form when it is
|
|
51
|
+
* a supported locale, otherwise `undefined`.
|
|
52
|
+
*
|
|
53
|
+
* Canonicalization lowercases the base subtag and uppercases the region
|
|
54
|
+
* subtag (`'es-es'` → `'es-ES'`, `'EN-us'` → `'en-US'`) and validates the
|
|
55
|
+
* result against {@link SUPPORTED_LANGUAGES}. A bare base subtag (`'es'`) has
|
|
56
|
+
* no region and is therefore not a locale — it returns `undefined`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function normalizeLocale(input: string): string | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Whether `input` resolves to a supported BCP-47 locale.
|
|
61
|
+
*/
|
|
62
|
+
export declare function isSupportedLocale(input: string): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Resolve catalog metadata for a locale.
|
|
65
|
+
*
|
|
66
|
+
* @param code - A locale tag (any case; e.g. `'es-ES'`, `'es-es'`).
|
|
67
|
+
* @returns The {@link SupportedLanguage} entry, or `null` when the tag is empty
|
|
68
|
+
* or not a supported locale.
|
|
18
69
|
*/
|
|
19
|
-
export declare function getLanguageMetadata(
|
|
70
|
+
export declare function getLanguageMetadata(code: string | null | undefined): SupportedLanguage | null;
|
|
20
71
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* @returns Language name (e.g., 'English') or the code if not found
|
|
72
|
+
* English display name for a locale (e.g. `'Spanish (Spain)'`).
|
|
73
|
+
* Falls back to the input tag, then {@link FALLBACK_LOCALE}.
|
|
24
74
|
*/
|
|
25
|
-
export declare function getLanguageName(
|
|
75
|
+
export declare function getLanguageName(code: string | null | undefined): string;
|
|
26
76
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* @returns Native language name (e.g., 'Español') or the code if not found
|
|
77
|
+
* Native display name (endonym) for a locale (e.g. `'Español (España)'`).
|
|
78
|
+
* Falls back to the input tag, then {@link FALLBACK_LOCALE}.
|
|
30
79
|
*/
|
|
31
|
-
export declare function getNativeLanguageName(
|
|
80
|
+
export declare function getNativeLanguageName(code: string | null | undefined): string;
|
|
32
81
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* @returns Normalized BCP-47 language code
|
|
82
|
+
* Whether a locale (or bare base subtag) is written right-to-left. Unknown
|
|
83
|
+
* tags are treated as left-to-right.
|
|
36
84
|
*/
|
|
37
|
-
export declare function normalizeLanguageCode(lang?: string | null): string;
|
|
38
85
|
export declare function isRTLLocale(locale?: string | null): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Resolve the ordered list of account locales for a user, primary first.
|
|
88
|
+
*
|
|
89
|
+
* Reads `user.languages` (the only source — there is no singular `language`
|
|
90
|
+
* field), then normalizes each entry to its canonical `language-REGION` form,
|
|
91
|
+
* drops non-string and unsupported entries, and de-duplicates while preserving
|
|
92
|
+
* first-seen order. Pure and side-effect free — never throws on bad input.
|
|
93
|
+
*/
|
|
94
|
+
export declare function getUserLanguages(user: Pick<User, 'languages'> | null | undefined): string[];
|
|
95
|
+
/**
|
|
96
|
+
* Resolve the single PRIMARY locale for a user (the first entry from
|
|
97
|
+
* {@link getUserLanguages}), or `undefined` when the user has no supported
|
|
98
|
+
* locale.
|
|
99
|
+
*/
|
|
100
|
+
export declare function getPrimaryLanguage(user: Pick<User, 'languages'> | null | undefined): string | undefined;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical text normalization for the Oxy ecosystem.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS
|
|
5
|
+
* ---------------
|
|
6
|
+
* Third-party text (federated actor display names, spoiler/CW text, image alt
|
|
7
|
+
* text, bios, `<title>` / `og:site_name` of scraped remote pages, …) arrives
|
|
8
|
+
* with the whitespace the remote author's markup happened to contain. A real
|
|
9
|
+
* bug: a remote page served
|
|
10
|
+
*
|
|
11
|
+
* <title>
|
|
12
|
+
* Mi título
|
|
13
|
+
* </title>
|
|
14
|
+
*
|
|
15
|
+
* and the extracted string — newlines and indentation included — was stored
|
|
16
|
+
* verbatim. Clients render text with React Native Web `Text`, which maps to
|
|
17
|
+
* CSS `white-space: pre-wrap`: unlike HTML, newlines and repeated spaces are
|
|
18
|
+
* NOT collapsed at render time, so the user saw a blank line and a six-space
|
|
19
|
+
* indent inside the link preview.
|
|
20
|
+
*
|
|
21
|
+
* Storage-time normalization is therefore the ONLY place this can be fixed:
|
|
22
|
+
* every renderer downstream is faithful by design.
|
|
23
|
+
*
|
|
24
|
+
* WHICH HELPER DO I USE?
|
|
25
|
+
* ----------------------
|
|
26
|
+
* - {@link normalizeInlineText} — the value is conceptually ONE LINE, and a
|
|
27
|
+
* line break in it is always an accident of the source markup: page titles,
|
|
28
|
+
* `siteName`, display names, image `alt`, handles, profile field labels.
|
|
29
|
+
* Every line break becomes a space.
|
|
30
|
+
*
|
|
31
|
+
* - {@link normalizeMultilineText} — the value is a BODY whose line breaks are
|
|
32
|
+
* the author's own paragraphs and must survive: post text, bios/summaries.
|
|
33
|
+
* Line breaks are preserved (capped at one blank line); only the surrounding
|
|
34
|
+
* whitespace noise is cleaned.
|
|
35
|
+
*
|
|
36
|
+
* Using the multiline helper on a title would keep the newlines and reproduce
|
|
37
|
+
* the original bug; using the inline helper on a post body would flatten the
|
|
38
|
+
* author's paragraphs into a single run-on line. Pick deliberately.
|
|
39
|
+
*
|
|
40
|
+
* Neither helper truncates, strips markup, or enforces a character policy —
|
|
41
|
+
* those are separate, product-specific concerns (see `isValidDisplayName` for
|
|
42
|
+
* the display-name character policy). These functions do exactly one thing:
|
|
43
|
+
* normalize whitespace and Unicode form.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Normalize a SINGLE-LINE text value: page/link-preview titles, `siteName`,
|
|
47
|
+
* display names, image alt text, handles, profile field labels.
|
|
48
|
+
*
|
|
49
|
+
* A line break in such a value is never meaningful — it is an artifact of the
|
|
50
|
+
* markup the value was extracted from (`<title>\n Título\n</title>`) — and it
|
|
51
|
+
* survives into the UI because RN Web renders `Text` with `white-space:
|
|
52
|
+
* pre-wrap`. So ALL whitespace, line breaks included, collapses to one space.
|
|
53
|
+
*
|
|
54
|
+
* 1. NFC-normalize, so visually identical strings store and compare
|
|
55
|
+
* identically (a decomposed `e`+◌́ recomposes into `é`).
|
|
56
|
+
* 2. Collapse every run of whitespace — spaces, tabs, `\n`, `\r`, and Unicode
|
|
57
|
+
* spaces such as NBSP — to a single plain space.
|
|
58
|
+
* 3. Trim both ends.
|
|
59
|
+
*
|
|
60
|
+
* Length is NOT capped: a maximum length is a product rule of the specific
|
|
61
|
+
* field (see `MAX_DISPLAY_NAME_LENGTH`), not a property of text normalization.
|
|
62
|
+
* Markup is NOT stripped: run the caller's sanitizer first if the source can
|
|
63
|
+
* contain HTML.
|
|
64
|
+
*
|
|
65
|
+
* A value that is empty or whitespace-only returns `''`. Callers decide whether
|
|
66
|
+
* that means "omit the field" (`|| undefined`) or "store an empty string".
|
|
67
|
+
*
|
|
68
|
+
* Idempotent: `f(f(x)) === f(x)`.
|
|
69
|
+
*/
|
|
70
|
+
export declare function normalizeInlineText(value: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Normalize a MULTILINE text BODY: post text, bios, summaries — anywhere the
|
|
73
|
+
* line breaks are the author's paragraphs and must be preserved.
|
|
74
|
+
*
|
|
75
|
+
* Cleans the whitespace noise around those paragraphs without destroying them:
|
|
76
|
+
*
|
|
77
|
+
* 1. NFC-normalize (same rationale as {@link normalizeInlineText}).
|
|
78
|
+
* 2. Unify every line-break form (CRLF, lone CR, U+2028, U+2029) to `\n`.
|
|
79
|
+
* 3. Collapse runs of HORIZONTAL whitespace (spaces, tabs, NBSP and friends)
|
|
80
|
+
* to a single space. Line breaks are untouched.
|
|
81
|
+
* 4. Strip the horizontal whitespace at the END of each line.
|
|
82
|
+
* 5. Collapse three or more line breaks to exactly one blank line (`\n\n`).
|
|
83
|
+
* 6. Trim both ends.
|
|
84
|
+
*
|
|
85
|
+
* STEP 4 MUST PRECEDE STEP 5 — this is the whole point of the function. A
|
|
86
|
+
* "blank" line that actually contains spaces (`"a\n \n \nb"`) breaks the
|
|
87
|
+
* run of `\n` characters, so a bare `\n{3,}` collapse (step 5 alone) never sees
|
|
88
|
+
* it and the extra blank lines survive into the UI. That is exactly the bug in
|
|
89
|
+
* federated post bodies. Removing the trailing horizontal whitespace first
|
|
90
|
+
* turns those lines into real, empty lines, which step 5 then collapses.
|
|
91
|
+
*
|
|
92
|
+
* A single space at the START of a line is preserved: only RUNS of horizontal
|
|
93
|
+
* whitespace collapse, and an indent is not trailing whitespace, so a one-space
|
|
94
|
+
* indent is treated as the author's and left alone.
|
|
95
|
+
*
|
|
96
|
+
* A value that is empty or whitespace-only returns `''`.
|
|
97
|
+
*
|
|
98
|
+
* Idempotent: `f(f(x)) === f(x)`.
|
|
99
|
+
*/
|
|
100
|
+
export declare function normalizeMultilineText(value: string): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.1.0",
|
|
4
4
|
"description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -94,8 +94,8 @@
|
|
|
94
94
|
}
|
|
95
95
|
},
|
|
96
96
|
"dependencies": {
|
|
97
|
-
"@oxyhq/contracts": "
|
|
98
|
-
"@oxyhq/protocol": "
|
|
97
|
+
"@oxyhq/contracts": "workspace:^",
|
|
98
|
+
"@oxyhq/protocol": "workspace:^",
|
|
99
99
|
"bip39": "^3.1.0",
|
|
100
100
|
"buffer": "^6.0.3",
|
|
101
101
|
"elliptic": "^6.6.1",
|
package/src/HttpService.ts
CHANGED
|
@@ -232,7 +232,7 @@ export class HttpService {
|
|
|
232
232
|
private logger: SimpleLogger;
|
|
233
233
|
private config: OxyConfig;
|
|
234
234
|
private tokenRefreshPromise: Promise<string | null> | null = null;
|
|
235
|
-
private tokenRefreshCooldownUntil
|
|
235
|
+
private tokenRefreshCooldownUntil = 0;
|
|
236
236
|
private authRefreshHandler: AuthRefreshHandler | null = null;
|
|
237
237
|
private accessTokenProvider: AccessTokenProvider | null = null;
|
|
238
238
|
private deviceSecretMintInFlight: Promise<DeviceSecretMintOutcome> | null = null;
|
|
@@ -242,7 +242,7 @@ export class HttpService {
|
|
|
242
242
|
* re-emitted. Throttles the {@link CACHE_SOFT_MAX_ENTRIES} warning to at most
|
|
243
243
|
* one per {@link CACHE_SIZE_WARNING_THROTTLE_MS} window.
|
|
244
244
|
*/
|
|
245
|
-
private cacheSizeWarningSilentUntil
|
|
245
|
+
private cacheSizeWarningSilentUntil = 0;
|
|
246
246
|
|
|
247
247
|
/**
|
|
248
248
|
* Fan-out listeners notified on EVERY access-token change on this instance:
|
package/src/crypto/keyManager.ts
CHANGED
|
@@ -1178,9 +1178,9 @@ export class KeyManager {
|
|
|
1178
1178
|
* @param userConfirmed - If true, user has explicitly confirmed deletion (default: false)
|
|
1179
1179
|
*/
|
|
1180
1180
|
static async deleteIdentity(
|
|
1181
|
-
skipBackup
|
|
1182
|
-
force
|
|
1183
|
-
userConfirmed
|
|
1181
|
+
skipBackup = false,
|
|
1182
|
+
force = false,
|
|
1183
|
+
userConfirmed = false
|
|
1184
1184
|
): Promise<void> {
|
|
1185
1185
|
if (isWebPlatform()) {
|
|
1186
1186
|
return; // Identity storage is only available on native platforms, nothing to delete
|
|
@@ -160,7 +160,7 @@ export class RecoveryPhraseService {
|
|
|
160
160
|
/**
|
|
161
161
|
* Get suggestions for a partial word
|
|
162
162
|
*/
|
|
163
|
-
static getSuggestions(partial: string, limit
|
|
163
|
+
static getSuggestions(partial: string, limit = 5): string[] {
|
|
164
164
|
const lowerPartial = partial.toLowerCase();
|
|
165
165
|
return bip39.wordlists.english
|
|
166
166
|
.filter((word: string) => word.startsWith(lowerPartial))
|
package/src/i18n/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import jaJP from './locales/ja-JP.json';
|
|
|
9
9
|
import koKR from './locales/ko-KR.json';
|
|
10
10
|
import zhCN from './locales/zh-CN.json';
|
|
11
11
|
import arSA from './locales/ar-SA.json';
|
|
12
|
+
import { getBaseLanguage } from '../utils/languageUtils';
|
|
12
13
|
|
|
13
14
|
export type LocaleDict = Record<string, any>;
|
|
14
15
|
|
|
@@ -39,12 +40,30 @@ const DICTS: Record<string, LocaleDict> = {
|
|
|
39
40
|
|
|
40
41
|
const FALLBACK = 'en-US';
|
|
41
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a locale tag to the key of the dictionary that should serve it.
|
|
45
|
+
*
|
|
46
|
+
* Account locales are full BCP-47 tags (e.g. `es-MX`, `pt-BR`, `fr-CA`), but a
|
|
47
|
+
* translation dictionary is shipped per base language. Resolution order:
|
|
48
|
+
* 1. Exact dictionary for the tag (e.g. `es-ES`).
|
|
49
|
+
* 2. Dictionary for the base subtag (e.g. `es-MX` → `es`).
|
|
50
|
+
* 3. The English fallback.
|
|
51
|
+
*/
|
|
52
|
+
function resolveLang(locale: string | undefined): string {
|
|
53
|
+
if (locale) {
|
|
54
|
+
if (DICTS[locale]) return locale;
|
|
55
|
+
const base = getBaseLanguage(locale);
|
|
56
|
+
if (DICTS[base]) return base;
|
|
57
|
+
}
|
|
58
|
+
return FALLBACK;
|
|
59
|
+
}
|
|
60
|
+
|
|
42
61
|
function getNested(obj: any, path: string): any {
|
|
43
62
|
return path.split('.').reduce((acc, key) => (acc && acc[key] != null ? acc[key] : undefined), obj);
|
|
44
63
|
}
|
|
45
64
|
|
|
46
65
|
export function translate(locale: string | undefined, key: string, vars?: Record<string, string | number>): string {
|
|
47
|
-
const lang = locale
|
|
66
|
+
const lang = resolveLang(locale);
|
|
48
67
|
const dict = DICTS[lang] || DICTS[FALLBACK];
|
|
49
68
|
let val = getNested(dict, key);
|
|
50
69
|
// Per-key fallback to the English dictionary when a key is missing from the
|
|
@@ -64,6 +83,6 @@ export function translate(locale: string | undefined, key: string, vars?: Record
|
|
|
64
83
|
}
|
|
65
84
|
|
|
66
85
|
export function hasKey(locale: string | undefined, key: string): boolean {
|
|
67
|
-
const lang = locale
|
|
86
|
+
const lang = resolveLang(locale);
|
|
68
87
|
return getNested(DICTS[lang], key) != null || getNested(DICTS[FALLBACK], key) != null;
|
|
69
88
|
}
|
|
@@ -169,9 +169,12 @@
|
|
|
169
169
|
},
|
|
170
170
|
"language": {
|
|
171
171
|
"title": "Language",
|
|
172
|
-
"subtitle": "Choose your
|
|
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
|
|
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/src/index.ts
CHANGED
|
@@ -53,6 +53,7 @@ export type {
|
|
|
53
53
|
BulkFollowResult,
|
|
54
54
|
BulkUnfollowEntry,
|
|
55
55
|
BulkUnfollowResult,
|
|
56
|
+
ViewerGraph,
|
|
56
57
|
} from './mixins/OxyServices.user';
|
|
57
58
|
export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData';
|
|
58
59
|
|
|
@@ -323,13 +324,18 @@ export type { TopicData, TopicTranslation } from './models/Topic';
|
|
|
323
324
|
// ---------------------------------------------------------------------------
|
|
324
325
|
export {
|
|
325
326
|
SUPPORTED_LANGUAGES,
|
|
327
|
+
FALLBACK_LOCALE,
|
|
328
|
+
getBaseLanguage,
|
|
329
|
+
normalizeLocale,
|
|
330
|
+
isSupportedLocale,
|
|
326
331
|
getLanguageMetadata,
|
|
327
332
|
getLanguageName,
|
|
328
333
|
getNativeLanguageName,
|
|
329
|
-
normalizeLanguageCode,
|
|
330
334
|
isRTLLocale,
|
|
335
|
+
getUserLanguages,
|
|
336
|
+
getPrimaryLanguage,
|
|
331
337
|
} from './utils/languageUtils';
|
|
332
|
-
export type {
|
|
338
|
+
export type { SupportedLanguage } from './utils/languageUtils';
|
|
333
339
|
|
|
334
340
|
// ---------------------------------------------------------------------------
|
|
335
341
|
// Platform detection
|
|
@@ -460,6 +466,14 @@ export {
|
|
|
460
466
|
validateAndSanitizeUserInput,
|
|
461
467
|
} from './utils/validationUtils';
|
|
462
468
|
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
// Text normalization
|
|
471
|
+
// ---------------------------------------------------------------------------
|
|
472
|
+
export {
|
|
473
|
+
normalizeInlineText,
|
|
474
|
+
normalizeMultilineText,
|
|
475
|
+
} from './utils/textNormalization';
|
|
476
|
+
|
|
463
477
|
// ---------------------------------------------------------------------------
|
|
464
478
|
// Logging
|
|
465
479
|
// ---------------------------------------------------------------------------
|
|
@@ -435,12 +435,16 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
435
435
|
if (error instanceof Error) {
|
|
436
436
|
errorMessage = error.message || errorMessage;
|
|
437
437
|
} else if (error && typeof error === 'object') {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
438
|
+
const errObj = error as Record<string, unknown>;
|
|
439
|
+
if ('message' in errObj) {
|
|
440
|
+
errorMessage = String(errObj.message) || errorMessage;
|
|
441
|
+
} else if (typeof errObj.error === 'string') {
|
|
442
|
+
errorMessage = errObj.error;
|
|
443
|
+
} else if (errObj.data && typeof errObj.data === 'object') {
|
|
444
|
+
const dataObj = errObj.data as Record<string, unknown>;
|
|
445
|
+
if (dataObj.message) {
|
|
446
|
+
errorMessage = String(dataObj.message);
|
|
447
|
+
}
|
|
444
448
|
}
|
|
445
449
|
} else if (error) {
|
|
446
450
|
errorMessage = String(error) || errorMessage;
|
|
@@ -462,8 +466,8 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
462
466
|
throw handledError;
|
|
463
467
|
}
|
|
464
468
|
|
|
465
|
-
const newError = new Error(errorMessage);
|
|
466
|
-
|
|
469
|
+
const newError: Error & { fileContext?: Record<string, unknown> } = new Error(errorMessage);
|
|
470
|
+
newError.fileContext = contextError.fileContext;
|
|
467
471
|
throw this.handleError(newError);
|
|
468
472
|
}
|
|
469
473
|
}
|
|
@@ -546,7 +550,7 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
546
550
|
/**
|
|
547
551
|
* Delete asset with optional force
|
|
548
552
|
*/
|
|
549
|
-
async assetDelete(fileId: string, force
|
|
553
|
+
async assetDelete(fileId: string, force = false): Promise<any> {
|
|
550
554
|
try {
|
|
551
555
|
const params: any = force ? { force: 'true' } : undefined;
|
|
552
556
|
const result = await this.makeRequest('DELETE', `/assets/${fileId}`, params, { cache: false });
|
|
@@ -590,7 +594,7 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
590
594
|
}
|
|
591
595
|
}
|
|
592
596
|
|
|
593
|
-
async uploadAvatar(file: AssetUploadInput, userId: string, app
|
|
597
|
+
async uploadAvatar(file: AssetUploadInput, userId: string, app = 'profiles'): Promise<any> {
|
|
594
598
|
try {
|
|
595
599
|
const asset = await this.assetUpload(file, 'public');
|
|
596
600
|
await this.assetLink(asset.file.id, app, 'avatar', userId, 'public');
|
|
@@ -600,7 +604,7 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
600
604
|
}
|
|
601
605
|
}
|
|
602
606
|
|
|
603
|
-
async uploadProfileBanner(file: AssetUploadInput, userId: string, app
|
|
607
|
+
async uploadProfileBanner(file: AssetUploadInput, userId: string, app = 'profiles'): Promise<any> {
|
|
604
608
|
try {
|
|
605
609
|
const asset = await this.assetUpload(file, 'public');
|
|
606
610
|
await this.assetLink(asset.file.id, app, 'profile-banner', userId, 'public');
|