@peers-app/peers-ui 0.25.2 → 0.25.4

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 (180) hide show
  1. package/README.md +2 -1
  2. package/dist/components/markdown-editor/mention-node.d.ts +4 -0
  3. package/dist/components/markdown-editor/mention-node.js +4 -1
  4. package/dist/components/responsive-section-nav.d.ts +34 -0
  5. package/dist/components/responsive-section-nav.js +19 -0
  6. package/dist/components/responsive-section-nav.test.d.ts +1 -0
  7. package/dist/components/responsive-section-nav.test.js +45 -0
  8. package/dist/components/router.d.ts +2 -3
  9. package/dist/components/router.js +6 -9
  10. package/dist/globals-hash.test.d.ts +1 -0
  11. package/dist/globals-hash.test.js +16 -0
  12. package/dist/globals.d.ts +19 -0
  13. package/dist/globals.js +51 -12
  14. package/dist/hooks/use-host-contract.d.ts +18 -0
  15. package/dist/hooks/use-host-contract.js +53 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/peers-services-registration.d.ts +1 -5
  19. package/dist/peers-services-registration.js +6 -16
  20. package/dist/root-layout/root-layout-host.js +7 -4
  21. package/dist/root-layout/root-layout-host.test.js +3 -6
  22. package/dist/screens/account/account-screen.d.ts +1 -0
  23. package/dist/screens/account/account-screen.js +29 -53
  24. package/dist/screens/account/account-screen.test.d.ts +1 -0
  25. package/dist/screens/account/account-screen.test.js +47 -0
  26. package/dist/screens/contacts/contact-details.js +2 -23
  27. package/dist/screens/contacts/contact-list.d.ts +24 -1
  28. package/dist/screens/contacts/contact-list.js +128 -143
  29. package/dist/screens/contacts/contact-list.test.d.ts +1 -0
  30. package/dist/screens/contacts/contact-list.test.js +48 -0
  31. package/dist/screens/contacts/index.d.ts +0 -2
  32. package/dist/screens/contacts/index.js +0 -3
  33. package/dist/screens/device-pairing/device-pairing.js +17 -16
  34. package/dist/screens/device-pairing/device-pairing.test.js +4 -9
  35. package/dist/screens/device-pairing/identity-ipc.d.ts +11 -0
  36. package/dist/screens/device-pairing/identity-ipc.js +20 -0
  37. package/dist/screens/device-pairing/use-device-pairing-state.d.ts +2 -2
  38. package/dist/screens/device-pairing/use-device-pairing-state.js +7 -25
  39. package/dist/screens/devices/devices-list.d.ts +18 -0
  40. package/dist/screens/devices/devices-list.js +173 -0
  41. package/dist/screens/devices/devices-list.test.d.ts +1 -0
  42. package/dist/screens/devices/devices-list.test.js +53 -0
  43. package/dist/screens/devices/index.d.ts +1 -0
  44. package/dist/screens/devices/index.js +5 -0
  45. package/dist/screens/groups/group-details.d.ts +1 -0
  46. package/dist/screens/groups/group-details.js +4 -63
  47. package/dist/screens/groups/group-invite-panel.js +2 -2
  48. package/dist/screens/groups/group-list.d.ts +18 -1
  49. package/dist/screens/groups/group-list.js +107 -136
  50. package/dist/screens/groups/group-list.test.d.ts +1 -0
  51. package/dist/screens/groups/group-list.test.js +38 -0
  52. package/dist/screens/groups/group-members.d.ts +1 -0
  53. package/dist/screens/groups/group-members.js +2 -1
  54. package/dist/screens/groups/index.d.ts +0 -3
  55. package/dist/screens/groups/index.js +0 -4
  56. package/dist/screens/identity/identity-routes.d.ts +107 -0
  57. package/dist/screens/identity/identity-routes.js +245 -0
  58. package/dist/screens/identity/identity-routes.test.d.ts +1 -0
  59. package/dist/screens/identity/identity-routes.test.js +51 -0
  60. package/dist/screens/identity/identity-screen.d.ts +9 -0
  61. package/dist/screens/identity/identity-screen.js +143 -0
  62. package/dist/screens/identity/identity-screen.test.d.ts +1 -0
  63. package/dist/screens/identity/identity-screen.test.js +103 -0
  64. package/dist/screens/identity/index.d.ts +2 -0
  65. package/dist/screens/identity/index.js +6 -0
  66. package/dist/screens/identity/invite-contact-panel.d.ts +4 -0
  67. package/dist/screens/identity/invite-contact-panel.js +30 -0
  68. package/dist/screens/identity/profile-invite.d.ts +7 -0
  69. package/dist/screens/identity/profile-invite.js +62 -0
  70. package/dist/screens/invites/accept-invite.js +44 -2
  71. package/dist/screens/invites/accept-invite.test.d.ts +1 -0
  72. package/dist/screens/invites/accept-invite.test.js +103 -0
  73. package/dist/screens/invites/activity-view.d.ts +24 -0
  74. package/dist/screens/invites/activity-view.js +64 -0
  75. package/dist/screens/invites/activity-view.test.d.ts +1 -0
  76. package/dist/screens/invites/activity-view.test.js +108 -0
  77. package/dist/screens/invites/index.d.ts +1 -2
  78. package/dist/screens/invites/index.js +1 -3
  79. package/dist/screens/invites/invite-capture.d.ts +0 -2
  80. package/dist/screens/invites/invite-capture.js +1 -3
  81. package/dist/screens/invites/invite-classify.test.d.ts +1 -0
  82. package/dist/screens/invites/invite-classify.test.js +115 -0
  83. package/dist/screens/invites/invites-ipc.d.ts +1 -1
  84. package/dist/screens/invites/invites-ipc.js +5 -45
  85. package/dist/screens/invites/pending-invites-list.d.ts +35 -16
  86. package/dist/screens/invites/pending-invites-list.js +75 -30
  87. package/dist/screens/network-viewer/network-viewer-ipc.js +4 -59
  88. package/dist/screens/search/global-search.d.ts +1 -0
  89. package/dist/screens/search/global-search.js +2 -1
  90. package/dist/screens/settings/settings-page.js +25 -58
  91. package/dist/screens/setup-user.js +5 -5
  92. package/dist/screens/tools/tool-access-ipc.d.ts +2 -0
  93. package/dist/screens/tools/tool-access-ipc.js +8 -55
  94. package/dist/screens/welcome-modal.js +3 -2
  95. package/dist/system-apps/identity.app.d.ts +3 -0
  96. package/dist/system-apps/identity.app.js +10 -0
  97. package/dist/system-apps/index.d.ts +1 -4
  98. package/dist/system-apps/index.js +5 -17
  99. package/dist/tabs-layout/network-status-indicator.js +10 -3
  100. package/dist/tabs-layout/network-status-indicator.test.js +15 -0
  101. package/dist/tabs-layout/tabs-layout.js +3 -1
  102. package/dist/tabs-layout/tabs-state.d.ts +79 -1
  103. package/dist/tabs-layout/tabs-state.js +256 -79
  104. package/dist/tabs-layout/tabs-state.test.d.ts +1 -0
  105. package/dist/tabs-layout/tabs-state.test.js +110 -0
  106. package/package.json +3 -3
  107. package/src/components/markdown-editor/mention-node.ts +5 -1
  108. package/src/components/responsive-section-nav.test.tsx +88 -0
  109. package/src/components/responsive-section-nav.tsx +92 -0
  110. package/src/components/router.tsx +6 -9
  111. package/src/globals-hash.test.ts +19 -0
  112. package/src/globals.tsx +53 -13
  113. package/src/hooks/use-host-contract.ts +75 -0
  114. package/src/index.tsx +1 -0
  115. package/src/peers-services-registration.ts +6 -17
  116. package/src/root-layout/root-layout-host.test.tsx +4 -7
  117. package/src/root-layout/root-layout-host.tsx +9 -11
  118. package/src/screens/account/account-screen.test.tsx +61 -0
  119. package/src/screens/account/account-screen.tsx +116 -95
  120. package/src/screens/contacts/contact-details.tsx +4 -26
  121. package/src/screens/contacts/contact-list.test.tsx +59 -0
  122. package/src/screens/contacts/contact-list.tsx +246 -260
  123. package/src/screens/contacts/index.ts +0 -4
  124. package/src/screens/device-pairing/device-pairing.test.tsx +7 -9
  125. package/src/screens/device-pairing/device-pairing.tsx +19 -23
  126. package/src/screens/device-pairing/identity-ipc.ts +25 -0
  127. package/src/screens/device-pairing/use-device-pairing-state.ts +10 -33
  128. package/src/screens/devices/devices-list.test.tsx +68 -0
  129. package/src/screens/devices/devices-list.tsx +361 -0
  130. package/src/screens/devices/index.ts +1 -0
  131. package/src/screens/groups/group-details.tsx +8 -142
  132. package/src/screens/groups/group-invite-panel.tsx +2 -7
  133. package/src/screens/groups/group-list.test.tsx +44 -0
  134. package/src/screens/groups/group-list.tsx +227 -288
  135. package/src/screens/groups/group-members.tsx +3 -2
  136. package/src/screens/groups/index.ts +0 -5
  137. package/src/screens/identity/identity-routes.test.ts +70 -0
  138. package/src/screens/identity/identity-routes.ts +301 -0
  139. package/src/screens/identity/identity-screen.test.tsx +138 -0
  140. package/src/screens/identity/identity-screen.tsx +282 -0
  141. package/src/screens/identity/index.ts +2 -0
  142. package/src/screens/identity/invite-contact-panel.tsx +45 -0
  143. package/src/screens/identity/profile-invite.tsx +93 -0
  144. package/src/screens/invites/accept-invite.test.tsx +115 -0
  145. package/src/screens/invites/accept-invite.tsx +50 -3
  146. package/src/screens/invites/activity-view.test.tsx +127 -0
  147. package/src/screens/invites/activity-view.tsx +162 -0
  148. package/src/screens/invites/index.ts +1 -4
  149. package/src/screens/invites/invite-capture.ts +0 -3
  150. package/src/screens/invites/invite-classify.test.ts +129 -0
  151. package/src/screens/invites/invites-ipc.ts +6 -55
  152. package/src/screens/invites/pending-invites-list.tsx +94 -62
  153. package/src/screens/network-viewer/network-viewer-ipc.ts +8 -82
  154. package/src/screens/search/global-search.tsx +2 -1
  155. package/src/screens/settings/settings-page.tsx +53 -126
  156. package/src/screens/setup-user.tsx +5 -6
  157. package/src/screens/tools/tool-access-ipc.ts +13 -74
  158. package/src/screens/welcome-modal.tsx +3 -3
  159. package/src/system-apps/identity.app.ts +9 -0
  160. package/src/system-apps/index.ts +3 -12
  161. package/src/tabs-layout/network-status-indicator.test.tsx +22 -0
  162. package/src/tabs-layout/network-status-indicator.tsx +31 -0
  163. package/src/tabs-layout/tabs-layout.tsx +7 -1
  164. package/src/tabs-layout/tabs-state.test.ts +152 -0
  165. package/src/tabs-layout/tabs-state.ts +300 -83
  166. package/dist/screens/invites/invites-screen.d.ts +0 -9
  167. package/dist/screens/invites/invites-screen.js +0 -36
  168. package/dist/system-apps/account.app.d.ts +0 -2
  169. package/dist/system-apps/account.app.js +0 -8
  170. package/dist/system-apps/contacts.app.d.ts +0 -2
  171. package/dist/system-apps/contacts.app.js +0 -9
  172. package/dist/system-apps/groups.app.d.ts +0 -2
  173. package/dist/system-apps/groups.app.js +0 -9
  174. package/dist/system-apps/invites.app.d.ts +0 -2
  175. package/dist/system-apps/invites.app.js +0 -9
  176. package/src/screens/invites/invites-screen.tsx +0 -78
  177. package/src/system-apps/account.app.ts +0 -7
  178. package/src/system-apps/contacts.app.ts +0 -8
  179. package/src/system-apps/groups.app.ts +0 -8
  180. package/src/system-apps/invites.app.ts +0 -8
