@oxyhq/core 3.4.15 → 3.4.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/AuthManager.js +5 -0
- package/dist/cjs/mixins/OxyServices.auth.js +1 -1
- package/dist/cjs/mixins/OxyServices.sso.js +2 -1
- package/dist/cjs/mixins/OxyServices.user.js +1 -1
- package/dist/cjs/utils/accountUtils.js +15 -14
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/AuthManager.js +5 -0
- package/dist/esm/mixins/OxyServices.auth.js +1 -1
- package/dist/esm/mixins/OxyServices.sso.js +2 -1
- package/dist/esm/mixins/OxyServices.user.js +1 -1
- package/dist/esm/utils/accountUtils.js +15 -14
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/mixins/OxyServices.user.d.ts +5 -8
- package/dist/types/models/interfaces.d.ts +11 -10
- package/dist/types/models/session.d.ts +2 -0
- package/dist/types/utils/accountUtils.d.ts +11 -18
- package/package.json +2 -2
- package/src/AuthManager.ts +6 -1
- package/src/mixins/OxyServices.auth.ts +1 -1
- package/src/mixins/OxyServices.sso.ts +4 -1
- package/src/mixins/OxyServices.user.ts +5 -4
- package/src/models/interfaces.ts +11 -10
- package/src/models/session.ts +3 -0
- package/src/utils/__tests__/accountUtils.test.ts +10 -23
- package/src/utils/accountUtils.ts +25 -24
- package/src/utils/asyncUtils.ts +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* User Management Methods Mixin
|
|
3
3
|
*/
|
|
4
4
|
import type { User, Notification, NotificationPreferences, UserPreferences, SearchProfilesResponse, PrivacySettings } from '../models/interfaces';
|
|
5
|
+
import type { UserNameResponse, UserProfileUpdate } from '@oxyhq/contracts';
|
|
5
6
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
6
7
|
import { type PaginationParams } from '../utils/apiUtils';
|
|
7
8
|
export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T): {
|
|
@@ -12,7 +13,7 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
|
|
|
12
13
|
getProfileByUsername(username: string): Promise<User>;
|
|
13
14
|
/**
|
|
14
15
|
* Lightweight username lookup for login flows.
|
|
15
|
-
* Returns minimal public info: exists, color, avatar, displayName.
|
|
16
|
+
* Returns minimal public info: exists, color, avatar, name.displayName.
|
|
16
17
|
* Faster than getProfileByUsername — no stats, no formatting.
|
|
17
18
|
*/
|
|
18
19
|
lookupUsername(username: string): Promise<{
|
|
@@ -20,7 +21,7 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
|
|
|
20
21
|
username: string;
|
|
21
22
|
color: string | null;
|
|
22
23
|
avatar: string | null;
|
|
23
|
-
|
|
24
|
+
name: UserNameResponse;
|
|
24
25
|
}>;
|
|
25
26
|
/**
|
|
26
27
|
* Search user profiles
|
|
@@ -62,11 +63,7 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
|
|
|
62
63
|
}): Promise<Array<{
|
|
63
64
|
id: string;
|
|
64
65
|
username: string;
|
|
65
|
-
name
|
|
66
|
-
first?: string;
|
|
67
|
-
last?: string;
|
|
68
|
-
full?: string;
|
|
69
|
-
};
|
|
66
|
+
name: UserNameResponse;
|
|
70
67
|
description?: string;
|
|
71
68
|
isFederated?: boolean;
|
|
72
69
|
isAgent?: boolean;
|
|
@@ -111,7 +108,7 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
|
|
|
111
108
|
*
|
|
112
109
|
* TanStack Query handles offline queuing automatically.
|
|
113
110
|
*/
|
|
114
|
-
updateProfile(updates:
|
|
111
|
+
updateProfile(updates: UserProfileUpdate): Promise<User>;
|
|
115
112
|
/**
|
|
116
113
|
* Get privacy settings for a user
|
|
117
114
|
* @param userId - The user ID (defaults to current user)
|
|
@@ -81,26 +81,27 @@ export interface User {
|
|
|
81
81
|
publicKey: string;
|
|
82
82
|
username: string;
|
|
83
83
|
email?: string;
|
|
84
|
-
avatar?: string;
|
|
85
|
-
color?: string;
|
|
84
|
+
avatar?: string | null;
|
|
85
|
+
color?: string | null;
|
|
86
86
|
privacySettings?: PrivacySettings;
|
|
87
87
|
/**
|
|
88
|
-
* Structured human name.
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* `@oxyhq/contracts` — do NOT re-declare a bare `string` here.
|
|
88
|
+
* Structured human name. `name.displayName` is the canonical display string
|
|
89
|
+
* resolved by the API; consumers render it directly instead of recomposing
|
|
90
|
+
* names from `first` / `last` / `full` / `username`.
|
|
92
91
|
*/
|
|
93
|
-
name
|
|
92
|
+
name: UserNameResponse;
|
|
94
93
|
bio?: string;
|
|
95
94
|
location?: string;
|
|
96
95
|
website?: string;
|
|
97
96
|
createdAt?: string;
|
|
98
97
|
updatedAt?: string;
|
|
99
|
-
links?:
|
|
98
|
+
links?: string[];
|
|
99
|
+
linksMetadata?: Array<{
|
|
100
|
+
url: string;
|
|
100
101
|
title?: string;
|
|
101
102
|
description?: string;
|
|
102
103
|
image?: string;
|
|
103
|
-
|
|
104
|
+
id?: string;
|
|
104
105
|
}>;
|
|
105
106
|
_count?: {
|
|
106
107
|
followers?: number;
|
|
@@ -539,7 +540,7 @@ export interface RefreshAllAccountUser {
|
|
|
539
540
|
* string. The server projects `name` verbatim from the user document. The
|
|
540
541
|
* single source of truth is `@oxyhq/contracts`.
|
|
541
542
|
*/
|
|
542
|
-
name
|
|
543
|
+
name: UserNameResponse;
|
|
543
544
|
avatar?: string | null;
|
|
544
545
|
email?: string;
|
|
545
546
|
color?: string | null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UserNameResponse } from '@oxyhq/contracts';
|
|
1
2
|
export interface ClientSession {
|
|
2
3
|
sessionId: string;
|
|
3
4
|
deviceId: string;
|
|
@@ -21,6 +22,7 @@ export interface StorageKeys {
|
|
|
21
22
|
export interface MinimalUserData {
|
|
22
23
|
id: string;
|
|
23
24
|
username: string;
|
|
25
|
+
name: UserNameResponse;
|
|
24
26
|
avatar?: string;
|
|
25
27
|
}
|
|
26
28
|
export interface SessionLoginResponse {
|
|
@@ -27,18 +27,13 @@ export interface QuickAccount {
|
|
|
27
27
|
/** Minimal user shape accepted by display-name helpers. Avoids importing the full User type. */
|
|
28
28
|
export interface DisplayNameUserShape {
|
|
29
29
|
name?: string | {
|
|
30
|
+
displayName?: string;
|
|
30
31
|
first?: string;
|
|
31
32
|
last?: string;
|
|
32
33
|
full?: string;
|
|
33
34
|
[key: string]: unknown;
|
|
34
35
|
};
|
|
35
|
-
/**
|
|
36
|
-
* Pre-resolved display name as emitted by the server's `displayName` virtual
|
|
37
|
-
* (raw `/users/me` responses). NOTE: the server virtual resolves to
|
|
38
|
-
* `username || truncatedPublicKey || 'Anonymous'` — it does NOT compose the
|
|
39
|
-
* structured `name`. It is therefore preferred only AFTER a real structured
|
|
40
|
-
* name, so a first-name-only account never collapses to its username/key.
|
|
41
|
-
*/
|
|
36
|
+
/** Pre-normalized account-row display name, not the API User DTO field. */
|
|
42
37
|
displayName?: string;
|
|
43
38
|
username?: string;
|
|
44
39
|
publicKey?: string;
|
|
@@ -52,16 +47,13 @@ export declare const formatPublicKeyHandle: (publicKey: string) => string;
|
|
|
52
47
|
* Resolve a friendly display name for a user.
|
|
53
48
|
*
|
|
54
49
|
* Order of preference:
|
|
55
|
-
* 1. `name.
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* 4. `username`
|
|
63
|
-
* 5. `Account 0x12345678…` (derived from publicKey, when present)
|
|
64
|
-
* 6. Translated fallback (e.g. "Unnamed")
|
|
50
|
+
* 1. `name.displayName` from the API user contract.
|
|
51
|
+
* 2. `name.full`, or composed `name.first name.last` for local unsaved shapes.
|
|
52
|
+
* 3. `name` when passed as a plain string by local non-DTO call sites.
|
|
53
|
+
* 4. pre-normalized account-row `displayName`.
|
|
54
|
+
* 5. `username`
|
|
55
|
+
* 6. `Account 0x12345678…` (derived from publicKey, when present)
|
|
56
|
+
* 7. Translated fallback (e.g. "Unnamed")
|
|
65
57
|
*
|
|
66
58
|
* The translation key `common.unnamed` is used for the final fallback. If the
|
|
67
59
|
* caller does not pass a locale, the default English translation is used.
|
|
@@ -89,6 +81,7 @@ export declare const buildAccountsArray: (accounts: Record<string, QuickAccount>
|
|
|
89
81
|
*/
|
|
90
82
|
export declare const createQuickAccount: (sessionId: string, userData: {
|
|
91
83
|
name?: string | {
|
|
84
|
+
displayName?: string;
|
|
92
85
|
full?: string;
|
|
93
86
|
first?: string;
|
|
94
87
|
last?: string;
|
|
@@ -99,7 +92,7 @@ export declare const createQuickAccount: (sessionId: string, userData: {
|
|
|
99
92
|
_id?: {
|
|
100
93
|
toString(): string;
|
|
101
94
|
} | string;
|
|
102
|
-
avatar?: string;
|
|
95
|
+
avatar?: string | null;
|
|
103
96
|
}, existingAccount?: QuickAccount, getFileDownloadUrl?: (fileId: string, variant: string) => string) => QuickAccount;
|
|
104
97
|
/**
|
|
105
98
|
* Merge a fresh `/auth/refresh-all` snapshot into an existing QuickAccount
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/core",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.17",
|
|
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",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
}
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
|
-
"@oxyhq/contracts": "^0.1.
|
|
100
|
+
"@oxyhq/contracts": "^0.1.1",
|
|
101
101
|
"bip39": "^3.1.0",
|
|
102
102
|
"buffer": "^6.0.3",
|
|
103
103
|
"elliptic": "^6.6.1",
|
package/src/AuthManager.ts
CHANGED
|
@@ -497,7 +497,7 @@ export class AuthManager {
|
|
|
497
497
|
this.oxyServices.httpService.setTokens(session.accessToken);
|
|
498
498
|
}
|
|
499
499
|
|
|
500
|
-
if (session.user && typeof
|
|
500
|
+
if (session.user && typeof session.user.id === 'string' && session.user.id.length > 0) {
|
|
501
501
|
this.currentUser = session.user;
|
|
502
502
|
}
|
|
503
503
|
|
|
@@ -514,6 +514,7 @@ export class AuthManager {
|
|
|
514
514
|
user: {
|
|
515
515
|
id: session.user.id,
|
|
516
516
|
username: session.user.username,
|
|
517
|
+
name: session.user.name,
|
|
517
518
|
avatar: session.user.avatar ?? null,
|
|
518
519
|
},
|
|
519
520
|
accessToken: session.accessToken,
|
|
@@ -758,6 +759,7 @@ export class AuthManager {
|
|
|
758
759
|
return {
|
|
759
760
|
id: account.user.id,
|
|
760
761
|
username: account.user.username,
|
|
762
|
+
name: account.user.name,
|
|
761
763
|
avatar: account.user.avatar ?? undefined,
|
|
762
764
|
};
|
|
763
765
|
}
|
|
@@ -806,6 +808,7 @@ export class AuthManager {
|
|
|
806
808
|
this.currentUser = {
|
|
807
809
|
id: hydrated.id,
|
|
808
810
|
username: hydrated.username,
|
|
811
|
+
name: hydrated.name,
|
|
809
812
|
avatar: hydrated.avatar ?? undefined,
|
|
810
813
|
};
|
|
811
814
|
this.notifyListeners();
|
|
@@ -1022,6 +1025,7 @@ export class AuthManager {
|
|
|
1022
1025
|
? {
|
|
1023
1026
|
id: updated.user.id,
|
|
1024
1027
|
username: updated.user.username,
|
|
1028
|
+
name: updated.user.name,
|
|
1025
1029
|
avatar: updated.user.avatar ?? undefined,
|
|
1026
1030
|
}
|
|
1027
1031
|
: null;
|
|
@@ -1083,6 +1087,7 @@ export class AuthManager {
|
|
|
1083
1087
|
? {
|
|
1084
1088
|
id: next.user.id,
|
|
1085
1089
|
username: next.user.username,
|
|
1090
|
+
name: next.user.name,
|
|
1086
1091
|
avatar: next.user.avatar ?? undefined,
|
|
1087
1092
|
}
|
|
1088
1093
|
: null;
|
|
@@ -684,7 +684,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
684
684
|
continue;
|
|
685
685
|
}
|
|
686
686
|
const userId = e.user.id ?? e.user._id;
|
|
687
|
-
if (!userId || !e.user.username) {
|
|
687
|
+
if (!userId || !e.user.username || !e.user.name?.displayName) {
|
|
688
688
|
continue;
|
|
689
689
|
}
|
|
690
690
|
if (typeof e.authuser !== 'number') {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
27
27
|
import type { SessionLoginResponse, MinimalUserData } from '../models/session';
|
|
28
|
+
import type { UserNameResponse } from '@oxyhq/contracts';
|
|
28
29
|
import { createDebugLogger } from '../shared/utils/debugUtils';
|
|
29
30
|
|
|
30
31
|
const debug = createDebugLogger('SSO');
|
|
@@ -40,6 +41,7 @@ interface SsoExchangeWireResponse {
|
|
|
40
41
|
id?: string;
|
|
41
42
|
_id?: string;
|
|
42
43
|
username?: string;
|
|
44
|
+
name?: UserNameResponse;
|
|
43
45
|
avatar?: string;
|
|
44
46
|
};
|
|
45
47
|
expiresAt?: string;
|
|
@@ -140,13 +142,14 @@ export function OxyServicesSsoMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
const userId = payload.user?.id ?? payload.user?._id;
|
|
143
|
-
if (!userId || typeof payload.user?.username !== 'string') {
|
|
145
|
+
if (!userId || typeof payload.user?.username !== 'string' || typeof payload.user.name?.displayName !== 'string') {
|
|
144
146
|
throw this.handleError(new Error('SSO exchange returned an invalid user'));
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
const user: MinimalUserData = {
|
|
148
150
|
id: userId,
|
|
149
151
|
username: payload.user.username,
|
|
152
|
+
name: payload.user.name,
|
|
150
153
|
avatar: payload.user.avatar,
|
|
151
154
|
};
|
|
152
155
|
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
PaginationInfo,
|
|
11
11
|
PrivacySettings,
|
|
12
12
|
} from '../models/interfaces';
|
|
13
|
+
import type { UserNameResponse, UserProfileUpdate } from '@oxyhq/contracts';
|
|
13
14
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
14
15
|
import { buildSearchParams, buildPaginationParams, type PaginationParams } from '../utils/apiUtils';
|
|
15
16
|
import { KeyManager } from '../crypto/keyManager';
|
|
@@ -38,7 +39,7 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Lightweight username lookup for login flows.
|
|
41
|
-
* Returns minimal public info: exists, color, avatar, displayName.
|
|
42
|
+
* Returns minimal public info: exists, color, avatar, name.displayName.
|
|
42
43
|
* Faster than getProfileByUsername — no stats, no formatting.
|
|
43
44
|
*/
|
|
44
45
|
async lookupUsername(username: string): Promise<{
|
|
@@ -46,7 +47,7 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
46
47
|
username: string;
|
|
47
48
|
color: string | null;
|
|
48
49
|
avatar: string | null;
|
|
49
|
-
|
|
50
|
+
name: UserNameResponse;
|
|
50
51
|
}> {
|
|
51
52
|
return await this.makeRequest('GET', `/auth/lookup/${encodeURIComponent(username)}`, undefined, {
|
|
52
53
|
cache: true,
|
|
@@ -150,7 +151,7 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
150
151
|
}): Promise<Array<{
|
|
151
152
|
id: string;
|
|
152
153
|
username: string;
|
|
153
|
-
name
|
|
154
|
+
name: UserNameResponse;
|
|
154
155
|
description?: string;
|
|
155
156
|
isFederated?: boolean;
|
|
156
157
|
isAgent?: boolean;
|
|
@@ -225,7 +226,7 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
225
226
|
*
|
|
226
227
|
* TanStack Query handles offline queuing automatically.
|
|
227
228
|
*/
|
|
228
|
-
async updateProfile(updates:
|
|
229
|
+
async updateProfile(updates: UserProfileUpdate): Promise<User> {
|
|
229
230
|
try {
|
|
230
231
|
const result = normalizeUserIdentity(
|
|
231
232
|
await this.makeRequest<User>('PUT', '/users/me', updates, { cache: false }),
|
package/src/models/interfaces.ts
CHANGED
|
@@ -89,28 +89,29 @@ export interface User {
|
|
|
89
89
|
username: string;
|
|
90
90
|
email?: string;
|
|
91
91
|
// Avatar file id (asset id)
|
|
92
|
-
avatar?: string;
|
|
92
|
+
avatar?: string | null;
|
|
93
93
|
// Named color preset (e.g. 'teal', 'blue', 'purple')
|
|
94
|
-
color?: string;
|
|
94
|
+
color?: string | null;
|
|
95
95
|
// Privacy and security settings
|
|
96
96
|
privacySettings?: PrivacySettings;
|
|
97
97
|
/**
|
|
98
|
-
* Structured human name.
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* `@oxyhq/contracts` — do NOT re-declare a bare `string` here.
|
|
98
|
+
* Structured human name. `name.displayName` is the canonical display string
|
|
99
|
+
* resolved by the API; consumers render it directly instead of recomposing
|
|
100
|
+
* names from `first` / `last` / `full` / `username`.
|
|
102
101
|
*/
|
|
103
|
-
name
|
|
102
|
+
name: UserNameResponse;
|
|
104
103
|
bio?: string;
|
|
105
104
|
location?: string;
|
|
106
105
|
website?: string;
|
|
107
106
|
createdAt?: string;
|
|
108
107
|
updatedAt?: string;
|
|
109
|
-
links?:
|
|
108
|
+
links?: string[];
|
|
109
|
+
linksMetadata?: Array<{
|
|
110
|
+
url: string;
|
|
110
111
|
title?: string;
|
|
111
112
|
description?: string;
|
|
112
113
|
image?: string;
|
|
113
|
-
|
|
114
|
+
id?: string;
|
|
114
115
|
}>;
|
|
115
116
|
// Social counts
|
|
116
117
|
_count?: {
|
|
@@ -651,7 +652,7 @@ export interface RefreshAllAccountUser {
|
|
|
651
652
|
* string. The server projects `name` verbatim from the user document. The
|
|
652
653
|
* single source of truth is `@oxyhq/contracts`.
|
|
653
654
|
*/
|
|
654
|
-
name
|
|
655
|
+
name: UserNameResponse;
|
|
655
656
|
avatar?: string | null;
|
|
656
657
|
email?: string;
|
|
657
658
|
color?: string | null;
|
package/src/models/session.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { UserNameResponse } from '@oxyhq/contracts';
|
|
2
|
+
|
|
1
3
|
export interface ClientSession {
|
|
2
4
|
sessionId: string;
|
|
3
5
|
deviceId: string;
|
|
@@ -23,6 +25,7 @@ export interface StorageKeys {
|
|
|
23
25
|
export interface MinimalUserData {
|
|
24
26
|
id: string;
|
|
25
27
|
username: string;
|
|
28
|
+
name: UserNameResponse;
|
|
26
29
|
avatar?: string; // file id
|
|
27
30
|
}
|
|
28
31
|
|
|
@@ -4,16 +4,16 @@ import {
|
|
|
4
4
|
formatPublicKeyHandle,
|
|
5
5
|
} from '../accountUtils';
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Regression coverage for the auth-app display-name drift (Phase 1 of the
|
|
9
|
-
* contract-centralisation refactor).
|
|
10
|
-
*
|
|
11
|
-
* The auth app previously required BOTH `name.first` AND `name.last` to compose
|
|
12
|
-
* a display name, falling back to the lowercase `username` for first-name-only
|
|
13
|
-
* accounts. The canonical resolver in core MUST be first-name-only safe: a user
|
|
14
|
-
* with only a first name resolves to that first name, never to the username.
|
|
15
|
-
*/
|
|
16
7
|
describe('getAccountDisplayName', () => {
|
|
8
|
+
it('prefers the API name.displayName when present', () => {
|
|
9
|
+
expect(
|
|
10
|
+
getAccountDisplayName({
|
|
11
|
+
name: { first: 'Nate', displayName: 'Nate Isern' },
|
|
12
|
+
username: 'nateus',
|
|
13
|
+
}),
|
|
14
|
+
).toBe('Nate Isern');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
17
|
it('returns first name for first-name-only accounts (NOT the username)', () => {
|
|
18
18
|
const result = getAccountDisplayName({
|
|
19
19
|
name: { first: 'Nate' },
|
|
@@ -50,20 +50,7 @@ describe('getAccountDisplayName', () => {
|
|
|
50
50
|
).toBe('Nathaniel Isern');
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
it('uses
|
|
54
|
-
// Server `displayName` virtual = `username || truncatedKey`; it ignores
|
|
55
|
-
// the structured name. A real name must win so a first-only account is
|
|
56
|
-
// never collapsed to its username.
|
|
57
|
-
expect(
|
|
58
|
-
getAccountDisplayName({
|
|
59
|
-
name: { first: 'Nate' },
|
|
60
|
-
displayName: 'nateus',
|
|
61
|
-
username: 'nateus',
|
|
62
|
-
}),
|
|
63
|
-
).toBe('Nate');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('uses displayName when there is no structured name', () => {
|
|
53
|
+
it('uses pre-normalized account-row displayName when there is no structured name', () => {
|
|
67
54
|
expect(
|
|
68
55
|
getAccountDisplayName({
|
|
69
56
|
displayName: 'Cool Display',
|
|
@@ -30,14 +30,14 @@ export interface QuickAccount {
|
|
|
30
30
|
|
|
31
31
|
/** Minimal user shape accepted by display-name helpers. Avoids importing the full User type. */
|
|
32
32
|
export interface DisplayNameUserShape {
|
|
33
|
-
name?: string | {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
name?: string | {
|
|
34
|
+
displayName?: string;
|
|
35
|
+
first?: string;
|
|
36
|
+
last?: string;
|
|
37
|
+
full?: string;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
};
|
|
40
|
+
/** Pre-normalized account-row display name, not the API User DTO field. */
|
|
41
41
|
displayName?: string;
|
|
42
42
|
username?: string;
|
|
43
43
|
publicKey?: string;
|
|
@@ -57,16 +57,13 @@ export const formatPublicKeyHandle = (publicKey: string): string => {
|
|
|
57
57
|
* Resolve a friendly display name for a user.
|
|
58
58
|
*
|
|
59
59
|
* Order of preference:
|
|
60
|
-
* 1. `name.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* 4. `username`
|
|
68
|
-
* 5. `Account 0x12345678…` (derived from publicKey, when present)
|
|
69
|
-
* 6. Translated fallback (e.g. "Unnamed")
|
|
60
|
+
* 1. `name.displayName` from the API user contract.
|
|
61
|
+
* 2. `name.full`, or composed `name.first name.last` for local unsaved shapes.
|
|
62
|
+
* 3. `name` when passed as a plain string by local non-DTO call sites.
|
|
63
|
+
* 4. pre-normalized account-row `displayName`.
|
|
64
|
+
* 5. `username`
|
|
65
|
+
* 6. `Account 0x12345678…` (derived from publicKey, when present)
|
|
66
|
+
* 7. Translated fallback (e.g. "Unnamed")
|
|
70
67
|
*
|
|
71
68
|
* The translation key `common.unnamed` is used for the final fallback. If the
|
|
72
69
|
* caller does not pass a locale, the default English translation is used.
|
|
@@ -80,6 +77,9 @@ export const getAccountDisplayName = (
|
|
|
80
77
|
const { name, displayName, username, publicKey } = user;
|
|
81
78
|
|
|
82
79
|
if (name && typeof name === 'object') {
|
|
80
|
+
if (typeof name.displayName === 'string' && name.displayName.trim()) {
|
|
81
|
+
return name.displayName.trim();
|
|
82
|
+
}
|
|
83
83
|
if (typeof name.full === 'string' && name.full.trim()) return name.full.trim();
|
|
84
84
|
const first = typeof name.first === 'string' ? name.first.trim() : '';
|
|
85
85
|
const last = typeof name.last === 'string' ? name.last.trim() : '';
|
|
@@ -146,12 +146,12 @@ export const buildAccountsArray = (
|
|
|
146
146
|
export const createQuickAccount = (
|
|
147
147
|
sessionId: string,
|
|
148
148
|
userData: {
|
|
149
|
-
name?: string | { full?: string; first?: string; last?: string };
|
|
149
|
+
name?: string | { displayName?: string; full?: string; first?: string; last?: string };
|
|
150
150
|
username?: string;
|
|
151
151
|
publicKey?: string;
|
|
152
152
|
id?: string;
|
|
153
153
|
_id?: { toString(): string } | string;
|
|
154
|
-
avatar?: string;
|
|
154
|
+
avatar?: string | null;
|
|
155
155
|
},
|
|
156
156
|
existingAccount?: QuickAccount,
|
|
157
157
|
getFileDownloadUrl?: (fileId: string, variant: string) => string
|
|
@@ -161,10 +161,11 @@ export const createQuickAccount = (
|
|
|
161
161
|
|
|
162
162
|
// Preserve existing avatarUrl if avatar hasn't changed (prevents image reload)
|
|
163
163
|
let avatarUrl: string | undefined;
|
|
164
|
-
|
|
164
|
+
const avatar = userData.avatar ?? undefined;
|
|
165
|
+
if (existingAccount && existingAccount.avatar === avatar && existingAccount.avatarUrl) {
|
|
165
166
|
avatarUrl = existingAccount.avatarUrl;
|
|
166
|
-
} else if (
|
|
167
|
-
avatarUrl = getFileDownloadUrl(
|
|
167
|
+
} else if (avatar && getFileDownloadUrl) {
|
|
168
|
+
avatarUrl = getFileDownloadUrl(avatar, 'thumb');
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
return {
|
|
@@ -172,7 +173,7 @@ export const createQuickAccount = (
|
|
|
172
173
|
userId,
|
|
173
174
|
username: userData.username || '',
|
|
174
175
|
displayName,
|
|
175
|
-
avatar
|
|
176
|
+
avatar,
|
|
176
177
|
avatarUrl,
|
|
177
178
|
};
|
|
178
179
|
};
|
package/src/utils/asyncUtils.ts
CHANGED