@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
@@ -8,26 +8,26 @@ import {
8
8
  import { useCallback, useEffect, useState } from "react";
9
9
  import { inviteTools } from "./invites-ipc";
10
10
 
11
- /** Props for {@link PendingInvitesList}. */
12
- export interface PendingInvitesListProps {
13
- /** Only show rows of this type. */
14
- type?: IInvite["type"];
15
- /** Only show rows for this group. */
16
- groupId?: string;
17
- /** Statuses to include; defaults to pending only. */
18
- statuses?: InviteStatus[];
19
- /** Hide the section entirely when there is nothing to show. */
20
- hideWhenEmpty?: boolean;
21
- title?: string;
11
+ /** Filtered invite rows plus whether any non-pending row exists for the same type and group. */
12
+ export interface IInviteRowQuery {
13
+ /** Rows matching the status filter, newest first. */
14
+ rows: IInvite[];
15
+ /** True when at least one matching invite is not pending. */
16
+ hasHistory: boolean;
22
17
  }
23
18
 
24
- /** Subscribe to the personal `Invites` table and return rows matching the filter, newest first. */
19
+ /**
20
+ * Subscribe to the personal `Invites` table.
21
+ *
22
+ * @param filter Type, group, and statuses to include. Statuses default to pending.
23
+ * @returns Matching rows and whether any non-pending row exists, or `undefined` while loading.
24
+ */
25
25
  export function useInviteRows(filter: {
26
26
  type?: IInvite["type"];
27
27
  groupId?: string;
28
28
  statuses?: InviteStatus[];
29
- }): IInvite[] | undefined {
30
- const [rows, setRows] = useState<IInvite[] | undefined>(undefined);
29
+ }): IInviteRowQuery | undefined {
30
+ const [query, setQuery] = useState<IInviteRowQuery | undefined>(undefined);
31
31
  const statuses = filter.statuses ?? ["pending"];
32
32
  const statusKey = statuses.join(",");
33
33
 
@@ -39,11 +39,12 @@ export function useInviteRows(filter: {
39
39
  ...(filter.groupId ? { groupId: filter.groupId } : {}),
40
40
  });
41
41
  const wanted = new Set(statusKey.split(","));
42
- setRows(
43
- all
42
+ setQuery({
43
+ rows: all
44
44
  .filter((row) => wanted.has(row.status))
45
45
  .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()),
46
- );
46
+ hasHistory: all.some((row) => row.status !== "pending"),
47
+ });
47
48
  }, [filter.type, filter.groupId, statusKey]);
48
49
 
49
50
  useEffect(() => {
@@ -65,7 +66,7 @@ export function useInviteRows(filter: {
65
66
  };
66
67
  }, [refresh]);
67
68
 
68
- return rows;
69
+ return query;
69
70
  }
70
71
 