@@ -1,96 +1,13 @@
1
- import { type ICreatedInvite, rpcServerCalls } from "@peers-app/peers-sdk";
2
- import { useEffect, useState } from "react";
1
+ import { copyToClipboard, getMe, thisDeviceId } from "@peers-app/peers-sdk";
2
+ import { useState } from "react";
3
3
  import { LoadingIndicator } from "../../components/loading-indicator";
4
4
  import { Tooltip } from "../../components/tooltip";
5
- import { useObservable } from "../../hooks";
5
+ import { useObservable, usePromise } from "../../hooks";
6
6
  import {
7
7
  persistPeersServicesRegistration,
8
8
  registrationStatusPvar,
9
9
  } from "../../peers-services-registration";
10
- import { InviteShare } from "../invites/invite-share";
11
- import { inviteTools } from "../invites/invites-ipc";
12
-
13
- const PROFILE_QR_INFO = `### Your profile QR
14
-
15
- Anyone who scans this (or opens the link) can send you a contact request. Requests show up under **Contacts → Pending** and nothing happens until you confirm.
16
-
17
- ### Regenerate
18
-
19
- Regenerating invalidates this QR **and every other invite link you have issued**. Use it if the QR leaked somewhere you did not intend.`;
20
-
21
- /** Reusable profile invite with a regenerate control that bumps the invite epoch. */
22
- function ProfileInviteSection() {
23
- const [invite, setInvite] = useState<ICreatedInvite | undefined>(undefined);
24
- const [busy, setBusy] = useState(false);
25
- const [error, setError] = useState<string | null>(null);
26
-
27
- useEffect(() => {
28
- let cancelled = false;
29
- void inviteTools()
30
- .then((tools) => tools.createContactInvite({ profile: true }))
31
- .then(({ invite: created }) => {
32
- if (!cancelled) setInvite(created);
33
- })
34
- .catch((err) => {
35
- if (!cancelled) setError((err as Error).message || "Could not load your profile QR.");
36
- });
37
- return () => {
38
- cancelled = true;
39
- };
40
- }, []);
41
-
42
- const regenerate = async () => {
43
- if (
44
- !confirm(
45
- "Regenerate your profile QR? Every invite link you have issued so far will stop working.",
46
- )
47
- ) {
48
- return;
49
- }
50
- setBusy(true);
51
- setError(null);
52
- try {
53
- const tools = await inviteTools();
54
- const { invite: created } = await tools.regenerateProfileInvite();
55
- setInvite(created);
56
- } catch (err) {
57
- setError((err as Error).message || "Could not regenerate.");
58
- } finally {
59
- setBusy(false);
60
- }
61
- };
62
-
63
- return (
64
- <div className="card mb-3">
65
- <div className="card-body">
66
- <h5 className="card-title d-flex align-items-center gap-2">
67
- Profile QR
68
- <Tooltip markdownContent={PROFILE_QR_INFO} />
69
- </h5>
70
- {error ? <div className="alert alert-danger py-2">{error}</div> : null}
71
- {invite ? (
72
- <InviteShare
73
- invite={invite}
74
- description="Let people connect with you by scanning this or opening the link. You confirm each request."
75
- shareText="Connect with me on Peers:"
76
- />
77
- ) : !error ? (
78
- <LoadingIndicator />
79
- ) : null}
80
- <div className="text-center mt-3">
81
- <button
82
- type="button"
83
- className="btn btn-outline-danger btn-sm"
84
- onClick={() => void regenerate()}
85
- disabled={busy || !invite}
86
- >
87
- {busy ? "Regenerating…" : "Regenerate (revokes all invites)"}
88
- </button>
89
- </div>
90
- </div>
91
- </div>
92
- );
93
- }
10
+ import { identityTools } from "../device-pairing/identity-ipc";
94
11
 
