@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,12 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.switchToPreviousTab = exports.switchToNextTab = exports.closeCurrentTab = exports.openNewTab = exports.setSingleTabMode = exports.updateActiveTabTitle = exports.handleMainPathChanged = exports.initializedTabs = exports.recentlyUsedApps = exports.activeTabId = exports.activeTabs = exports.launcherApp = void 0;
4
+ exports.navigateTo = navigateTo;
4
5
  exports.goToTabPath = goToTabPath;
6
+ exports.restoreNavigationState = restoreNavigationState;
7
+ exports.migrateRecentPath = migrateRecentPath;
5
8
  exports.determineAppFromPath = determineAppFromPath;
6
9
  const peers_sdk_1 = require("@peers-app/peers-sdk");
7
10
  const globals_1 = require("../globals");
11
+ const identity_routes_1 = require("../screens/identity/identity-routes");
8
12
  const system_apps_1 = require("../system-apps");
9
13
  const routes_loader_1 = require("../ui-router/routes-loader");
14
+ /** The app launcher tab. It is always present and cannot be closed. */
10
15
  exports.launcherApp = {
11
16
  tabId: "launcher",
12
17
  packageId: "launcher",
@@ -24,109 +29,163 @@ const _persistentActiveTabId = (0, peers_sdk_1.groupDeviceVar)("activeTabId", {
24
29
  const _persistentRecentlyUsedApps = (0, peers_sdk_1.groupUserVar)("recentlyUsedApps", {
25
30
  defaultValue: [],
26
31
  });
32
+ let finishNavigationRestore = () => { };
33
+ const navigationReady = new Promise((resolve) => {
34
+ finishNavigationRestore = resolve;
35
+ });
27
36
  // In-memory observables for UI state (loaded once, then write-through to persistent vars)
37
+ /** Open tabs. Writes through to device storage after the initial restore. */
28
38
  exports.activeTabs = (() => {
29
39
  const obs = (0, peers_sdk_1.observable)([exports.launcherApp]);
30
40
  // Write-through to persistent var on change
31
41
  obs.subscribe((value) => {
32
42
  _persistentActiveTabs(value);
33
43
  });
34
- // Load initial value once
35
- const loadingPromise = _persistentActiveTabs.loadingPromise.then(() => {
36
- obs(_persistentActiveTabs());
37
- });
38
- return Object.assign(obs, { loadingPromise });
44
+ return Object.assign(obs, { loadingPromise: navigationReady });
39
45
  })();
46
+ /** Id of the tab whose path is showing. */
40
47
  exports.activeTabId = (() => {
41
48
  const obs = (0, peers_sdk_1.observable)("launcher");
42
49
  // Write-through to persistent var on change
43
50
  obs.subscribe((value) => {
44
51
  _persistentActiveTabId(value);
45
52
  });
46
- // Load initial value once
47
- const loadingPromise = _persistentActiveTabId.loadingPromise.then(() => {
48
- obs(_persistentActiveTabId());
49
- });
50
- return Object.assign(obs, { loadingPromise });
53
+ return Object.assign(obs, { loadingPromise: navigationReady });
51
54
  })();
55
+ /** App roots recently opened from the launcher, newest first. */
52
56
  exports.recentlyUsedApps = (() => {
53
57
  const obs = (0, peers_sdk_1.observable)([]);
54
58
  // Write-through to persistent var on change
55
59
  obs.subscribe((value) => {
56
60
  _persistentRecentlyUsedApps(value);
57
61
  });
58
- // Load initial value once
59
- const loadingPromise = _persistentRecentlyUsedApps.loadingPromise.then(() => {
60
- obs(_persistentRecentlyUsedApps());
61
- });
62
- return Object.assign(obs, { loadingPromise });
62
+ return Object.assign(obs, { loadingPromise: navigationReady });
63
63
  })();
64
- exports.initializedTabs = new Set();
65
- function goToTabPath(path) {
66
- const tab = (0, exports.activeTabs)().find((t) => t.path === path);
67
- if (tab) {
68
- (0, exports.activeTabId)(tab.tabId);
64
+ void Promise.all([
65
+ _persistentActiveTabs.loadingPromise,
66
+ _persistentActiveTabId.loadingPromise,
67
+ _persistentRecentlyUsedApps.loadingPromise,
68
+ ]).then(() => {
69
+ const restored = restoreNavigationState(_persistentActiveTabs(), _persistentActiveTabId());
70
+ (0, exports.activeTabs)(restored.tabs);
71
+ if ((0, exports.activeTabId)() !== restored.activeId) {
72
+ (0, exports.activeTabId)(restored.activeId);
69
73
  }
70
- else {
71
- const appInfo = determineAppFromPath(path);
72
- if (appInfo) {
73
- (0, exports.openNewTab)({
74
- path: path,
75
- title: appInfo.navItem.displayName || appInfo.navItem.name,
76
- packageId: appInfo.package.packageId,
77
- iconClassName: appInfo.navItem.iconClassName,
78
- });
74
+ (0, exports.recentlyUsedApps)(migrateRecentPaths(_persistentRecentlyUsedApps()));
75
+ finishNavigationRestore();
76
+ });
77
+ /** Tab ids whose bodies have been mounted at least once. */
78
+ exports.initializedTabs = new Set();
79
+ /**
80
+ * Open a path in the tab that owns its app.
81
+ *
82
+ * Legacy identity aliases are rewritten first. A route owned by the current
83
+ * app replaces that tab's path. A route owned by another open app activates
84
+ * that app's tab. Otherwise a new tab is opened.
85
+ *
86
+ * @param rawPath - Requested route, with or without a query string.
87
+ * @param options - App-root behavior. Launcher opens preserve the current section.
88
+ */
89
+ function navigateTo(rawPath, options) {
90
+ const requestedPathname = (0, identity_routes_1.parseRoute)(rawPath).pathname;
91
+ if (!requestedPathname) {
92
+ const launcher = (0, exports.activeTabs)().find((tab) => tab.tabId === "launcher") ?? (0, exports.activeTabs)()[0];
93
+ if (launcher && (0, exports.activeTabId)() !== launcher.tabId) {
94
+ (0, exports.activeTabId)(launcher.tabId);
79
95
  }
80
- else {
81
- // No app matched — navigate by updating the active tab's path (e.g. deep link).
82
- // Must replace the tab object (immutable): TabContentRenderer is memoized on tab.path,
83
- // and mutating in place makes prev/next props compare equal so Router never re-renders.
84
- const currentTabId = (0, exports.activeTabId)();
85
- const currentTabs = (0, exports.activeTabs)();
86
- const idx = currentTabs.findIndex((t) => t.tabId === currentTabId);
87
- if (idx >= 0 && currentTabs[idx].path !== "") {
88
- const prev = currentTabs[idx];
89
- const newTabs = [...currentTabs];
90
- newTabs[idx] = { ...prev, path };
91
- (0, exports.activeTabs)(newTabs);
92
- if (prev.packageId !== "launcher") {
93
- (0, globals_1._mainContentPath)(path);
94
- }
95
- }
96
- else {
97
- (0, exports.openNewTab)({
98
- path: path,
99
- title: "New Tab",
100
- packageId: "system-apps",
101
- iconClassName: "bi-question-circle",
102
- });
96
+ return;
97
+ }
98
+ const next = (0, identity_routes_1.canonicalizeRoute)(rawPath);
99
+ const app = determineAppFromPath(next);
100
+ if (options?.preserveSectionOnAppRoot && app && isAppRootRequest(requestedPathname, app)) {
101
+ const existing = findTabForApp(app);
102
+ if (existing) {
103
+ if ((0, exports.activeTabId)() !== existing.tabId) {
104
+ (0, exports.activeTabId)(existing.tabId);
103
105
  }
106
+ rememberRecentApp(existing.path);
107
+ return;
104
108
  }
105
109
  }
106
- }
107
- const handleMainPathChanged = (oldPath, newPath, setNewMainPath) => {
108
- const oldPathPart = oldPath.toLowerCase().trim().split("?")[0];
109
- const newPathPart = newPath.toLowerCase().trim().split("?")[0];
110
- const currentTab = (0, exports.activeTabs)().find((t) => t.tabId === (0, exports.activeTabId)());
111
- if (oldPathPart === newPathPart || (currentTab && currentTab.path === newPath)) {
112
- // nothing to do
113
- setNewMainPath(newPath);
110
+ if (!app || app.package.packageId === "launcher") {
111
+ openUnresolved(next);
114
112
  return;
115
113
  }
116
- const appInfo = determineAppFromPath(newPath);
117
- if (appInfo && oldPath !== "") {
118
- (0, exports.openNewTab)({
119
- path: newPath,
120
- title: appInfo.navItem.displayName || appInfo.navItem.name,
121
- packageId: appInfo.package.packageId,
122
- iconClassName: appInfo.navItem.iconClassName,
123
- });
114
+ const current = (0, exports.activeTabs)().find((tab) => tab.tabId === (0, exports.activeTabId)());
115
+ const currentApp = current ? determineAppFromPath(current.path) : undefined;
116
+ if (current && current.packageId !== "launcher" && currentApp && sameApp(currentApp, app)) {
117
+ showTab(current.tabId, next);
118
+ return;
124
119
  }
125
- else {
126
- setNewMainPath(newPath);
120
+ const existing = findTabForApp(app);
121
+ if (existing) {
122
+ showTab(existing.tabId, next);
123
+ return;
127
124
  }
125
+ (0, exports.openNewTab)({
126
+ path: next,
127
+ title: app.navItem.displayName || app.navItem.name,
128
+ packageId: app.package.packageId,
129
+ iconClassName: app.navItem.iconClassName,
130
+ });
131
+ }
132
+ /**
133
+ * Open an app or path from the launcher.
134
+ *
135
+ * Choosing an app that is already open keeps the section the user was on.
136
+ *
137
+ * @param path - App root or deep link.
138
+ */
139
+ function goToTabPath(path) {
140
+ navigateTo(path, { preserveSectionOnAppRoot: true });
141
+ }
142
+ /**
143
+ * Apply a {@link mainContentPath} write to the open tabs.
144
+ *
145
+ * @param _oldPath - Previous route. Retained for existing call sites.
146
+ * @param newPath - Requested route.
147
+ * @param _setNewMainPath - Direct path setter. Navigation updates the path itself.
148
+ */
149
+ const handleMainPathChanged = (_oldPath, newPath, _setNewMainPath) => {
150
+ navigateTo(newPath);
128
151
  };
129
152
  exports.handleMainPathChanged = handleMainPathChanged;
153
+ /**
154
+ * Rewrite stored tabs onto canonical routes.
155
+ *
156
+ * Contacts, Groups, and Devices tabs from before the Identity app become one
157
+ * Identity tab. The active tab is kept when it is one of those duplicates.
158
+ *
159
+ * @param tabs - Persisted tabs.
160
+ * @param activeId - Persisted active tab id.
161
+ */
162
+ function restoreNavigationState(tabs, activeId) {
163
+ const migrated = (tabs.length > 0 ? tabs : [exports.launcherApp]).map(migrateStoredTab);
164
+ const merged = mergeIdentityTabs(migrated, activeId);
165
+ const activeExists = merged.tabs.some((tab) => tab.tabId === merged.activeId);
166
+ return {
167
+ tabs: merged.tabs,
168
+ activeId: activeExists ? merged.activeId : merged.tabs[0].tabId,
169
+ };
170
+ }
171
+ /**
172
+ * Map a stored recent-app path onto the current app root.
173
+ *
174
+ * @param path - Persisted recent path, possibly a legacy alias.
175
+ */
176
+ function migrateRecentPath(path) {
177
+ const canonical = (0, identity_routes_1.canonicalizeRoute)(path);
178
+ if ((0, identity_routes_1.isIdentityPath)(canonical)) {
179
+ return identity_routes_1.IDENTITY_APP_PATH;
180
+ }
181
+ return canonical;
182
+ }
183
+ /**
184
+ * Resolve the app that owns a route.
185
+ *
186
+ * @param path - Route, with or without a query string.
187
+ * @returns The matching app, the launcher for an empty path, or `undefined`.
188
+ */
130
189
  function determineAppFromPath(path) {
131
190
  const launcherInfo = {
132
191
  navItem: {
@@ -192,7 +251,123 @@ function determineAppFromPath(path) {
192
251
  console.warn(`No app found for path: ${path}`);
193
252
  return undefined;
194
253
  }
195
- // Utility function for detail screens to update the active tab's title
254
+ function appKey(app) {
255
+ const nav = app.navItem.navigationPath || app.navItem.name.toLowerCase();
256
+ return `${app.package.packageId}:${nav}`;
257
+ }
258
+ function sameApp(left, right) {
259
+ return appKey(left) === appKey(right);
260
+ }
261
+ function isAppRootRequest(pathname, app) {
262
+ const nav = (app.navItem.navigationPath || app.navItem.name).toLowerCase();
263
+ return pathname.toLowerCase() === nav;
264
+ }
265
+ function findTabForApp(app) {
266
+ const key = appKey(app);
267
+ return (0, exports.activeTabs)().find((tab) => {
268
+ const info = determineAppFromPath(tab.path);
269
+ return info ? appKey(info) === key : false;
270
+ });
271
+ }
272
+ /**
273
+ * Replace a tab's path immutably so the memoized tab body re-renders.
274
+ * Activating a different tab lets the tab subscription publish the path.
275
+ */
276
+ function showTab(tabId, path) {
277
+ const tabs = (0, exports.activeTabs)();
278
+ const index = tabs.findIndex((tab) => tab.tabId === tabId);
279
+ if (index < 0) {
280
+ return;
281
+ }
282
+ const current = tabs[index];
283
+ if (current.path !== path) {
284
+ const nextTabs = [...tabs];
285
+ nextTabs[index] = { ...current, path };
286
+ (0, exports.activeTabs)(nextTabs);
287
+ }
288
+ if ((0, exports.activeTabId)() !== tabId) {
289
+ (0, exports.activeTabId)(tabId);
290
+ }
291
+ else if ((0, globals_1._mainContentPath)() !== path) {
292
+ (0, globals_1._mainContentPath)(path);
293
+ }
294
+ rememberRecentApp(path);
295
+ }
296
+ function openUnresolved(path) {
297
+ const current = (0, exports.activeTabs)().find((tab) => tab.tabId === (0, exports.activeTabId)());
298
+ if (current && current.packageId === "system-apps") {
299
+ showTab(current.tabId, path);
300
+ return;
301
+ }
302
+ (0, exports.openNewTab)({
303
+ path,
304
+ title: "New Tab",
305
+ packageId: "system-apps",
306
+ iconClassName: "bi-question-circle",
307
+ });
308
+ }
309
+ function rememberRecentApp(path) {
310
+ const app = determineAppFromPath((0, identity_routes_1.canonicalizeRoute)(path));
311
+ if (!app || app.package.packageId === "launcher") {
312
+ return;
313
+ }
314
+ const recentPath = app.package.packageId === system_apps_1.systemPackage.packageId ? app.navItem.navigationPath || path : path;
315
+ if (!recentPath) {
316
+ return;
317
+ }
318
+ const recent = (0, exports.recentlyUsedApps)();
319
+ const filtered = recent.filter((entry) => entry !== recentPath);
320
+ const next = [recentPath, ...filtered].slice(0, 8);
321
+ if (next.length === recent.length && next.every((entry, index) => entry === recent[index])) {
322
+ return;
323
+ }
324
+ (0, exports.recentlyUsedApps)(next);
325
+ }
326
+ function migrateStoredTab(tab) {
327
+ const path = (0, identity_routes_1.canonicalizeRoute)(tab.path);
328
+ if (path === tab.path) {
329
+ return tab;
330
+ }
331
+ const app = determineAppFromPath(path);
332
+ const identity = app?.navItem.navigationPath === identity_routes_1.IDENTITY_APP_PATH;
333
+ return {
334
+ ...tab,
335
+ path,
336
+ packageId: app?.package.packageId ?? tab.packageId,
337
+ title: identity ? app?.navItem.displayName || app?.navItem.name || tab.title : tab.title,
338
+ iconClassName: identity ? app?.navItem.iconClassName || tab.iconClassName : tab.iconClassName,
339
+ };
340
+ }
341
+ function mergeIdentityTabs(tabs, activeId) {
342
+ const identityTabs = tabs.filter((tab) => (0, identity_routes_1.isIdentityPath)(tab.path));
343
+ if (identityTabs.length <= 1) {
344
+ return { tabs, activeId };
345
+ }
346
+ const keeper = identityTabs.find((tab) => tab.tabId === activeId) ?? identityTabs[0];
347
+ const remove = new Set(identityTabs.filter((tab) => tab.tabId !== keeper.tabId).map((tab) => tab.tabId));
348
+ return {
349
+ tabs: tabs.filter((tab) => !remove.has(tab.tabId)),
350
+ activeId: remove.has(activeId) ? keeper.tabId : activeId,
351
+ };
352
+ }
353
+ function migrateRecentPaths(paths) {
354
+ const seen = new Set();
355
+ const next = [];
356
+ for (const path of paths) {
357
+ const migrated = migrateRecentPath(path);
358
+ if (!migrated || seen.has(migrated)) {
359
+ continue;
360
+ }
361
+ seen.add(migrated);
362
+ next.push(migrated);
363
+ }
364
+ return next.slice(0, 8);
365
+ }
366
+ /**
367
+ * Set the active tab's title.
368
+ *
369
+ * @param newTitle - Title shown on the tab strip.
370
+ */
196
371
  const updateActiveTabTitle = (newTitle) => {
197
372
  // TODO Fix this to use a more reliable update mechanism
198
373
  // using setTimeout to get around a race condition for setting title on new tabs
@@ -222,7 +397,12 @@ const setSingleTabMode = (enabled) => {
222
397
  singleTabModeEnabled = enabled;
223
398
  };
224
399
  exports.setSingleTabMode = setSingleTabMode;
225
- // Global function to open content in new tab
400
+ /**
401
+ * Open a tab, or activate an existing tab with the same package and path.
402
+ *
403
+ * @param tab - Tab contents. `tabId` is assigned here.
404
+ * @param forceNew - When true, always append a tab instead of reusing one.
405
+ */
226
406
  const openNewTab = (tab, forceNew = false) => {
227
407
  const newTab = {
228
408
  ...tab,
@@ -255,15 +435,12 @@ const openNewTab = (tab, forceNew = false) => {
255
435
  (0, exports.activeTabs)([...currentTabs, newTab]);
256
436
  (0, exports.activeTabId)(newTab.tabId);
257
437
  }
258
- // Update recently used (track by path instead of packageId)
259
438
  if (tab.path && tab.packageId !== "launcher") {
260
- const recent = (0, exports.recentlyUsedApps)();
261
- const filtered = recent.filter((path) => path !== tab.path);
262
- (0, exports.recentlyUsedApps)([tab.path, ...filtered].slice(0, 8));
439
+ rememberRecentApp(tab.path);
263
440
  }
264
441
  };
265
442
  exports.openNewTab = openNewTab;
266
- // Close the current active tab (but not launcher)
443
+ /** Close the active tab. The launcher tab stays open. */
267
444
  const closeCurrentTab = () => {
268
445
  const currentTabs = (0, exports.activeTabs)();
269
446
  const _activeTabId = (0, exports.activeTabId)();
@@ -285,7 +462,7 @@ const closeCurrentTab = () => {
285
462
  }
286
463
  };
287
464
  exports.closeCurrentTab = closeCurrentTab;
288
- // Navigate to next tab
465
+ /** Activate the next open tab, wrapping around. */
289
466
  const switchToNextTab = () => {
290
467
  const currentTabs = (0, exports.activeTabs)();
291
468
  const activeTab = (0, exports.activeTabId)();
@@ -296,7 +473,7 @@ const switchToNextTab = () => {
296
473
  (0, exports.activeTabId)(currentTabs[nextIndex].tabId);
297
474
  };
298
475
  exports.switchToNextTab = switchToNextTab;
299
- // Navigate to previous tab
476
+ /** Activate the previous open tab, wrapping around. */
300
477
  const switchToPreviousTab = () => {
301
478
  const currentTabs = (0, exports.activeTabs)();
302
479
  const activeTab = (0, exports.activeTabId)();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ jest.mock("../globals", () => {
4
+ const { observable } = jest.requireActual("@peers-app/peers-sdk");
5
+ return { _mainContentPath: observable("") };
6
+ });
7
+ jest.mock("../ui-router/routes-loader", () => {
8
+ const { observable } = jest.requireActual("@peers-app/peers-sdk");
9
+ return { allPackages: observable([]) };
10
+ });
11
+ jest.mock("@peers-app/peers-sdk", () => {
12
+ const actual = jest.requireActual("@peers-app/peers-sdk");
13
+ let nextId = 0;
14
+ const deviceVars = new Map();
15
+ const userVars = new Map();
16
+ function stored(map, id, initial) {
17
+ const existing = map.get(id);
18
+ if (existing) {
19
+ return existing;
20
+ }
21
+ const value = actual.observable(initial);
22
+ Object.assign(value, { loadingPromise: Promise.resolve() });
23
+ map.set(id, value);
24
+ return value;
25
+ }
26
+ return {
27
+ ...actual,
28
+ newid: () => `tab${++nextId}`,
29
+ groupDeviceVar: (id, options) => stored(deviceVars, id, options?.defaultValue),
30
+ groupUserVar: (id, options) => stored(userVars, id, options?.defaultValue),
31
+ };
32
+ });
33
+ const globals_1 = require("../globals");
34
+ const tabs_state_1 = require("./tabs-state");
35
+ const PERSON_ID = "abcdefghijklmnopqrstuvwxy";
36
+ function tab(partial) {
37
+ return {
38
+ packageId: "system-apps",
39
+ title: partial.path,
40
+ iconClassName: "bi-circle",
41
+ ...partial,
42
+ };
43
+ }
44
+ describe("restoreNavigationState", () => {
45
+ it("merges legacy identity tabs into the active Identity tab", () => {
46
+ const restored = (0, tabs_state_1.restoreNavigationState)([
47
+ tabs_state_1.launcherApp,
48
+ tab({ tabId: "people", path: "contacts", title: "Contacts" }),
49
+ tab({ tabId: "groups", path: `groups/${PERSON_ID}`, title: "Groups" }),
50
+ tab({ tabId: "devices", path: "devices?action=pair", title: "Devices" }),
51
+ tab({ tabId: "account", path: "account", title: "Account" }),
52
+ ], "groups");
53
+ expect(restored.tabs.map((entry) => entry.path)).toEqual(["", `identity/groups/${PERSON_ID}`]);
54
+ expect(restored.activeId).toBe("groups");
55
+ expect(restored.tabs[1]?.title).toBe("Identity");
56
+ expect((0, tabs_state_1.migrateRecentPath)("contacts")).toBe("identity");
57
+ expect((0, tabs_state_1.migrateRecentPath)("invites/accept")).toBe("identity");
58
+ expect((0, tabs_state_1.migrateRecentPath)("account")).toBe("identity");
59
+ });
60
+ });
61
+ describe("navigateTo", () => {
62
+ beforeEach(async () => {
63
+ await tabs_state_1.activeTabs.loadingPromise;
64
+ (0, tabs_state_1.activeTabs)([tabs_state_1.launcherApp, tab({ tabId: "threads", path: "threads", title: "Threads" })]);
65
+ (0, tabs_state_1.activeTabId)("threads");
66
+ (0, tabs_state_1.recentlyUsedApps)([]);
67
+ (0, globals_1._mainContentPath)("threads");
68
+ });
69
+ it("updates the same app in place and preserves the query", () => {
70
+ (0, tabs_state_1.navigateTo)("threads/thread-1?draft=1");
71
+ expect((0, tabs_state_1.activeTabs)()).toHaveLength(2);
72
+ expect((0, tabs_state_1.activeTabs)().find((entry) => entry.tabId === "threads")?.path).toBe("threads/thread-1?draft=1");
73
+ expect((0, tabs_state_1.activeTabId)()).toBe("threads");
74
+ expect((0, globals_1._mainContentPath)()).toBe("threads/thread-1?draft=1");
75
+ });
76
+ it("opens another app without duplicating an existing tab", () => {
77
+ (0, tabs_state_1.navigateTo)(`contacts/${PERSON_ID}`);
78
+ const identityId = (0, tabs_state_1.activeTabId)();
79
+ (0, tabs_state_1.activeTabId)("threads");
80
+ (0, tabs_state_1.navigateTo)("identity/devices?foo=bar");
81
+ const identityTabs = (0, tabs_state_1.activeTabs)().filter((entry) => entry.path.startsWith("identity"));
82
+ expect(identityTabs).toHaveLength(1);
83
+ expect(identityTabs[0]?.path).toBe("identity/devices?foo=bar");
84
+ expect((0, tabs_state_1.activeTabId)()).toBe(identityId);
85
+ expect((0, tabs_state_1.activeTabs)().some((entry) => entry.tabId === "threads")).toBe(true);
86
+ });
87
+ it("opens the account alias inside the Identity tab", () => {
88
+ (0, tabs_state_1.navigateTo)("account");
89
+ const identityId = (0, tabs_state_1.activeTabId)();
90
+ expect((0, globals_1._mainContentPath)()).toBe("identity/account");
91
+ (0, tabs_state_1.navigateTo)("identity/people");
92
+ expect((0, tabs_state_1.activeTabId)()).toBe(identityId);
93
+ expect((0, tabs_state_1.activeTabs)().filter((entry) => entry.path.startsWith("identity"))).toHaveLength(1);
94
+ expect((0, globals_1._mainContentPath)()).toBe("identity/people");
95
+ });
96
+ it("rewrites invite acceptance onto the open Identity tab", () => {
97
+ (0, tabs_state_1.navigateTo)("identity/people");
98
+ const identityId = (0, tabs_state_1.activeTabId)();
99
+ (0, tabs_state_1.navigateTo)("invites/accept");
100
+ expect((0, tabs_state_1.activeTabId)()).toBe(identityId);
101
+ expect((0, tabs_state_1.activeTabs)().filter((entry) => entry.path.startsWith("identity"))).toHaveLength(1);
102
+ expect((0, globals_1._mainContentPath)()).toBe("identity/activity/accept");
103
+ });
104
+ it("keeps the current section when the app root is opened again", () => {
105
+ (0, tabs_state_1.navigateTo)("identity/devices/pair");
106
+ (0, tabs_state_1.goToTabPath)("identity");
107
+ expect((0, globals_1._mainContentPath)()).toBe("identity/devices/pair");
108
+ expect((0, tabs_state_1.recentlyUsedApps)()).toEqual(["identity"]);
109
+ });
110
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-ui",
3
- "version": "0.25.2",
3
+ "version": "0.25.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-ui.git"
@@ -28,7 +28,7 @@
28
28
  "lint:fix": "biome check --write ."
29
29
  },
30
30
  "peerDependencies": {
31
- "@peers-app/peers-sdk": "^0.25.2",
31
+ "@peers-app/peers-sdk": "^0.25.4",
32
32
  "bootstrap": "^5.3.3",
33
33
  "react": "^18.0.0",
34
34
  "react-dom": "^18.0.0"
@@ -39,7 +39,7 @@
39
39
  "@babel/preset-react": "^7.24.1",
40
40
  "@babel/preset-typescript": "^7.27.1",
41
41
  "@biomejs/biome": "2.3.11",
42
- "@peers-app/peers-sdk": "0.25.2",
42
+ "@peers-app/peers-sdk": "0.25.4",
43
43
  "@testing-library/dom": "^10.4.0",
44
44
  "@testing-library/jest-dom": "^6.6.3",
45
45
  "@testing-library/react": "^16.3.0",
@@ -14,6 +14,7 @@ import {
14
14
  import { mainContentPath } from "../../globals";
15
15
  import { mentionConfigs } from "../../mention-configs";
16
16
 
17
+ /** Serialized Lexical payload for a Peers mention node. */
17
18
  export type SerializedMentionNode = Spread<
18
19
  {
19
20
  data: IMentionData;
@@ -43,6 +44,7 @@ function $convertMentionElement(domNode: HTMLElement): DOMConversionOutput | nul
43
44
  return null;
44
45
  }
45
46
 
47
+ /** Lexical text entity that renders and navigates a typed Peers mention. */
46
48
  export class MentionNode extends TextNode {
47
49
  static getType(): string {
48
50
  return "mention";
@@ -109,7 +111,7 @@ export class MentionNode extends TextNode {
109
111
  onClick(this.data);
110
112
  } else {
111
113
  const id = this.data.id;
112
- const href = `#${kind}s/${id}`;
114
+ const href = kind === "user" ? `identity/people/${id}` : `${kind}s/${id}`;
113
115
  mainContentPath(href);
114
116
  }
115
117
  });
@@ -189,12 +191,14 @@ export class MentionNode extends TextNode {
189
191
  }
190
192
  }
191
193
 
194
+ /** Create a segmented Lexical mention node from mention data. */
192
195
  export function $createMentionNode(data: IMentionData): MentionNode {
193
196
  const mentionNode = new MentionNode(data);
194
197
  mentionNode.setMode("segmented").toggleDirectionless();
195
198
  return $applyNodeReplacement(mentionNode);
196
199
  }
197
200
 
201
+ /** Return whether a Lexical node is a {@link MentionNode}. */
198
202
  export function $isMentionNode(node: LexicalNode | null | undefined): node is MentionNode {
199
203
  return node instanceof MentionNode;
200
204
  }