71
72
  function counterpartLabel(row: IInvite): string {
@@ -77,8 +78,20 @@ export function describeInviteRow(row: IInvite): { title: string; detail: string
77
78
  const who = counterpartLabel(row);
78
79
  const role = row.role !== undefined ? getRoleLabel(row.role) : undefined;
79
80
  const group = row.groupName ?? "a group";
81
+ if (row.type === "device") {
82
+ return {
83
+ title: row.direction === "in" ? `Device request from ${who}` : `Device invite for ${who}`,
84
+ detail: "Device admission actions are not available yet.",
85
+ };
86
+ }
80
87
  if (row.type === "contact") {
81
88
  if (row.direction === "in") {
89
+ if (row.recipientUserId) {
90
+ return {
91
+ title: `Connection request to ${who}`,
92
+ detail: "Waiting for them to confirm.",
93
+ };
94
+ }
82
95
  return { title: `${who} wants to connect`, detail: "Accept to add them as a contact." };
83
96
  }
84
97
  if (row.counterpart) {
@@ -90,13 +103,25 @@ export function describeInviteRow(row: IInvite): { title: string; detail: string
90
103
  };
91
104
  }
92
105
  if (row.direction === "in") {
106
+ if (row.recipientUserId) {
107
+ if (row.mode === "open") {
108
+ return {
109
+ title: `Joining ${group}`,
110
+ detail: "Completing automatically; no approval is required.",
111
+ };
112
+ }
113
+ return { title: `Request to join ${group}`, detail: `Waiting for ${who} to approve.` };
114
+ }
93
115
  if (row.mode === "direct") {
94
116
  return {
95
117
  title: `${who} invited you to ${group}`,
96
118
  detail: role ? `You would join as ${role}.` : "",
97
119
  };
98
120
  }
99
- return { title: `Joining ${group}`, detail: `Waiting for ${who} to approve.` };
121
+ return {
122
+ title: `${who} wants to join ${group}`,
123
+ detail: role ? `Requested as ${role}. Approve or deny.` : "Approve or deny.",
124
+ };
100
125
  }
101
126
  if (row.mode === "direct") {
102
127
  return { title: `Invited ${who} to ${group}`, detail: "Waiting for them to accept." };
@@ -113,26 +138,55 @@ export function describeInviteRow(row: IInvite): { title: string; detail: string
113
138
  };
114
139
  }
115
140
 
116
- type RowAction = "accept" | "decline" | "approve" | "deny" | "revoke";
141
+ /** Which Activity bucket a row belongs in. */
142
+ export type InviteBucket = "attention" | "waiting" | "history";
117
143
 
118
- function actionsFor(row: IInvite): RowAction[] {
144
+ /**
145
+ * Group an invite by what the signed-in user can do with it.
146
+ *
147
+ * Pending rows that need a decision are `attention`. Pending rows waiting on
148
+ * someone else, including links the user issued, are `waiting`.
149
+ *
150
+ * @param row - Invite row from the personal Invites table.
151
+ */
152
+ export function classifyInvite(row: IInvite): InviteBucket {
119
153
  if (row.status !== "pending") {
154
+ return "history";
155
+ }
156
+ return actionsForInvite(row).some((action) => action !== "revoke") ? "attention" : "waiting";
157
+ }
158
+
159
+ /** Action that the local user can take on an Activity row. */
160
+ export type InviteRowAction = "accept" | "decline" | "approve" | "deny" | "revoke";
161
+
162
+ /**
163
+ * Return actions that are valid from the local user's perspective.
164
+ *
165
+ * Inbound rows with `recipientUserId` were initiated locally and are waiting
166
+ * for that recipient. Actionable profile requests and join requests omit it.
167
+ */
168
+ export function actionsForInvite(row: IInvite): InviteRowAction[] {
169
+ if (row.status !== "pending" || row.type === "device") {
170
+ return [];
171
+ }
172
+ if (row.direction === "out") {
173
+ return ["revoke"];
174
+ }
175
+ if (row.recipientUserId) {
120
176
  return [];
121
177
  }
122
178
  if (row.direction === "in") {
123
- // Inbound rows I can act on: contact requests and direct group invites.
124
179
  if (row.type === "contact" || row.mode === "direct") {
125
180
  return ["accept", "decline"];
126
181
  }
127
- return []; // my own outstanding join request; nothing to do but wait
128
- }
129
- if (row.counterpart && row.type === "group" && row.mode !== "direct") {
130
- return ["approve", "deny"];
182
+ if (row.type === "group" && row.mode === "approve" && row.counterpart) {
183
+ return ["approve", "deny"];
184
+ }
131
185
  }
132
- return ["revoke"];
186
+ return [];
133
187
  }
134
188
 
135
- const ACTION_LABELS: Record<RowAction, { text: string; className: string }> = {
189
+ const ACTION_LABELS: Record<InviteRowAction, { text: string; className: string }> = {
136
190
  accept: { text: "Accept", className: "btn-primary" },
137
191
  decline: { text: "Decline", className: "btn-outline-secondary" },
138
192
  approve: { text: "Approve", className: "btn-primary" },
@@ -140,12 +194,13 @@ const ACTION_LABELS: Record<RowAction, { text: string; className: string }> = {
140
194
  revoke: { text: "Revoke", className: "btn-outline-danger" },
141
195
  };
142
196
 
143
- function InviteRow({ row }: { row: IInvite }) {
144
- const [busy, setBusy] = useState<RowAction | undefined>(undefined);
197
+ /** One invite row and the actions its direction and status allow. */
198
+ export function InviteRow({ row }: { row: IInvite }) {
199
+ const [busy, setBusy] = useState<InviteRowAction | undefined>(undefined);
145
200
  const [error, setError] = useState("");
146
201
  const { title, detail } = describeInviteRow(row);
147
202
 
148
- const run = async (action: RowAction) => {
203
+ const run = async (action: InviteRowAction) => {
149
204
  setBusy(action);
150
205
  setError("");
151
206
  try {
@@ -167,17 +222,22 @@ function InviteRow({ row }: { row: IInvite }) {
167
222
  <div className="list-group-item">
168
223
  <div className="d-flex justify-content-between align-items-center gap-2 flex-wrap">
169
224
  <div className="flex-grow-1">
170
- <div className="fw-semibold">{title}</div>
225
+ <div className="fw-semibold">
226
+ <span className="badge text-bg-light border me-2 text-capitalize">{row.type}</span>
227
+ {title}
228
+ </div>
171
229
  {detail ? <small className="text-muted d-block">{detail}</small> : null}
172
230
  {row.deliveryState === "queued" ? (
173
- <small className="text-warning d-block">Queued until they are reachable</small>
231
+ <small className="text-warning d-block">
232
+ Waiting for a route; Peers will retry automatically
233
+ </small>
174
234
  ) : null}
175
235
  {row.status !== "pending" ? (
176
236
  <small className="text-muted d-block text-capitalize">{row.status}</small>
177
237
  ) : null}
178
238
  </div>
179
239
  <div className="d-flex gap-1">
180
- {actionsFor(row).map((action) => (
240
+ {actionsForInvite(row).map((action) => (
181
241
  <button
182
242
  key={action}
183
243
  type="button"
@@ -194,31 +254,3 @@ function InviteRow({ row }: { row: IInvite }) {
194
254
  </div>
195
255
  );
196
256
  }
197
-
198
- /**
199
- * Live list of invite rows with the actions each one supports. Subscribes to the `Invites`
200
- * table directly, so it updates when replies arrive on any of the user's devices.
201
- */
202
- export function PendingInvitesList(props: PendingInvitesListProps) {
203
- const rows = useInviteRows(props);
204
- if (!rows) {
205
- return null;
206
- }
207
- if (rows.length === 0 && props.hideWhenEmpty) {
208
- return null;
209
- }
210
- return (
211
- <section className="mb-3">
212
- {props.title ? <h6 className="mb-2">{props.title}</h6> : null}
213
- {rows.length === 0 ? (
214
- <p className="text-muted small mb-0">No pending invites.</p>
215
- ) : (
216
- <div className="list-group">
217
- {rows.map((row) => (
218
- <InviteRow key={row.inviteId} row={row} />
219
- ))}
220
- </div>
221
- )}
222
- </section>
223
- );
224
- }
@@ -1,18 +1,11 @@
1
1
  import {
2
- createContractConsumer,
3
- getHostContractTransport,
4
2
  getNetworkControlContractDefinition,
5
3
  getNetworkDiagnosticsContractDefinition,
6
- type IContractTransport,
4
+ hostContractConsumer,
7
5
  type INetworkControlConsumer,
8
6
  type INetworkDiagnosticsConsumer,
9
- subscribeHostContractTransport,
10
- waitForHostContractTransport,
11
7
  } from "@peers-app/peers-sdk";
12
8
 
13
- /** Default wait for the host transport before treating diagnostics as unavailable. */
14
- const HOST_TRANSPORT_TIMEOUT_MS = 10_000;
15
-
16
9
  /** Electron-only capabilities intentionally excluded from portable network contracts. */
17
10
  export type NetworkViewerPlatformApi = {
18
11
  downloadRemoteDatabase?: (groupId: string, deviceId: string) => Promise<unknown>;
@@ -23,88 +16,21 @@ type WindowWithNetworkViewer = Window & {
23
16
  electronAPI?: { networkViewer?: NetworkViewerPlatformApi };
24
17
  };
25
18
 
26
- interface INetworkViewerConsumers {
27
- transport: IContractTransport;
28
- diagnostics: INetworkDiagnosticsConsumer;
29
- control: INetworkControlConsumer;
30
- }
31
-
32
- let cachedConsumers: INetworkViewerConsumers | undefined;
33
- let pendingConsumers: Promise<INetworkViewerConsumers> | undefined;
34
-
35
- function disposeCachedConsumers(): void {
36
- cachedConsumers?.diagnostics.dispose();
37
- cachedConsumers?.control.dispose();
38
- cachedConsumers = undefined;
39
- }
40
-
41
- subscribeHostContractTransport(
42
- (transport) => {
43
- if (cachedConsumers && cachedConsumers.transport !== transport) {
44
- disposeCachedConsumers();
45
- }
46
- },
47
- { emitCurrent: false },
19
+ const diagnostics = hostContractConsumer<INetworkDiagnosticsConsumer>(
20
+ getNetworkDiagnosticsContractDefinition(),
21
+ );
22
+ const control = hostContractConsumer<INetworkControlConsumer>(
23
+ getNetworkControlContractDefinition(),
48
24
  );
49
-
50
- async function loadConsumers(transport: IContractTransport): Promise<INetworkViewerConsumers> {
51
- disposeCachedConsumers();
52
- const diagnostics = createContractConsumer(
53
- getNetworkDiagnosticsContractDefinition(),
54
- transport,
55
- ) as unknown as INetworkDiagnosticsConsumer;
56
- const control = createContractConsumer(
57
- getNetworkControlContractDefinition(),
58
- transport,
59
- ) as unknown as INetworkControlConsumer;
60
- try {
61
- await diagnostics.loadingPromise;
62
- if (getHostContractTransport() !== transport) {
63
- diagnostics.dispose();
64
- control.dispose();
65
- // Clear before recursing so getConsumers can start a fresh in-flight memo for the
66
- // current transport, and so this promise's finally does not wipe that newer memo.
67
- pendingConsumers = undefined;
68
- return getConsumers();
69
- }
70
- cachedConsumers = { transport, diagnostics, control };
71
- return cachedConsumers;
72
- } catch (error) {
73
- diagnostics.dispose();
74
- control.dispose();
75
- throw error;
76
- }
77
- }
78
-
79
- async function getConsumers(): Promise<INetworkViewerConsumers> {
80
- const transport =
81
- getHostContractTransport() ??
82
- (await waitForHostContractTransport({ timeoutMs: HOST_TRANSPORT_TIMEOUT_MS }));
83
- if (cachedConsumers?.transport === transport) {
84
- return cachedConsumers;
85
- }
86
- if (pendingConsumers) {
87
- return pendingConsumers;
88
- }
89
- const loadPromise = loadConsumers(transport);
90
- pendingConsumers = loadPromise;
91
- try {
92
- return await loadPromise;
93
- } finally {
94
- if (pendingConsumers === loadPromise) {
95
- pendingConsumers = undefined;
96
- }
97
- }
98
- }
99
25
 
100
26
  /** Return a typed diagnostics consumer bound to the current local host transport. */
101
27
  export async function getNetworkDiagnosticsConsumer(): Promise<INetworkDiagnosticsConsumer> {
102
- return (await getConsumers()).diagnostics;
28
+ return diagnostics.get();
103
29
  }
104
30
 
105
31
  /** Return a typed control consumer bound to the current local host transport. */
106
32
  export async function getNetworkControlConsumer(): Promise<INetworkControlConsumer> {
107
- return (await getConsumers()).control;
33
+ return control.get();
108
34
  }
109
35
 
110
36
  /** Return the context-isolated desktop-only capability bridge, when available. */
@@ -39,9 +39,10 @@ const searchConfigs: Array<{
39
39
  { config: assistantMentionConfig, category: "Assistants", navigationPath: "assistants" },
40
40
  { config: workflowMentionConfig, category: "Workflows", navigationPath: "workflows" },
41
41
  { config: valueTypeMentionConfig, category: "Types", navigationPath: "peer-types" },
42
- { config: userMentionConfig, category: "Users", navigationPath: "contacts" },
42
+ { config: userMentionConfig, category: "People", navigationPath: "identity/people" },
43
43
  ];
44
44
 
45
+ /** Search system apps and known entities, then navigate to their canonical routes. */
45
46
  export function GlobalSearch() {
46
47
  const [_colorMode] = useObservable(colorMode);
47
48
  const [packages] = useObservable(allPackages);
@@ -1,14 +1,10 @@
1
1
  import {
2
2
  autoUpdatePeersCore,
3
- Devices,
4
3
  DISPLAY_NAME_MAX_LENGTH,
5
4
  getUserContext,
6
- type IDevice,
7
5
  packagesRootDir,
8
6
  reloadPackagesOnPageRefresh,
9
7
  rpcServerCalls,
10
- TrustLevel,
11
- thisDeviceId,
12
8
  Users,
13
9
  } from "@peers-app/peers-sdk";
14
10
  import type React from "react";
@@ -17,9 +13,10 @@ import { Input } from "../../components/input";
17
13
  import { Tabs } from "../../components/tabs";
18
14
  import { Tooltip } from "../../components/tooltip";
19
15
  import * as globals from "../../globals";
20
- import { useObservable, useObservableState, usePromise } from "../../hooks";
16
+ import { useObservable, usePromise } from "../../hooks";
21
17
  import { LayoutPicker } from "../../root-layout/layout-picker";
22
- import { SourceDevicePairing } from "../device-pairing";
18
+ import { identityTools } from "../device-pairing/identity-ipc";
19
+ import { IDENTITY_ACCOUNT_PATH, IDENTITY_PAIR_PATH } from "../identity/identity-routes";
23
20
  import { ColorModeDropdown } from "./color-mode-dropdown";
24
21
 
25
22
  /** Electron file picker attaches a filesystem path on `File`. */
@@ -46,24 +43,29 @@ function getAppVersion() {
46
43
  /** Application settings organized by user, system, and developer concerns. */
47
44
  export const SettingsPage: React.FC = () => {
48
45
  return (
49
- <div className="container-fluid">
50
- <Tabs
51
- tabs={[
52
- {
53
- name: "User",
54
- content: <UserSettingsTab />,
55
- active: true,
56
- },
57
- {
58
- name: "Appearance",
59
- content: <AppearanceSettingsTab />,
60
- },
61
- {
62
- name: "Advanced",
63
- content: <AdvancedSettingsTab />,
64
- },
65
- ]}
66
- />
46
+ <div className="d-flex flex-column h-100 overflow-hidden" style={{ minHeight: 0 }}>
47
+ <div className="container-fluid flex-grow-1 overflow-auto" style={{ minHeight: 0 }}>
48
+ <Tabs
49
+ tabs={[
50
+ {
51
+ name: "User",
52
+ content: <UserSettingsTab />,
53
+ active: true,
54
+ },
55
+ {
56
+ name: "Appearance",
57
+ content: <AppearanceSettingsTab />,
58
+ },
59
+ {
60
+ name: "Advanced",
61
+ content: <AdvancedSettingsTab />,
62
+ },
63
+ ]}
64
+ />
65
+ </div>
66
+ <footer className="flex-shrink-0 border-top px-3 py-2 small text-muted">
67
+ Peers {getAppVersion()}
68
+ </footer>
67
69
  </div>
68
70
  );
69
71
  };
@@ -72,6 +74,7 @@ const UserSettingsTab: React.FC = () => {
72
74
  return (
73
75
  <>
74
76
  <ProfileSection />
77
+ <IdentityAccountSection />
75
78
  <AddDeviceSection />
76
79
  <LogoutSection />
77
80
  </>
@@ -109,31 +112,11 @@ const AdvancedSettingsTab: React.FC = () => {
109
112
  };
110
113
 
111
114
  const ProfileSection: React.FC = () => {
112
- const [deviceId] = useObservable(thisDeviceId);
113
- const deviceName = useObservableState<string>("");
114
- const savedDeviceName = useObservableState<string>("");
115
-
116
115
  const me = usePromise(async () => {
117
116
  const userContext = await getUserContext();
118
117
  return Users(userContext.userDataContext).initDoc(globals.me);
119
118
  });
120
119
 
121
- // Load device name
122
- useEffect(() => {
123
- const loadDevice = async () => {
124
- if (!deviceId) return;
125
- try {
126
- const userContext = await getUserContext();
127
- const device = await Devices(userContext.userDataContext).get(deviceId);
128
- deviceName((device?.name || "").slice(0, DISPLAY_NAME_MAX_LENGTH));
129
- savedDeviceName(device?.name || "");
130
- } catch (err) {
131
- console.error("Error loading device name:", err);
132
- }
133
- };
134
- loadDevice();
135
- }, [deviceId, deviceName, savedDeviceName]);
136
-
137
120
  const handleSave = async () => {
138
121
  try {
139
122
  // Save user profile (only when it actually changed). Pass me.toJS() — the
@@ -147,22 +130,6 @@ const ProfileSection: React.FC = () => {
147
130
  me.qs.name(updatedUser.name);
148
131
  me.q(0); // Reset change counter
149
132
  }
150
-
151
- // Save device name if changed
152
- if (deviceId && deviceName() !== savedDeviceName()) {
153
- const userContext = await getUserContext();
154
- const devicesTable = Devices(userContext.userDataContext);
155
- const device: IDevice = (await devicesTable.get(deviceId)) || {
156
- deviceId,
157
- userId: userContext.userId,
158
- firstSeen: new Date(),
159
- lastSeen: new Date(),
160
- trustLevel: TrustLevel.NewDevice,
161
- };
162
- device.name = deviceName() || undefined;
163
- await devicesTable.save(device);
164
- savedDeviceName(deviceName());
165
- }
166
133
  } catch (err) {
167
134
  console.error("Error saving profile:", err);
168
135
  alert(`Failed to save: ${(err as Error).message}`);
@@ -171,103 +138,63 @@ const ProfileSection: React.FC = () => {
171
138
 
172
139
  // Subscribe to changes
173
140
  useObservable(me?.q);
174
- useObservable(deviceName);
175
141
 
176
142
  if (!me) {
177
143
  return null;
178
144
  }
179
145
 
180
- const hasChanges = !!me.q() || deviceName() !== savedDeviceName();
146
+ const hasChanges = !!me.q();
181
147
 
182
148
  return (
183
149
  <>
184
150
  <div className="d-flex align-items-center mb-3">
185
- <h5 className="mb-0 me-auto">Settings</h5>
151
+ <h5 className="mb-0 me-auto">Profile</h5>
186
152
  <button className="btn btn-primary btn-sm" onClick={handleSave} disabled={!hasChanges}>
187
153
  Save
188
154
  </button>
189
155
  </div>
190
156
 
191
157
  <div className="mb-3">
192
- <small className="form-label">Display Name:</small>
158
+ <label className="form-label small" htmlFor="settings-display-name">
159
+ Display name
160
+ </label>
193
161
  <Input
162
+ id="settings-display-name"
194
163
  value={me.qs.name}
195
164
  className="form-control"
196
165
  maxLength={DISPLAY_NAME_MAX_LENGTH}
197
166
  placeholder="Enter your name"
198
167
  />
199
168
  </div>
200
-
201
- <div className="mb-3">
202
- <small className="form-label">
203
- Device Name:
204
- <Tooltip markdownContent="Give this device a friendly name to identify it in your network." />
205
- </small>
206
- <Input
207
- value={deviceName}
208
- className="form-control"
209
- maxLength={DISPLAY_NAME_MAX_LENGTH}
210
- placeholder="e.g., My Laptop"
211
- />
212
- </div>
213
-
214
- <div className="mb-3">
215
- <small className="form-label">
216
- User ID:
217
- <Tooltip markdownContent="This uniquely identifies you to all other users." />
218
- </small>
219
- <Input value={me.userId} className="form-control" disabled />
220
- </div>
221
-
222
- <div className="row">
223
- <div className="col-md-6 mb-3">
224
- <small className="form-label">
225
- Public Key:
226
- <Tooltip markdownContent="This is your public key that other users will use to verify your signatures." />
227
- </small>
228
- <Input value={me.publicKey} className="form-control form-control-sm" disabled />
229
- </div>
230
- <div className="col-md-6 mb-3">
231
- <small className="form-label">
232
- Public Box Key:
233
- <Tooltip markdownContent="This is the public key that other users can use to encrypt data so that only you can open it." />
234
- </small>
235
- <Input value={me.publicBoxKey} className="form-control form-control-sm" disabled />
236
- </div>
237
- </div>
238
-
239
- <div className="mb-3">
240
- <small className="form-label">
241
- Device ID:
242
- <Tooltip markdownContent="This uniquely identifies this device on Peers networks." />
243
- </small>
244
- <Input value={deviceId || ""} className="form-control form-control-sm" disabled />
245
- </div>
246
-
247
- <div className="mb-3">
248
- <small className="form-label">App Version:</small>
249
- <Input value={getAppVersion()} className="form-control form-control-sm" disabled />
250
- </div>
251
169
  </>
252
170
  );
253
171
  };
254
172
 
173
+ const IdentityAccountSection: React.FC = () => {
174
+ return (
175
+ <section className="mt-4 pt-3 border-top">
176
+ <h6>Identity account</h6>
177
+ <p className="small text-muted">
178
+ View account identifiers, security keys, this device’s ID, and Peers Services registration.
179
+ </p>
180
+ <a href={`#${IDENTITY_ACCOUNT_PATH}`} className="btn btn-outline-primary">
181
+ View Identity account
182
+ </a>
183
+ </section>
184
+ );
185
+ };
186
+
255
187
  const AddDeviceSection: React.FC = () => {
256
- const [open, setOpen] = useState(false);
257
188
  return (
258
189
  <section className="mt-4 pt-3 border-top">
259
190
  <h6>Add another device</h6>
260
191
  <p className="small text-muted">
261
- On the new device choose “Sign in from another device”, then type the code it shows here and
262
- approve. Account access is transferred only after you approve.
192
+ On the new device choose “Sign in from another device”, then type the code it shows on
193
+ Identity → Devices and approve. Account access is transferred only after you approve.
263
194
  </p>
264
- {open ? (
265
- <SourceDevicePairing onClose={() => setOpen(false)} />
266
- ) : (
267
- <button type="button" className="btn btn-outline-primary" onClick={() => setOpen(true)}>
268
- Add another device
269
- </button>
270
- )}
195
+ <a href={`#${IDENTITY_PAIR_PATH}`} className="btn btn-outline-primary">
196
+ Add another device
197
+ </a>
271
198
  </section>
272
199
  );
273
200
  };
@@ -728,7 +655,7 @@ const LogoutSection: React.FC = () => {
728
655
  "Make sure you have your User ID and Secret Key saved securely, or you will lose access to your account.",
729
656
  );
730
657
  if (!confirmed) return;
731
- await rpcServerCalls.logout();
658
+ await (await identityTools()).signOut();
732
659
  }}
733
660
  >
734
661
  Logout