95
12
  const INFO_TEXT = `### What does registering do?
96
13
 
@@ -109,8 +26,11 @@ Your **secret key never leaves your device**. Only your public key is shared wit
109
26
 
110
27
  No. Peers works fully in local/peer-to-peer mode without registration. Cloud features simply won't be available until you register.`;
111
28
 
29
+ /** Identity profile, public identifiers, device shortcuts, and service registration. */
112
30
  export function AccountScreen() {
113
31
  const [registrationStatus] = useObservable(registrationStatusPvar);
32
+ const [deviceId] = useObservable(thisDeviceId);
33
+ const me = usePromise(getMe);
114
34
  const [isRegistering, setIsRegistering] = useState(false);
115
35
  const [error, setError] = useState<string | null>(null);
116
36
  const [success, setSuccess] = useState<string | null>(null);
@@ -123,8 +43,8 @@ export function AccountScreen() {
123
43
  setSuccess(null);
124
44
 
125
45
  try {
126
- const token = await rpcServerCalls.registerWithPeersServices();
127
- persistPeersServicesRegistration(token);
46
+ const { token } = await (await identityTools()).registerWithPeersServices();
47
+ await persistPeersServicesRegistration(token);
128
48
  setSuccess("Account registered successfully.");
129
49
  } catch (err) {
130
50
  setError(`Registration failed: ${String(err)}`);
@@ -133,12 +53,19 @@ export function AccountScreen() {
133
53
  }
134
54
  };
135
55
 
56
+ if (!me) {
57
+ return <LoadingIndicator />;
58
+ }
59
+
136
60
  return (
137
- <div className="container-fluid p-3" style={{ maxWidth: 600 }}>
138
- <h4 className="mb-3 d-flex align-items-center gap-2">
139
- <i className="bi bi-person-circle" />
140
- Account
141
- </h4>
61
+ <div className="container-xxl p-3">
62
+ <div className="mb-3">
63
+ <h4 className="mb-1 d-flex align-items-center gap-2">
64
+ <i className="bi bi-person-circle" />
65
+ Account
66
+ </h4>
67
+ <div className="small text-muted">Your public identity and account-level services</div>
68
+ </div>
142
69
 
143
70
  {error && (
144
71
  <div className="alert alert-danger alert-dismissible" role="alert">
@@ -166,7 +93,61 @@ export function AccountScreen() {
166
93
  </div>
167
94
  )}
168
95
 
169
- <ProfileInviteSection />
96
+ <div className="card mb-3">
97
+ <div className="card-body d-flex align-items-center gap-3 flex-wrap">
98
+ <span
99
+ className="rounded-circle bg-primary-subtle text-primary-emphasis d-inline-flex align-items-center justify-content-center fs-4 fw-semibold"
100
+ style={{ width: 58, height: 58 }}
101
+ aria-hidden="true"
102
+ >
103
+ {profileInitials(me.name)}
104
+ </span>
105
+ <div className="flex-grow-1">
106
+ <h5 className="card-title mb-1">{me.name || "Unnamed user"}</h5>
107
+ <div className="small text-muted">Peers identity</div>
108
+ </div>
109
+ <div className="d-flex gap-2 flex-wrap">
110
+ <a href="#settings" className="btn btn-outline-secondary btn-sm">
111
+ Edit display name
112
+ </a>
113
+ <a href="#identity/devices" className="btn btn-outline-primary btn-sm">
114
+ Manage devices
115
+ </a>
116
+ <a href="#identity/devices/pair" className="btn btn-primary btn-sm">
117
+ Add device
118
+ </a>
119
+ </div>
120
+ </div>
121
+ </div>
122
+
123
+ <div className="card mb-3">
124
+ <div className="card-body">
125
+ <h5 className="card-title">Technical identity</h5>
126
+ <p className="small text-muted">
127
+ Share these values only when diagnosing identity or connection problems.
128
+ </p>
129
+ <TechnicalValue
130
+ label="User ID"
131
+ value={me.userId}
132
+ help="Your stable identifier across Peers."
133
+ />
134
+ <TechnicalValue
135
+ label="Signing key"
136
+ value={me.publicKey}
137
+ help="Other people use this public key to verify your signatures."
138
+ />
139
+ <TechnicalValue
140
+ label="Encryption key"
141
+ value={me.publicBoxKey}
142
+ help="Other people use this public key to encrypt data for you."
143
+ />
144
+ <TechnicalValue
145
+ label="Current device ID"
146
+ value={deviceId || "Not available"}
147
+ help="Identifies this installation on Peers networks."
148
+ />
149
+ </div>
150
+ </div>
170
151
 
171
152
  <div className="card">
172
153
  <div className="card-body">
@@ -238,3 +219,43 @@ export function AccountScreen() {
238
219
  </div>
239
220
  );
240
221
  }
222
+
223
+ function TechnicalValue(props: { label: string; value: string; help: string }) {
224
+ const [copied, setCopied] = useState(false);
225
+ const copy = async () => {
226
+ if (await copyToClipboard(props.value)) {
227
+ setCopied(true);
228
+ window.setTimeout(() => setCopied(false), 1500);
229
+ }
230
+ };
231
+ return (
232
+ <div className="border-top py-3">
233
+ <div className="d-flex align-items-center gap-2 mb-1">
234
+ <strong className="small">{props.label}</strong>
235
+ <Tooltip markdownContent={props.help} />
236
+ <button
237
+ type="button"
238
+ className="btn btn-link btn-sm ms-auto py-0"
239
+ onClick={() => void copy()}
240
+ aria-label={`Copy ${props.label}`}
241
+ >
242
+ <i className={`bi ${copied ? "bi-check-lg" : "bi-copy"} me-1`} />
243
+ {copied ? "Copied" : "Copy"}
244
+ </button>
245
+ </div>
246
+ <code className="small text-break user-select-all">{props.value}</code>
247
+ </div>
248
+ );
249
+ }
250
+
251
+ function profileInitials(name: string): string {
252
+ return (
253
+ name
254
+ .trim()
255
+ .split(/\s+/)
256
+ .filter(Boolean)
257
+ .slice(0, 2)
258
+ .map((part) => part[0]?.toLocaleUpperCase())
259
+ .join("") || "?"
260
+ );
261
+ }
@@ -2,11 +2,9 @@ import { getUserContext, type IDoc, type IUser, Users } from "@peers-app/peers-s
2
2
  import { Input } from "../../components/input";
3
3
  import { LoadingIndicator } from "../../components/loading-indicator";
4
4
  import { SaveButton } from "../../components/save-button";
5
- import { ScreenTabBody, Tabs } from "../../components/tabs";
5
+ import { Tabs } from "../../components/tabs";
6
6
  import { TrustLevelDropdown } from "../../components/trust-level-dropdown";
7
7
  import { usePromise } from "../../hooks";
8
- import { updateActiveTabTitle } from "../../tabs-layout/tabs-state";
9
- import { registerInternalPeersUI } from "../../ui-router/ui-loader";
10
8
 
11
9
  interface IProps {
12
10
  userId: string;
@@ -21,7 +19,6 @@ export const ContactDetails = (props: IProps) => {
21
19
  if (!contact) {
22
20
  throw new Error("Contact not found");
23
21
  }
24
- updateActiveTabTitle(contact.name || "Contact");
25
22
  return Users(userContext.userDataContext).initDoc(contact);
26
23
  },
27
24
  undefined,
@@ -33,7 +30,7 @@ export const ContactDetails = (props: IProps) => {
33
30
  }
34
31
 
35
32
  return (
36
- <div className="container-fluid p-3">
33
+ <div className="container-xxl p-3">
37
34
  <div className="d-flex">
38
35
  <div>
39
36
  <h4>
@@ -61,9 +58,9 @@ export const ContactDetails = (props: IProps) => {
61
58
  {
62
59
  name: "Info",
63
60
  content: (
64
- <ScreenTabBody>
61
+ <div>
65
62
  <ContactInfo contact={contact} />
66
- </ScreenTabBody>
63
+ </div>
67
64
  ),
68
65
  },
69
66
  ]}
@@ -124,22 +121,3 @@ const ContactInfo = (props: { contact: IDoc<IUser> }) => {
124
121
  </div>
125
122
  );
126
123
  };
127
-
128
- registerInternalPeersUI({
129
- peersUIId: "00contact00details00component",
130
- component: ContactDetails,
131
- routes: [
132
- {
133
- isMatch: (props, context) => {
134
- const match = context.path.match(/^contacts\/([a-zA-Z0-9]+)$/);
135
- if (match) {
136
- props.userId = match[1];
137
- return true;
138
- }
139
- return false;
140
- },
141
- uiCategory: "screen",
142
- priority: 2,
143
- },
144
- ],
145
- });
@@ -0,0 +1,59 @@
1
+ import { TrustLevel } from "@peers-app/peers-sdk";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { type IPersonSummary, PersonRow } from "./contact-list";
4
+
5
+ jest.mock("../../globals", () => ({
6
+ mainContentPath: jest.fn(),
7
+ }));
8
+
9
+ jest.mock("../../hooks", () => ({
10
+ usePromise: jest.fn(),
11
+ }));
12
+
13
+ jest.mock("../../ui-router/ui-loader", () => ({
14
+ registerInternalPeersUI: jest.fn(),
15
+ }));
16
+
17
+ jest.mock("../identity/invite-contact-panel", () => ({
18
+ InviteContactPanel: () => null,
19
+ }));
20
+
21
+ jest.mock("../identity/profile-invite", () => ({
22
+ ProfileInviteSection: () => null,
23
+ }));
24
+
25
+ jest.mock("../invites/activity-view", () => ({
26
+ PendingWorkLink: () => null,
27
+ }));
28
+
29
+ const person: IPersonSummary = {
30
+ user: {
31
+ userId: "abcdefghijklmnopqrstuvwxy",
32
+ name: "Alex",
33
+ publicKey: "signing-key",
34
+ publicBoxKey: "box-key",
35
+ },
36
+ isContact: true,
37
+ trustLevel: TrustLevel.Known,
38
+ sharedGroups: [{ groupId: "zyxwvutsrqponmlkjihgfedcb", name: "Neighbors" }],
39
+ };
40
+
41
+ describe("PersonRow", () => {
42
+ it("shows useful relationship context without exposing an ordinary user id", () => {
43
+ render(<PersonRow row={person} duplicateName={false} />);
44
+
45
+ expect(screen.getByText("Alex")).toBeInTheDocument();
46
+ expect(screen.getByText("Contact · Shared group: Neighbors")).toBeInTheDocument();
47
+ expect(screen.queryByText(person.user.userId)).not.toBeInTheDocument();
48
+ expect(screen.getByRole("link")).toHaveAttribute(
49
+ "href",
50
+ `#identity/people/${person.user.userId}`,
51
+ );
52
+ });
53
+
54
+ it("adds only a short discriminator when names collide", () => {
55
+ render(<PersonRow row={person} duplicateName />);
56
+
57
+ expect(screen.getByText(`…${person.user.userId.slice(-6)}`)).toBeInTheDocument();
58
+ });
59
+ });