@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.
- 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 +44 -2
- package/dist/screens/invites/accept-invite.test.d.ts +1 -0
- package/dist/screens/invites/accept-invite.test.js +103 -0
- 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 +115 -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 +75 -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.test.tsx +115 -0
- package/src/screens/invites/accept-invite.tsx +50 -3
- 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 +129 -0
- package/src/screens/invites/invites-ipc.ts +6 -55
- package/src/screens/invites/pending-invites-list.tsx +94 -62
- 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
|
@@ -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 {};
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
}
|