@peers-app/peers-ui 0.25.2 → 0.25.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/components/markdown-editor/mention-node.d.ts +4 -0
- package/dist/components/markdown-editor/mention-node.js +4 -1
- package/dist/components/responsive-section-nav.d.ts +34 -0
- package/dist/components/responsive-section-nav.js +19 -0
- package/dist/components/responsive-section-nav.test.d.ts +1 -0
- package/dist/components/responsive-section-nav.test.js +45 -0
- package/dist/components/router.d.ts +2 -3
- package/dist/components/router.js +6 -9
- package/dist/globals-hash.test.d.ts +1 -0
- package/dist/globals-hash.test.js +16 -0
- package/dist/globals.d.ts +19 -0
- package/dist/globals.js +51 -12
- package/dist/hooks/use-host-contract.d.ts +18 -0
- package/dist/hooks/use-host-contract.js +53 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/peers-services-registration.d.ts +1 -5
- package/dist/peers-services-registration.js +6 -16
- package/dist/root-layout/root-layout-host.js +7 -4
- package/dist/root-layout/root-layout-host.test.js +3 -6
- package/dist/screens/account/account-screen.d.ts +1 -0
- package/dist/screens/account/account-screen.js +29 -53
- package/dist/screens/account/account-screen.test.d.ts +1 -0
- package/dist/screens/account/account-screen.test.js +47 -0
- package/dist/screens/contacts/contact-details.js +2 -23
- package/dist/screens/contacts/contact-list.d.ts +24 -1
- package/dist/screens/contacts/contact-list.js +128 -143
- package/dist/screens/contacts/contact-list.test.d.ts +1 -0
- package/dist/screens/contacts/contact-list.test.js +48 -0
- package/dist/screens/contacts/index.d.ts +0 -2
- package/dist/screens/contacts/index.js +0 -3
- package/dist/screens/device-pairing/device-pairing.js +17 -16
- package/dist/screens/device-pairing/device-pairing.test.js +4 -9
- package/dist/screens/device-pairing/identity-ipc.d.ts +11 -0
- package/dist/screens/device-pairing/identity-ipc.js +20 -0
- package/dist/screens/device-pairing/use-device-pairing-state.d.ts +2 -2
- package/dist/screens/device-pairing/use-device-pairing-state.js +7 -25
- package/dist/screens/devices/devices-list.d.ts +18 -0
- package/dist/screens/devices/devices-list.js +173 -0
- package/dist/screens/devices/devices-list.test.d.ts +1 -0
- package/dist/screens/devices/devices-list.test.js +53 -0
- package/dist/screens/devices/index.d.ts +1 -0
- package/dist/screens/devices/index.js +5 -0
- package/dist/screens/groups/group-details.d.ts +1 -0
- package/dist/screens/groups/group-details.js +4 -63
- package/dist/screens/groups/group-invite-panel.js +2 -2
- package/dist/screens/groups/group-list.d.ts +18 -1
- package/dist/screens/groups/group-list.js +107 -136
- package/dist/screens/groups/group-list.test.d.ts +1 -0
- package/dist/screens/groups/group-list.test.js +38 -0
- package/dist/screens/groups/group-members.d.ts +1 -0
- package/dist/screens/groups/group-members.js +2 -1
- package/dist/screens/groups/index.d.ts +0 -3
- package/dist/screens/groups/index.js +0 -4
- package/dist/screens/identity/identity-routes.d.ts +107 -0
- package/dist/screens/identity/identity-routes.js +245 -0
- package/dist/screens/identity/identity-routes.test.d.ts +1 -0
- package/dist/screens/identity/identity-routes.test.js +51 -0
- package/dist/screens/identity/identity-screen.d.ts +9 -0
- package/dist/screens/identity/identity-screen.js +143 -0
- package/dist/screens/identity/identity-screen.test.d.ts +1 -0
- package/dist/screens/identity/identity-screen.test.js +103 -0
- package/dist/screens/identity/index.d.ts +2 -0
- package/dist/screens/identity/index.js +6 -0
- package/dist/screens/identity/invite-contact-panel.d.ts +4 -0
- package/dist/screens/identity/invite-contact-panel.js +30 -0
- package/dist/screens/identity/profile-invite.d.ts +7 -0
- package/dist/screens/identity/profile-invite.js +62 -0
- package/dist/screens/invites/accept-invite.js +1 -1
- package/dist/screens/invites/activity-view.d.ts +24 -0
- package/dist/screens/invites/activity-view.js +64 -0
- package/dist/screens/invites/activity-view.test.d.ts +1 -0
- package/dist/screens/invites/activity-view.test.js +108 -0
- package/dist/screens/invites/index.d.ts +1 -2
- package/dist/screens/invites/index.js +1 -3
- package/dist/screens/invites/invite-capture.d.ts +0 -2
- package/dist/screens/invites/invite-capture.js +1 -3
- package/dist/screens/invites/invite-classify.test.d.ts +1 -0
- package/dist/screens/invites/invite-classify.test.js +98 -0
- package/dist/screens/invites/invites-ipc.d.ts +1 -1
- package/dist/screens/invites/invites-ipc.js +5 -45
- package/dist/screens/invites/pending-invites-list.d.ts +35 -16
- package/dist/screens/invites/pending-invites-list.js +69 -30
- package/dist/screens/network-viewer/network-viewer-ipc.js +4 -59
- package/dist/screens/search/global-search.d.ts +1 -0
- package/dist/screens/search/global-search.js +2 -1
- package/dist/screens/settings/settings-page.js +25 -58
- package/dist/screens/setup-user.js +5 -5
- package/dist/screens/tools/tool-access-ipc.d.ts +2 -0
- package/dist/screens/tools/tool-access-ipc.js +8 -55
- package/dist/screens/welcome-modal.js +3 -2
- package/dist/system-apps/identity.app.d.ts +3 -0
- package/dist/system-apps/identity.app.js +10 -0
- package/dist/system-apps/index.d.ts +1 -4
- package/dist/system-apps/index.js +5 -17
- package/dist/tabs-layout/network-status-indicator.js +10 -3
- package/dist/tabs-layout/network-status-indicator.test.js +15 -0
- package/dist/tabs-layout/tabs-layout.js +3 -1
- package/dist/tabs-layout/tabs-state.d.ts +79 -1
- package/dist/tabs-layout/tabs-state.js +256 -79
- package/dist/tabs-layout/tabs-state.test.d.ts +1 -0
- package/dist/tabs-layout/tabs-state.test.js +110 -0
- package/package.json +3 -3
- package/src/components/markdown-editor/mention-node.ts +5 -1
- package/src/components/responsive-section-nav.test.tsx +88 -0
- package/src/components/responsive-section-nav.tsx +92 -0
- package/src/components/router.tsx +6 -9
- package/src/globals-hash.test.ts +19 -0
- package/src/globals.tsx +53 -13
- package/src/hooks/use-host-contract.ts +75 -0
- package/src/index.tsx +1 -0
- package/src/peers-services-registration.ts +6 -17
- package/src/root-layout/root-layout-host.test.tsx +4 -7
- package/src/root-layout/root-layout-host.tsx +9 -11
- package/src/screens/account/account-screen.test.tsx +61 -0
- package/src/screens/account/account-screen.tsx +116 -95
- package/src/screens/contacts/contact-details.tsx +4 -26
- package/src/screens/contacts/contact-list.test.tsx +59 -0
- package/src/screens/contacts/contact-list.tsx +246 -260
- package/src/screens/contacts/index.ts +0 -4
- package/src/screens/device-pairing/device-pairing.test.tsx +7 -9
- package/src/screens/device-pairing/device-pairing.tsx +19 -23
- package/src/screens/device-pairing/identity-ipc.ts +25 -0
- package/src/screens/device-pairing/use-device-pairing-state.ts +10 -33
- package/src/screens/devices/devices-list.test.tsx +68 -0
- package/src/screens/devices/devices-list.tsx +361 -0
- package/src/screens/devices/index.ts +1 -0
- package/src/screens/groups/group-details.tsx +8 -142
- package/src/screens/groups/group-invite-panel.tsx +2 -7
- package/src/screens/groups/group-list.test.tsx +44 -0
- package/src/screens/groups/group-list.tsx +227 -288
- package/src/screens/groups/group-members.tsx +3 -2
- package/src/screens/groups/index.ts +0 -5
- package/src/screens/identity/identity-routes.test.ts +70 -0
- package/src/screens/identity/identity-routes.ts +301 -0
- package/src/screens/identity/identity-screen.test.tsx +138 -0
- package/src/screens/identity/identity-screen.tsx +282 -0
- package/src/screens/identity/index.ts +2 -0
- package/src/screens/identity/invite-contact-panel.tsx +45 -0
- package/src/screens/identity/profile-invite.tsx +93 -0
- package/src/screens/invites/accept-invite.tsx +1 -1
- package/src/screens/invites/activity-view.test.tsx +127 -0
- package/src/screens/invites/activity-view.tsx +162 -0
- package/src/screens/invites/index.ts +1 -4
- package/src/screens/invites/invite-capture.ts +0 -3
- package/src/screens/invites/invite-classify.test.ts +110 -0
- package/src/screens/invites/invites-ipc.ts +6 -55
- package/src/screens/invites/pending-invites-list.tsx +85 -61
- package/src/screens/network-viewer/network-viewer-ipc.ts +8 -82
- package/src/screens/search/global-search.tsx +2 -1
- package/src/screens/settings/settings-page.tsx +53 -126
- package/src/screens/setup-user.tsx +5 -6
- package/src/screens/tools/tool-access-ipc.ts +13 -74
- package/src/screens/welcome-modal.tsx +3 -3
- package/src/system-apps/identity.app.ts +9 -0
- package/src/system-apps/index.ts +3 -12
- package/src/tabs-layout/network-status-indicator.test.tsx +22 -0
- package/src/tabs-layout/network-status-indicator.tsx +31 -0
- package/src/tabs-layout/tabs-layout.tsx +7 -1
- package/src/tabs-layout/tabs-state.test.ts +152 -0
- package/src/tabs-layout/tabs-state.ts +300 -83
- package/dist/screens/invites/invites-screen.d.ts +0 -9
- package/dist/screens/invites/invites-screen.js +0 -36
- package/dist/system-apps/account.app.d.ts +0 -2
- package/dist/system-apps/account.app.js +0 -8
- package/dist/system-apps/contacts.app.d.ts +0 -2
- package/dist/system-apps/contacts.app.js +0 -9
- package/dist/system-apps/groups.app.d.ts +0 -2
- package/dist/system-apps/groups.app.js +0 -9
- package/dist/system-apps/invites.app.d.ts +0 -2
- package/dist/system-apps/invites.app.js +0 -9
- package/src/screens/invites/invites-screen.tsx +0 -78
- package/src/system-apps/account.app.ts +0 -7
- package/src/system-apps/contacts.app.ts +0 -8
- package/src/system-apps/groups.app.ts +0 -8
- package/src/system-apps/invites.app.ts +0 -8
|
@@ -12,6 +12,7 @@ exports.TabsStripIndicators = TabsStripIndicators;
|
|
|
12
12
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
13
13
|
const moment_1 = __importDefault(require("moment"));
|
|
14
14
|
const react_1 = require("react");
|
|
15
|
+
const identity_routes_1 = require("../screens/identity/identity-routes");
|
|
15
16
|
const network_viewer_ipc_1 = require("../screens/network-viewer/network-viewer-ipc");
|
|
16
17
|
const tabs_state_1 = require("./tabs-state");
|
|
17
18
|
const use_network_status_1 = require("./use-network-status");
|
|
@@ -189,8 +190,8 @@ function ConnectedDevicesList({ loadState }) {
|
|
|
189
190
|
}
|
|
190
191
|
return ((0, jsx_runtime_1.jsx)("ul", { className: "small mb-0 ps-0", style: { maxHeight: "180px", overflowY: "auto" }, children: loadState.connections.map((connection) => ((0, jsx_runtime_1.jsx)(ConnectedDeviceRow, { connection: connection }, connection.deviceId))) }));
|
|
191
192
|
}
|
|
192
|
-
function NetworkStatusDetails({ detailsId, status, unavailable, connectionsLoadState, onOpenViewer, }) {
|
|
193
|
-
return ((0, jsx_runtime_1.jsx)("div", { id: detailsId, role: "dialog", "aria-label": "Network status details", className: "card shadow position-absolute end-0 mt-1", style: { top: "100%", width: "320px", zIndex: 1100 }, children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body p-3", children: [(0, jsx_runtime_1.jsx)("h6", { className: "card-title mb-2", children: "Network status" }), unavailable || !status ? ((0, jsx_runtime_1.jsx)("p", { className: "small text-muted mb-3", children: "Network diagnostics are temporarily unavailable. The local host may still be starting or reconnecting." })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("dl", { className: "row small mb-2", children: [(0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Connections" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.connectionCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Syncing contexts" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.syncingContextCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Sync errors" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.errorCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Groups connected" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.connectedContextCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Groups offline" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.disconnectedContextCount }), status.lastSuccessfulSyncAt != null && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Last sync" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end text-truncate", title: new Date(status.lastSuccessfulSyncAt).toLocaleString(), children: formatRelativeSyncTime(status.lastSuccessfulSyncAt) })] }))] }), status.errors.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-danger py-2 px-2 small mb-2", children: [status.errors[0].message, status.errors.length > 1 && ` (+${status.errors.length - 1} more)`] })), (0, jsx_runtime_1.jsxs)("div", { className: "border-top pt-2 mt-1 mb-2", children: [(0, jsx_runtime_1.jsx)("div", { className: "small text-muted mb-1", children: "Devices" }), (0, jsx_runtime_1.jsx)(ConnectedDevicesList, { loadState: connectionsLoadState })] })] })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-primary w-100", onClick: onOpenViewer, children: "Open Network Viewer" })] }) }));
|
|
193
|
+
function NetworkStatusDetails({ detailsId, status, unavailable, connectionsLoadState, onUseInvite, onOpenIdentity, onOpenViewer, }) {
|
|
194
|
+
return ((0, jsx_runtime_1.jsx)("div", { id: detailsId, role: "dialog", "aria-label": "Network status details", className: "card shadow position-absolute end-0 mt-1", style: { top: "100%", width: "320px", zIndex: 1100 }, children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body p-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2 mb-3", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-sm btn-outline-primary flex-fill", onClick: onUseInvite, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-qr-code me-1", "aria-hidden": "true" }), "Use Invite"] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-sm btn-outline-primary flex-fill", onClick: onOpenIdentity, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-person-badge me-1", "aria-hidden": "true" }), "Identity"] })] }), (0, jsx_runtime_1.jsx)("h6", { className: "card-title mb-2", children: "Network status" }), unavailable || !status ? ((0, jsx_runtime_1.jsx)("p", { className: "small text-muted mb-3", children: "Network diagnostics are temporarily unavailable. The local host may still be starting or reconnecting." })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("dl", { className: "row small mb-2", children: [(0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Connections" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.connectionCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Syncing contexts" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.syncingContextCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Sync errors" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.errorCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Groups connected" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.connectedContextCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Groups offline" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.disconnectedContextCount }), status.lastSuccessfulSyncAt != null && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Last sync" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end text-truncate", title: new Date(status.lastSuccessfulSyncAt).toLocaleString(), children: formatRelativeSyncTime(status.lastSuccessfulSyncAt) })] }))] }), status.errors.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-danger py-2 px-2 small mb-2", children: [status.errors[0].message, status.errors.length > 1 && ` (+${status.errors.length - 1} more)`] })), (0, jsx_runtime_1.jsxs)("div", { className: "border-top pt-2 mt-1 mb-2", children: [(0, jsx_runtime_1.jsx)("div", { className: "small text-muted mb-1", children: "Devices" }), (0, jsx_runtime_1.jsx)(ConnectedDevicesList, { loadState: connectionsLoadState })] })] })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-primary w-100", onClick: onOpenViewer, children: "Open Network Viewer" })] }) }));
|
|
194
195
|
}
|
|
195
196
|
/** Accessible reactive network status button and details popover. */
|
|
196
197
|
function NetworkStatusIndicator({ compact = false }) {
|
|
@@ -305,7 +306,13 @@ function NetworkStatusIndicator({ compact = false }) {
|
|
|
305
306
|
fontWeight: 700,
|
|
306
307
|
right: "-0.28rem",
|
|
307
308
|
bottom: "-0.32rem",
|
|
308
|
-
}, "aria-hidden": "true", children: presentation.disconnectedGroupsOverlay.count }))] }), !compact && ((0, jsx_runtime_1.jsxs)("span", { className: "small", children: [viewState.status?.connectionCount ?? 0, (0, jsx_runtime_1.jsx)("span", { className: "visually-hidden", children: " connections" })] }))] }), open && ((0, jsx_runtime_1.jsx)(NetworkStatusDetails, { detailsId: detailsId, status: viewState.status, unavailable: viewState.availability !== "ready", connectionsLoadState: connectionsLoadState,
|
|
309
|
+
}, "aria-hidden": "true", children: presentation.disconnectedGroupsOverlay.count }))] }), !compact && ((0, jsx_runtime_1.jsxs)("span", { className: "small", children: [viewState.status?.connectionCount ?? 0, (0, jsx_runtime_1.jsx)("span", { className: "visually-hidden", children: " connections" })] }))] }), open && ((0, jsx_runtime_1.jsx)(NetworkStatusDetails, { detailsId: detailsId, status: viewState.status, unavailable: viewState.availability !== "ready", connectionsLoadState: connectionsLoadState, onUseInvite: () => {
|
|
310
|
+
setOpen(false);
|
|
311
|
+
(0, tabs_state_1.goToTabPath)(identity_routes_1.IDENTITY_ACCEPT_PATH);
|
|
312
|
+
}, onOpenIdentity: () => {
|
|
313
|
+
setOpen(false);
|
|
314
|
+
(0, tabs_state_1.goToTabPath)(identity_routes_1.IDENTITY_ACCOUNT_PATH);
|
|
315
|
+
}, onOpenViewer: () => {
|
|
309
316
|
setOpen(false);
|
|
310
317
|
(0, tabs_state_1.goToTabPath)("network-viewer");
|
|
311
318
|
} }))] }));
|
|
@@ -7,6 +7,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
7
7
|
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
8
8
|
const react_1 = require("@testing-library/react");
|
|
9
9
|
const moment_1 = __importDefault(require("moment"));
|
|
10
|
+
const identity_routes_1 = require("../screens/identity/identity-routes");
|
|
10
11
|
const network_status_indicator_1 = require("./network-status-indicator");
|
|
11
12
|
const goToTabPath = jest.fn();
|
|
12
13
|
const getConnections = jest.fn();
|
|
@@ -424,6 +425,20 @@ describe("NetworkStatusIndicator", () => {
|
|
|
424
425
|
expect(view.getByRole("dialog")).toHaveTextContent("Couldn't load devices");
|
|
425
426
|
});
|
|
426
427
|
});
|
|
428
|
+
it("offers shortcuts to use an invite and open the Identity account", () => {
|
|
429
|
+
mockViewState = { availability: "ready", status: status({ state: "synced" }) };
|
|
430
|
+
const view = (0, react_1.render)((0, jsx_runtime_1.jsx)(network_status_indicator_1.NetworkStatusIndicator, {}));
|
|
431
|
+
react_1.fireEvent.click(view.getByRole("button", { name: /Network synced/ }));
|
|
432
|
+
expect(view.getByRole("button", { name: "Use Invite" }).querySelector(".bi-qr-code")).toBeTruthy();
|
|
433
|
+
expect(view.getByRole("button", { name: "Identity" }).querySelector(".bi-person-badge")).toBeTruthy();
|
|
434
|
+
react_1.fireEvent.click(view.getByRole("button", { name: "Use Invite" }));
|
|
435
|
+
expect(goToTabPath).toHaveBeenCalledWith(identity_routes_1.IDENTITY_ACCEPT_PATH);
|
|
436
|
+
expect(view.queryByRole("dialog", { name: "Network status details" })).toBeNull();
|
|
437
|
+
react_1.fireEvent.click(view.getByRole("button", { name: /Network synced/ }));
|
|
438
|
+
react_1.fireEvent.click(view.getByRole("button", { name: "Identity" }));
|
|
439
|
+
expect(goToTabPath).toHaveBeenCalledWith(identity_routes_1.IDENTITY_ACCOUNT_PATH);
|
|
440
|
+
expect(view.queryByRole("dialog", { name: "Network status details" })).toBeNull();
|
|
441
|
+
});
|
|
427
442
|
it("updates on rerender and opens details before navigating to Network Viewer", async () => {
|
|
428
443
|
const view = (0, react_1.render)((0, jsx_runtime_1.jsx)(network_status_indicator_1.NetworkStatusIndicator, {}));
|
|
429
444
|
expect(view.getByRole("button", { name: /loading/ })).toBeTruthy();
|
|
@@ -44,6 +44,7 @@ const group_switcher_1 = require("../components/group-switcher");
|
|
|
44
44
|
const router_1 = require("../components/router");
|
|
45
45
|
const globals_1 = require("../globals");
|
|
46
46
|
const hooks_1 = require("../hooks");
|
|
47
|
+
const identity_routes_1 = require("../screens/identity/identity-routes");
|
|
47
48
|
const color_mode_dropdown_1 = require("../screens/settings/color-mode-dropdown");
|
|
48
49
|
const system_apps_1 = require("../system-apps");
|
|
49
50
|
const routes_loader_1 = require("../ui-router/routes-loader");
|
|
@@ -281,7 +282,8 @@ const TabContentRenderer = react_1.default.memo(({ tab, isMobile }) => {
|
|
|
281
282
|
// System apps use the main Router (which reads from mainContentPath)
|
|
282
283
|
// Package apps use UIRouter with explicit path
|
|
283
284
|
const isSystemApp = tab.packageId === "system-apps";
|
|
284
|
-
|
|
285
|
+
const identityApp = isSystemApp && (0, identity_routes_1.isIdentityPath)(tab.path);
|
|
286
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: `h-100 ${identityApp ? "overflow-hidden" : "overflow-auto"} ${identityApp ? "" : isMobile ? "p-2" : "p-3"}`, children: isSystemApp ? ((0, jsx_runtime_1.jsx)(router_1.Router, { path: tab.path })) : ((0, jsx_runtime_1.jsx)(ui_loader_1.UIRouter, { path: tab.path, uiCategory: "screen", props: {} })) }));
|
|
285
287
|
}, (prevProps, nextProps) => {
|
|
286
288
|
// Only re-render if the tab or isMobile changed
|
|
287
289
|
return (prevProps.tab.tabId === nextProps.tab.tabId &&
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type IAppNav, type Observable } from "@peers-app/peers-sdk";
|
|
2
2
|
import { type IPackageWithNavs } from "../ui-router/routes-loader";
|
|
3
|
+
/** One top-level app tab. */
|
|
3
4
|
export interface TabState {
|
|
4
5
|
tabId: string;
|
|
5
6
|
packageId?: string;
|
|
@@ -7,24 +8,92 @@ export interface TabState {
|
|
|
7
8
|
title: string;
|
|
8
9
|
iconClassName?: string;
|
|
9
10
|
}
|
|
11
|
+
/** The app launcher tab. It is always present and cannot be closed. */
|
|
10
12
|
export declare const launcherApp: TabState;
|
|
13
|
+
/** Open tabs. Writes through to device storage after the initial restore. */
|
|
11
14
|
export declare const activeTabs: Observable<TabState[]> & {
|
|
12
15
|
loadingPromise: Promise<void>;
|
|
13
16
|
};
|
|
17
|
+
/** Id of the tab whose path is showing. */
|
|
14
18
|
export declare const activeTabId: Observable<string> & {
|
|
15
19
|
loadingPromise: Promise<void>;
|
|
16
20
|
};
|
|
21
|
+
/** App roots recently opened from the launcher, newest first. */
|
|
17
22
|
export declare const recentlyUsedApps: Observable<string[]> & {
|
|
18
23
|
loadingPromise: Promise<void>;
|
|
19
24
|
};
|
|
25
|
+
/** Tab ids whose bodies have been mounted at least once. */
|
|
20
26
|
export declare const initializedTabs: Set<string>;
|
|
27
|
+
/** Options for {@link navigateTo}. */
|
|
28
|
+
export interface NavigateOptions {
|
|
29
|
+
/**
|
|
30
|
+
* When the requested path is an app's root and that app is already open,
|
|
31
|
+
* activate the existing tab without resetting its section.
|
|
32
|
+
*/
|
|
33
|
+
preserveSectionOnAppRoot?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Open a path in the tab that owns its app.
|
|
37
|
+
*
|
|
38
|
+
* Legacy identity aliases are rewritten first. A route owned by the current
|
|
39
|
+
* app replaces that tab's path. A route owned by another open app activates
|
|
40
|
+
* that app's tab. Otherwise a new tab is opened.
|
|
41
|
+
*
|
|
42
|
+
* @param rawPath - Requested route, with or without a query string.
|
|
43
|
+
* @param options - App-root behavior. Launcher opens preserve the current section.
|
|
44
|
+
*/
|
|
45
|
+
export declare function navigateTo(rawPath: string, options?: NavigateOptions): void;
|
|
46
|
+
/**
|
|
47
|
+
* Open an app or path from the launcher.
|
|
48
|
+
*
|
|
49
|
+
* Choosing an app that is already open keeps the section the user was on.
|
|
50
|
+
*
|
|
51
|
+
* @param path - App root or deep link.
|
|
52
|
+
*/
|
|
21
53
|
export declare function goToTabPath(path: string): void;
|
|
22
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Apply a {@link mainContentPath} write to the open tabs.
|
|
56
|
+
*
|
|
57
|
+
* @param _oldPath - Previous route. Retained for existing call sites.
|
|
58
|
+
* @param newPath - Requested route.
|
|
59
|
+
* @param _setNewMainPath - Direct path setter. Navigation updates the path itself.
|
|
60
|
+
*/
|
|
61
|
+
export declare const handleMainPathChanged: (_oldPath: string, newPath: string, _setNewMainPath: (path: string) => void) => void;
|
|
62
|
+
/**
|
|
63
|
+
* Rewrite stored tabs onto canonical routes.
|
|
64
|
+
*
|
|
65
|
+
* Contacts, Groups, and Devices tabs from before the Identity app become one
|
|
66
|
+
* Identity tab. The active tab is kept when it is one of those duplicates.
|
|
67
|
+
*
|
|
68
|
+
* @param tabs - Persisted tabs.
|
|
69
|
+
* @param activeId - Persisted active tab id.
|
|
70
|
+
*/
|
|
71
|
+
export declare function restoreNavigationState(tabs: TabState[], activeId: string): {
|
|
72
|
+
tabs: TabState[];
|
|
73
|
+
activeId: string;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Map a stored recent-app path onto the current app root.
|
|
77
|
+
*
|
|
78
|
+
* @param path - Persisted recent path, possibly a legacy alias.
|
|
79
|
+
*/
|
|
80
|
+
export declare function migrateRecentPath(path: string): string;
|
|
23
81
|
type AppInfo = {
|
|
24
82
|
navItem: IAppNav;
|
|
25
83
|
package: IPackageWithNavs;
|
|
26
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* Resolve the app that owns a route.
|
|
87
|
+
*
|
|
88
|
+
* @param path - Route, with or without a query string.
|
|
89
|
+
* @returns The matching app, the launcher for an empty path, or `undefined`.
|
|
90
|
+
*/
|
|
27
91
|
export declare function determineAppFromPath(path: string): AppInfo | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* Set the active tab's title.
|
|
94
|
+
*
|
|
95
|
+
* @param newTitle - Title shown on the tab strip.
|
|
96
|
+
*/
|
|
28
97
|
export declare const updateActiveTabTitle: (newTitle: string) => void;
|
|
29
98
|
/**
|
|
30
99
|
* Enable or disable single-tab mode.
|
|
@@ -36,8 +105,17 @@ export declare const updateActiveTabTitle: (newTitle: string) => void;
|
|
|
36
105
|
* and disable it on unmount.
|
|
37
106
|
*/
|
|
38
107
|
export declare const setSingleTabMode: (enabled: boolean) => void;
|
|
108
|
+
/**
|
|
109
|
+
* Open a tab, or activate an existing tab with the same package and path.
|
|
110
|
+
*
|
|
111
|
+
* @param tab - Tab contents. `tabId` is assigned here.
|
|
112
|
+
* @param forceNew - When true, always append a tab instead of reusing one.
|
|
113
|
+
*/
|
|
39
114
|
export declare const openNewTab: (tab: Omit<TabState, "tabId">, forceNew?: boolean) => void;
|
|
115
|
+
/** Close the active tab. The launcher tab stays open. */
|
|
40
116
|
export declare const closeCurrentTab: () => void;
|
|
117
|
+
/** Activate the next open tab, wrapping around. */
|
|
41
118
|
export declare const switchToNextTab: () => void;
|
|
119
|
+
/** Activate the previous open tab, wrapping around. */
|
|
42
120
|
export declare const switchToPreviousTab: () => void;
|
|
43
121
|
export {};
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
|
|
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
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {};
|