@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
@@ -4,12 +4,12 @@ import {
4
4
  newid,
5
5
  newKeys,
6
6
  openMessageWithPublicKey,
7
- rpcServerCalls,
8
7
  signMessageWithSecretKey,
9
8
  } from "@peers-app/peers-sdk";
10
9
  import { useState } from "react";
11
10
  import { useObservable } from "../hooks";
12
11
  import { DestinationDevicePairing } from "./device-pairing";
12
+ import { identityTools } from "./device-pairing/identity-ipc";
13
13
  import { colorMode } from "./settings/color-mode-dropdown";
14
14
 
15
15
  type SetupStep = "select-user-type" | "complete";
@@ -37,9 +37,9 @@ export const SetupUser = () => {
37
37
  const keys = newKeys();
38
38
  const _userId = newid();
39
39
 
40
- // Call setUserIdAndSecretKey. This triggers device init, which installs
41
- // peers-core into the personal context (S3-first, bundle fallback).
42
- await rpcServerCalls.setUserIdAndSecretKey(_userId, keys.secretKey);
40
+ // signIn triggers device init, which installs peers-core into the personal
41
+ // context (S3-first, bundle fallback).
42
+ await (await identityTools()).signIn({ userId: _userId, secretKey: keys.secretKey });
43
43
 
44
44
  // Success - reload the app (peers-services registration runs after Welcome modal)
45
45
  window.location.reload();
@@ -76,8 +76,7 @@ export const SetupUser = () => {
76
76
  throw new Error("Secret key validation failed");
77
77
  }
78
78
 
79
- // Call setUserIdAndSecretKey
80
- await rpcServerCalls.setUserIdAndSecretKey(userId, keys.secretKey);
79
+ await (await identityTools()).signIn({ userId, secretKey: keys.secretKey });
81
80
 
82
81
  // Success - reload the app
83
82
  window.location.reload();
@@ -1,88 +1,27 @@
1
1
  import {
2
- createContractConsumer,
3
- getHostContractTransport,
4
2
  getToolAccessControlContractDefinition,
5
- type IContractTransport,
3
+ hostContractConsumer,
4
+ type IHostContractConsumer,
6
5
  type IToolAccessControlConsumer,
7
- subscribeHostContractTransport,
8
- waitForHostContractTransport,
9
6
  } from "@peers-app/peers-sdk";
10
7
 
11
- const HOST_TRANSPORT_TIMEOUT_MS = 10_000;
12
-
13
- type CachedConsumer = {
14
- transport: IContractTransport;
15
- consumer: IToolAccessControlConsumer;
16
- };
17
-
18
- const cachedByContext = new Map<string, CachedConsumer>();
19
- const pendingByContext = new Map<string, Promise<IToolAccessControlConsumer>>();
20
-
21
- function disposeCachedConsumers(): void {
22
- for (const cached of cachedByContext.values()) {
23
- cached.consumer.dispose();
24
- }
25
- cachedByContext.clear();
26
- }
27
-
28
- subscribeHostContractTransport(
29
- (transport) => {
30
- if ([...cachedByContext.values()].some((cached) => cached.transport !== transport)) {
31
- disposeCachedConsumers();
32
- pendingByContext.clear();
33
- }
34
- },
35
- { emitCurrent: false },
36
- );
37
-
38
- async function loadConsumer(
39
- transport: IContractTransport,
40
- dataContextId: string,
41
- ): Promise<IToolAccessControlConsumer> {
42
- cachedByContext.get(dataContextId)?.consumer.dispose();
43
- cachedByContext.delete(dataContextId);
44
- const consumer = createContractConsumer(getToolAccessControlContractDefinition(), transport, {
45
- dataContextId,
46
- }) as unknown as IToolAccessControlConsumer;
47
- try {
48
- await consumer.loadingPromise;
49
- if (getHostContractTransport() !== transport) {
50
- consumer.dispose();
51
- pendingByContext.delete(dataContextId);
52
- return getToolAccessControlConsumer(dataContextId);
53
- }
54
- cachedByContext.set(dataContextId, { transport, consumer });
55
- return consumer;
56
- } catch (error) {
57
- consumer.dispose();
58
- throw error;
59
- }
60
- }
8
+ const consumers = new Map<string, IHostContractConsumer<IToolAccessControlConsumer>>();
61
9
 
62
10
  /**
63
11
  * Return a typed local-host consumer for Tool Access Control in `dataContextId`.
12
+ *
13
+ * One consumer is cached per data context and rebuilt when the host transport changes.
64
14
  */
65
15
  export async function getToolAccessControlConsumer(
66
16
  dataContextId: string,
67
17
  ): Promise<IToolAccessControlConsumer> {
68
- const transport =
69
- getHostContractTransport() ??
70
- (await waitForHostContractTransport({ timeoutMs: HOST_TRANSPORT_TIMEOUT_MS }));
71
- const cached = cachedByContext.get(dataContextId);
72
- if (cached?.transport === transport) {
73
- return cached.consumer;
74
- }
75
- const pending = pendingByContext.get(dataContextId);
76
- if (pending) {
77
- return pending;
78
- }
79
- const loadPromise = loadConsumer(transport, dataContextId);
80
- pendingByContext.set(dataContextId, loadPromise);
81
- try {
82
- return await loadPromise;
83
- } finally {
84
- if (pendingByContext.get(dataContextId) === loadPromise) {
85
- pendingByContext.delete(dataContextId);
86
- }
18
+ let handle = consumers.get(dataContextId);
19
+ if (!handle) {
20
+ handle = hostContractConsumer<IToolAccessControlConsumer>(
21
+ getToolAccessControlContractDefinition(),
22
+ { dataContextId: async () => dataContextId },
23
+ );
24
+ consumers.set(dataContextId, handle);
87
25
  }
26
+ return handle.get();
88
27
  }
@@ -5,7 +5,6 @@ import {
5
5
  getUserContext,
6
6
  hasShownWelcomeModal,
7
7
  type IDevice,
8
- rpcServerCalls,
9
8
  TrustLevel,
10
9
  thisDeviceId,
11
10
  Users,
@@ -14,6 +13,7 @@ import type React from "react";
14
13
  import { useEffect, useState } from "react";
15
14
  import { useObservable, usePromise } from "../hooks";
16
15
  import { persistPeersServicesRegistration } from "../peers-services-registration";
16
+ import { identityTools } from "./device-pairing/identity-ipc";
17
17
 
18
18
  interface WelcomeModalProps {
19
19
  colorMode: string;
@@ -22,8 +22,8 @@ interface WelcomeModalProps {
22
22
 
23
23
  async function tryRegisterPeersServicesAfterWelcome(): Promise<void> {
24
24
  try {
25
- const token = await rpcServerCalls.registerWithPeersServices();
26
- persistPeersServicesRegistration(token);
25
+ const { token } = await (await identityTools()).registerWithPeersServices();
26
+ await persistPeersServicesRegistration(token);
27
27
  } catch (err: unknown) {
28
28
  console.warn("Peers services registration after welcome modal:", err);
29
29
  }
@@ -0,0 +1,9 @@
1
+ import type { IAppNav } from "@peers-app/peers-sdk";
2
+
3
+ /** Identity system app: activity, people, groups, devices, and account. */
4
+ export const identityApp: IAppNav = {
5
+ name: "Identity",
6
+ displayName: "Identity",
7
+ iconClassName: "bi-person-badge",
8
+ navigationPath: "identity",
9
+ };
@@ -1,13 +1,10 @@
1
1
  import type { IAppNav } from "@peers-app/peers-sdk";
2
2
  import type { IPackageWithNavs } from "../ui-router/routes-loader";
3
3
 
4
- export { accountApp } from "./account.app";
5
4
  export { assistantsApp } from "./assistants.app";
6
5
  export { consoleLogsApp } from "./console-logs.app";
7
- export { contactsApp } from "./contacts.app";
8
6
  export { dataExplorerApp } from "./data-explorer.app";
9
- export { groupsApp } from "./groups.app";
10
- export { invitesApp } from "./invites.app";
7
+ export { identityApp } from "./identity.app";
11
8
  export { mobileSettingsApp } from "./mobile-settings.app";
12
9
  export { networkViewerApp } from "./network-viewer.app";
13
10
  export { packagesApp } from "./packages.app";
@@ -20,13 +17,10 @@ export { typesApp } from "./types.app";
20
17
  export { variablesApp } from "./variables.app";
21
18
  export { workflowsApp } from "./workflows.app";
22
19
 
23
- import { accountApp } from "./account.app";
24
20
  import { assistantsApp } from "./assistants.app";
25
21
  import { consoleLogsApp } from "./console-logs.app";
26
- import { contactsApp } from "./contacts.app";
27
22
  import { dataExplorerApp } from "./data-explorer.app";
28
- import { groupsApp } from "./groups.app";
29
- import { invitesApp } from "./invites.app";
23
+ import { identityApp } from "./identity.app";
30
24
  import { mobileSettingsApp } from "./mobile-settings.app";
31
25
  import { networkViewerApp } from "./network-viewer.app";
32
26
  import { packagesApp } from "./packages.app";
@@ -62,12 +56,9 @@ export const systemApps: IAppNav[] = [
62
56
  workflowsApp,
63
57
  assistantsApp,
64
58
  packagesApp,
65
- contactsApp,
66
- groupsApp,
67
- invitesApp,
59
+ identityApp,
68
60
 
69
61
  // User & Settings Apps
70
- accountApp,
71
62
  settingsApp,
72
63
  // Mobile Settings (only in React Native)
73
64
  ...(isReactNative() ? [mobileSettingsApp] : []),
@@ -2,6 +2,7 @@ import type { INetworkConnectionInfo, INetworkStatusSummary } from "@peers-app/p
2
2
  import { TrustLevel } from "@peers-app/peers-sdk";
3
3
  import { act, fireEvent, render, waitFor } from "@testing-library/react";
4
4
  import moment from "moment";
5
+ import { IDENTITY_ACCEPT_PATH, IDENTITY_ACCOUNT_PATH } from "../screens/identity/identity-routes";
5
6
  import {
6
7
  connectionIdentityLabel,
7
8
  formatRelativeSyncTime,
@@ -496,6 +497,27 @@ describe("NetworkStatusIndicator", () => {
496
497
  });
497
498
  });
498
499
 
500
+ it("offers shortcuts to use an invite and open the Identity account", () => {
501
+ mockViewState = { availability: "ready", status: status({ state: "synced" }) };
502
+ const view = render(<NetworkStatusIndicator />);
503
+
504
+ fireEvent.click(view.getByRole("button", { name: /Network synced/ }));
505
+ expect(
506
+ view.getByRole("button", { name: "Use Invite" }).querySelector(".bi-qr-code"),
507
+ ).toBeTruthy();
508
+ expect(
509
+ view.getByRole("button", { name: "Identity" }).querySelector(".bi-person-badge"),
510
+ ).toBeTruthy();
511
+ fireEvent.click(view.getByRole("button", { name: "Use Invite" }));
512
+ expect(goToTabPath).toHaveBeenCalledWith(IDENTITY_ACCEPT_PATH);
513
+ expect(view.queryByRole("dialog", { name: "Network status details" })).toBeNull();
514
+
515
+ fireEvent.click(view.getByRole("button", { name: /Network synced/ }));
516
+ fireEvent.click(view.getByRole("button", { name: "Identity" }));
517
+ expect(goToTabPath).toHaveBeenCalledWith(IDENTITY_ACCOUNT_PATH);
518
+ expect(view.queryByRole("dialog", { name: "Network status details" })).toBeNull();
519
+ });
520
+
499
521
  it("updates on rerender and opens details before navigating to Network Viewer", async () => {
500
522
  const view = render(<NetworkStatusIndicator />);
501
523
  expect(view.getByRole("button", { name: /loading/ })).toBeTruthy();
@@ -1,6 +1,7 @@
1
1
  import type { INetworkConnectionInfo, INetworkStatusSummary } from "@peers-app/peers-sdk";
2
2
  import moment from "moment";
3
3
  import { useEffect, useId, useRef, useState } from "react";
4
+ import { IDENTITY_ACCEPT_PATH, IDENTITY_ACCOUNT_PATH } from "../screens/identity/identity-routes";
4
5
  import { getNetworkDiagnosticsConsumer } from "../screens/network-viewer/network-viewer-ipc";
5
6
  import { goToTabPath } from "./tabs-state";
6
7
  import { type INetworkStatusViewState, useNetworkStatus } from "./use-network-status";
@@ -311,12 +312,16 @@ function NetworkStatusDetails({
311
312
  status,
312
313
  unavailable,
313
314
  connectionsLoadState,
315
+ onUseInvite,
316
+ onOpenIdentity,
314
317
  onOpenViewer,
315
318
  }: {
316
319
  detailsId: string;
317
320
  status?: INetworkStatusSummary;
318
321
  unavailable: boolean;
319
322
  connectionsLoadState: ConnectionsLoadState;
323
+ onUseInvite: () => void;
324
+ onOpenIdentity: () => void;
320
325
  onOpenViewer: () => void;
321
326
  }) {
322
327
  return (
@@ -328,6 +333,24 @@ function NetworkStatusDetails({
328
333
  style={{ top: "100%", width: "320px", zIndex: 1100 }}
329
334
  >
330
335
  <div className="card-body p-3">
336
+ <div className="d-flex gap-2 mb-3">
337
+ <button
338
+ type="button"
339
+ className="btn btn-sm btn-outline-primary flex-fill"
340
+ onClick={onUseInvite}
341
+ >
342
+ <i className="bi bi-qr-code me-1" aria-hidden="true" />
343
+ Use Invite
344
+ </button>
345
+ <button
346
+ type="button"
347
+ className="btn btn-sm btn-outline-primary flex-fill"
348
+ onClick={onOpenIdentity}
349
+ >
350
+ <i className="bi bi-person-badge me-1" aria-hidden="true" />
351
+ Identity
352
+ </button>
353
+ </div>
331
354
  <h6 className="card-title mb-2">Network status</h6>
332
355
  {unavailable || !status ? (
333
356
  <p className="small text-muted mb-3">
@@ -559,6 +582,14 @@ export function NetworkStatusIndicator({ compact = false }: { compact?: boolean
559
582
  status={viewState.status}
560
583
  unavailable={viewState.availability !== "ready"}
561
584
  connectionsLoadState={connectionsLoadState}
585
+ onUseInvite={() => {
586
+ setOpen(false);
587
+ goToTabPath(IDENTITY_ACCEPT_PATH);
588
+ }}
589
+ onOpenIdentity={() => {
590
+ setOpen(false);
591
+ goToTabPath(IDENTITY_ACCOUNT_PATH);
592
+ }}
562
593
  onOpenViewer={() => {
563
594
  setOpen(false);
564
595
  goToTabPath("network-viewer");
@@ -10,6 +10,7 @@ import { GroupSwitcher } from "../components/group-switcher";
10
10
  import { Router } from "../components/router";
11
11
  import { isDesktop } from "../globals";
12
12
  import { useObservable } from "../hooks";
13
+ import { isIdentityPath } from "../screens/identity/identity-routes";
13
14
  import { colorMode } from "../screens/settings/color-mode-dropdown";
14
15
  import { systemPackage } from "../system-apps";
15
16
  import { allPackages } from "../ui-router/routes-loader";
@@ -546,9 +547,14 @@ const TabContentRenderer = React.memo(
546
547
  // System apps use the main Router (which reads from mainContentPath)
547
548
  // Package apps use UIRouter with explicit path
548
549
  const isSystemApp = tab.packageId === "system-apps";
550
+ const identityApp = isSystemApp && isIdentityPath(tab.path);
549
551
 
550
552
  return (
551
- <div className={`h-100 overflow-auto ${isMobile ? "p-2" : "p-3"}`}>
553
+ <div
554
+ className={`h-100 ${identityApp ? "overflow-hidden" : "overflow-auto"} ${
555
+ identityApp ? "" : isMobile ? "p-2" : "p-3"
556
+ }`}
557
+ >
552
558
  {isSystemApp ? (
553
559
  <Router path={tab.path} />
554
560
  ) : (
@@ -0,0 +1,152 @@
1
+ jest.mock("../globals", () => {
2
+ const { observable } =
3
+ jest.requireActual<typeof import("@peers-app/peers-sdk")>("@peers-app/peers-sdk");
4
+ return { _mainContentPath: observable("") };
5
+ });
6
+
7
+ jest.mock("../ui-router/routes-loader", () => {
8
+ const { observable } =
9
+ jest.requireActual<typeof import("@peers-app/peers-sdk")>("@peers-app/peers-sdk");
10
+ return { allPackages: observable([]) };
11
+ });
12
+
13
+ jest.mock("@peers-app/peers-sdk", () => {
14
+ const actual = jest.requireActual<typeof import("@peers-app/peers-sdk")>("@peers-app/peers-sdk");
15
+ let nextId = 0;
16
+ const deviceVars = new Map<string, ReturnType<typeof actual.observable>>();
17
+ const userVars = new Map<string, ReturnType<typeof actual.observable>>();
18
+
19
+ function stored<T>(
20
+ map: Map<string, ReturnType<typeof actual.observable>>,
21
+ id: string,
22
+ initial: T,
23
+ ) {
24
+ const existing = map.get(id);
25
+ if (existing) {
26
+ return existing;
27
+ }
28
+ const value = actual.observable(initial);
29
+ Object.assign(value, { loadingPromise: Promise.resolve() });
30
+ map.set(id, value);
31
+ return value;
32
+ }
33
+
34
+ return {
35
+ ...actual,
36
+ newid: () => `tab${++nextId}`,
37
+ groupDeviceVar: (id: string, options?: { defaultValue?: unknown }) =>
38
+ stored(deviceVars, id, options?.defaultValue),
39
+ groupUserVar: (id: string, options?: { defaultValue?: unknown }) =>
40
+ stored(userVars, id, options?.defaultValue),
41
+ };
42
+ });
43
+
44
+ import { _mainContentPath } from "../globals";
45
+ import {
46
+ activeTabId,
47
+ activeTabs,
48
+ goToTabPath,
49
+ launcherApp,
50
+ migrateRecentPath,
51
+ navigateTo,
52
+ recentlyUsedApps,
53
+ restoreNavigationState,
54
+ type TabState,
55
+ } from "./tabs-state";
56
+
57
+ const PERSON_ID = "abcdefghijklmnopqrstuvwxy";
58
+
59
+ function tab(partial: Partial<TabState> & Pick<TabState, "tabId" | "path">): TabState {
60
+ return {
61
+ packageId: "system-apps",
62
+ title: partial.path,
63
+ iconClassName: "bi-circle",
64
+ ...partial,
65
+ };
66
+ }
67
+
68
+ describe("restoreNavigationState", () => {
69
+ it("merges legacy identity tabs into the active Identity tab", () => {
70
+ const restored = restoreNavigationState(
71
+ [
72
+ launcherApp,
73
+ tab({ tabId: "people", path: "contacts", title: "Contacts" }),
74
+ tab({ tabId: "groups", path: `groups/${PERSON_ID}`, title: "Groups" }),
75
+ tab({ tabId: "devices", path: "devices?action=pair", title: "Devices" }),
76
+ tab({ tabId: "account", path: "account", title: "Account" }),
77
+ ],
78
+ "groups",
79
+ );
80
+
81
+ expect(restored.tabs.map((entry) => entry.path)).toEqual(["", `identity/groups/${PERSON_ID}`]);
82
+ expect(restored.activeId).toBe("groups");
83
+ expect(restored.tabs[1]?.title).toBe("Identity");
84
+ expect(migrateRecentPath("contacts")).toBe("identity");
85
+ expect(migrateRecentPath("invites/accept")).toBe("identity");
86
+ expect(migrateRecentPath("account")).toBe("identity");
87
+ });
88
+ });
89
+
90
+ describe("navigateTo", () => {
91
+ beforeEach(async () => {
92
+ await activeTabs.loadingPromise;
93
+ activeTabs([launcherApp, tab({ tabId: "threads", path: "threads", title: "Threads" })]);
94
+ activeTabId("threads");
95
+ recentlyUsedApps([]);
96
+ _mainContentPath("threads");
97
+ });
98
+
99
+ it("updates the same app in place and preserves the query", () => {
100
+ navigateTo("threads/thread-1?draft=1");
101
+
102
+ expect(activeTabs()).toHaveLength(2);
103
+ expect(activeTabs().find((entry) => entry.tabId === "threads")?.path).toBe(
104
+ "threads/thread-1?draft=1",
105
+ );
106
+ expect(activeTabId()).toBe("threads");
107
+ expect(_mainContentPath()).toBe("threads/thread-1?draft=1");
108
+ });
109
+
110
+ it("opens another app without duplicating an existing tab", () => {
111
+ navigateTo(`contacts/${PERSON_ID}`);
112
+ const identityId = activeTabId();
113
+ activeTabId("threads");
114
+ navigateTo("identity/devices?foo=bar");
115
+
116
+ const identityTabs = activeTabs().filter((entry) => entry.path.startsWith("identity"));
117
+ expect(identityTabs).toHaveLength(1);
118
+ expect(identityTabs[0]?.path).toBe("identity/devices?foo=bar");
119
+ expect(activeTabId()).toBe(identityId);
120
+ expect(activeTabs().some((entry) => entry.tabId === "threads")).toBe(true);
121
+ });
122
+
123
+ it("opens the account alias inside the Identity tab", () => {
124
+ navigateTo("account");
125
+ const identityId = activeTabId();
126
+
127
+ expect(_mainContentPath()).toBe("identity/account");
128
+ navigateTo("identity/people");
129
+
130
+ expect(activeTabId()).toBe(identityId);
131
+ expect(activeTabs().filter((entry) => entry.path.startsWith("identity"))).toHaveLength(1);
132
+ expect(_mainContentPath()).toBe("identity/people");
133
+ });
134
+
135
+ it("rewrites invite acceptance onto the open Identity tab", () => {
136
+ navigateTo("identity/people");
137
+ const identityId = activeTabId();
138
+ navigateTo("invites/accept");
139
+
140
+ expect(activeTabId()).toBe(identityId);
141
+ expect(activeTabs().filter((entry) => entry.path.startsWith("identity"))).toHaveLength(1);
142
+ expect(_mainContentPath()).toBe("identity/activity/accept");
143
+ });
144
+
145
+ it("keeps the current section when the app root is opened again", () => {
146
+ navigateTo("identity/devices/pair");
147
+ goToTabPath("identity");
148
+
149
+ expect(_mainContentPath()).toBe("identity/devices/pair");
150
+ expect(recentlyUsedApps()).toEqual(["identity"]);
151
+ });
152
+ });