@peers-app/peers-ui 0.25.1 → 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
|
@@ -7,9 +7,16 @@ import {
|
|
|
7
7
|
observable,
|
|
8
8
|
} from "@peers-app/peers-sdk";
|
|
9
9
|
import { _mainContentPath } from "../globals";
|
|
10
|
+
import {
|
|
11
|
+
canonicalizeRoute,
|
|
12
|
+
IDENTITY_APP_PATH,
|
|
13
|
+
isIdentityPath,
|
|
14
|
+
parseRoute,
|
|
15
|
+
} from "../screens/identity/identity-routes";
|
|
10
16
|
import { systemPackage } from "../system-apps";
|
|
11
17
|
import { allPackages, type IPackageWithNavs } from "../ui-router/routes-loader";
|
|
12
18
|
|
|
19
|
+
/** One top-level app tab. */
|
|
13
20
|
export interface TabState {
|
|
14
21
|
tabId: string;
|
|
15
22
|
packageId?: string; // 'launcher' for app launcher tab
|
|
@@ -18,6 +25,7 @@ export interface TabState {
|
|
|
18
25
|
iconClassName?: string;
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
/** The app launcher tab. It is always present and cannot be closed. */
|
|
21
29
|
export const launcherApp: TabState = {
|
|
22
30
|
tabId: "launcher",
|
|
23
31
|
packageId: "launcher",
|
|
@@ -39,7 +47,13 @@ const _persistentRecentlyUsedApps = groupUserVar<string[]>("recentlyUsedApps", {
|
|
|
39
47
|
defaultValue: [],
|
|
40
48
|
});
|
|
41
49
|
|
|
50
|
+
let finishNavigationRestore: () => void = () => {};
|
|
51
|
+
const navigationReady = new Promise<void>((resolve) => {
|
|
52
|
+
finishNavigationRestore = resolve;
|
|
53
|
+
});
|
|
54
|
+
|
|
42
55
|
// In-memory observables for UI state (loaded once, then write-through to persistent vars)
|
|
56
|
+
/** Open tabs. Writes through to device storage after the initial restore. */
|
|
43
57
|
export const activeTabs: Observable<TabState[]> & { loadingPromise: Promise<void> } = (() => {
|
|
44
58
|
const obs = observable<TabState[]>([launcherApp]);
|
|
45
59
|
|
|
@@ -48,14 +62,10 @@ export const activeTabs: Observable<TabState[]> & { loadingPromise: Promise<void
|
|
|
48
62
|
_persistentActiveTabs(value);
|
|
49
63
|
});
|
|
50
64
|
|
|
51
|
-
|
|
52
|
-
const loadingPromise = _persistentActiveTabs.loadingPromise.then(() => {
|
|
53
|
-
obs(_persistentActiveTabs());
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
return Object.assign(obs, { loadingPromise });
|
|
65
|
+
return Object.assign(obs, { loadingPromise: navigationReady });
|
|
57
66
|
})();
|
|
58
67
|
|
|
68
|
+
/** Id of the tab whose path is showing. */
|
|
59
69
|
export const activeTabId: Observable<string> & { loadingPromise: Promise<void> } = (() => {
|
|
60
70
|
const obs = observable<string>("launcher");
|
|
61
71
|
|
|
@@ -64,14 +74,10 @@ export const activeTabId: Observable<string> & { loadingPromise: Promise<void> }
|
|
|
64
74
|
_persistentActiveTabId(value);
|
|
65
75
|
});
|
|
66
76
|
|
|
67
|
-
|
|
68
|
-
const loadingPromise = _persistentActiveTabId.loadingPromise.then(() => {
|
|
69
|
-
obs(_persistentActiveTabId());
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
return Object.assign(obs, { loadingPromise });
|
|
77
|
+
return Object.assign(obs, { loadingPromise: navigationReady });
|
|
73
78
|
})();
|
|
74
79
|
|
|
80
|
+
/** App roots recently opened from the launcher, newest first. */
|
|
75
81
|
export const recentlyUsedApps: Observable<string[]> & { loadingPromise: Promise<void> } = (() => {
|
|
76
82
|
const obs = observable<string[]>([]);
|
|
77
83
|
|
|
@@ -80,85 +86,163 @@ export const recentlyUsedApps: Observable<string[]> & { loadingPromise: Promise<
|
|
|
80
86
|
_persistentRecentlyUsedApps(value);
|
|
81
87
|
});
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
const loadingPromise = _persistentRecentlyUsedApps.loadingPromise.then(() => {
|
|
85
|
-
obs(_persistentRecentlyUsedApps());
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
return Object.assign(obs, { loadingPromise });
|
|
89
|
+
return Object.assign(obs, { loadingPromise: navigationReady });
|
|
89
90
|
})();
|
|
90
91
|
|
|
92
|
+
void Promise.all([
|
|
93
|
+
_persistentActiveTabs.loadingPromise,
|
|
94
|
+
_persistentActiveTabId.loadingPromise,
|
|
95
|
+
_persistentRecentlyUsedApps.loadingPromise,
|
|
96
|
+
]).then(() => {
|
|
97
|
+
const restored = restoreNavigationState(_persistentActiveTabs(), _persistentActiveTabId());
|
|
98
|
+
activeTabs(restored.tabs);
|
|
99
|
+
if (activeTabId() !== restored.activeId) {
|
|
100
|
+
activeTabId(restored.activeId);
|
|
101
|
+
}
|
|
102
|
+
recentlyUsedApps(migrateRecentPaths(_persistentRecentlyUsedApps()));
|
|
103
|
+
finishNavigationRestore();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/** Tab ids whose bodies have been mounted at least once. */
|
|
91
107
|
export const initializedTabs = new Set<string>();
|
|
92
108
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
109
|
+
/** Options for {@link navigateTo}. */
|
|
110
|
+
export interface NavigateOptions {
|
|
111
|
+
/**
|
|
112
|
+
* When the requested path is an app's root and that app is already open,
|
|
113
|
+
* activate the existing tab without resetting its section.
|
|
114
|
+
*/
|
|
115
|
+
preserveSectionOnAppRoot?: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Open a path in the tab that owns its app.
|
|
120
|
+
*
|
|
121
|
+
* Legacy identity aliases are rewritten first. A route owned by the current
|
|
122
|
+
* app replaces that tab's path. A route owned by another open app activates
|
|
123
|
+
* that app's tab. Otherwise a new tab is opened.
|
|
124
|
+
*
|
|
125
|
+
* @param rawPath - Requested route, with or without a query string.
|
|
126
|
+
* @param options - App-root behavior. Launcher opens preserve the current section.
|
|
127
|
+
*/
|
|
128
|
+
export function navigateTo(rawPath: string, options?: NavigateOptions) {
|
|
129
|
+
const requestedPathname = parseRoute(rawPath).pathname;
|
|
130
|
+
if (!requestedPathname) {
|
|
131
|
+
const launcher = activeTabs().find((tab) => tab.tabId === "launcher") ?? activeTabs()[0];
|
|
132
|
+
if (launcher && activeTabId() !== launcher.tabId) {
|
|
133
|
+
activeTabId(launcher.tabId);
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const next = canonicalizeRoute(rawPath);
|
|
139
|
+
const app = determineAppFromPath(next);
|
|
140
|
+
if (options?.preserveSectionOnAppRoot && app && isAppRootRequest(requestedPathname, app)) {
|
|
141
|
+
const existing = findTabForApp(app);
|
|
142
|
+
if (existing) {
|
|
143
|
+
if (activeTabId() !== existing.tabId) {
|
|
144
|
+
activeTabId(existing.tabId);
|
|
128
145
|
}
|
|
146
|
+
rememberRecentApp(existing.path);
|
|
147
|
+
return;
|
|
129
148
|
}
|
|
130
149
|
}
|
|
150
|
+
|
|
151
|
+
if (!app || app.package.packageId === "launcher") {
|
|
152
|
+
openUnresolved(next);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const current = activeTabs().find((tab) => tab.tabId === activeTabId());
|
|
157
|
+
const currentApp = current ? determineAppFromPath(current.path) : undefined;
|
|
158
|
+
if (current && current.packageId !== "launcher" && currentApp && sameApp(currentApp, app)) {
|
|
159
|
+
showTab(current.tabId, next);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const existing = findTabForApp(app);
|
|
164
|
+
if (existing) {
|
|
165
|
+
showTab(existing.tabId, next);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
openNewTab({
|
|
170
|
+
path: next,
|
|
171
|
+
title: app.navItem.displayName || app.navItem.name,
|
|
172
|
+
packageId: app.package.packageId,
|
|
173
|
+
iconClassName: app.navItem.iconClassName,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Open an app or path from the launcher.
|
|
179
|
+
*
|
|
180
|
+
* Choosing an app that is already open keeps the section the user was on.
|
|
181
|
+
*
|
|
182
|
+
* @param path - App root or deep link.
|
|
183
|
+
*/
|
|
184
|
+
export function goToTabPath(path: string) {
|
|
185
|
+
navigateTo(path, { preserveSectionOnAppRoot: true });
|
|
131
186
|
}
|
|
132
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Apply a {@link mainContentPath} write to the open tabs.
|
|
190
|
+
*
|
|
191
|
+
* @param _oldPath - Previous route. Retained for existing call sites.
|
|
192
|
+
* @param newPath - Requested route.
|
|
193
|
+
* @param _setNewMainPath - Direct path setter. Navigation updates the path itself.
|
|
194
|
+
*/
|
|
133
195
|
export const handleMainPathChanged = (
|
|
134
|
-
|
|
196
|
+
_oldPath: string,
|
|
135
197
|
newPath: string,
|
|
136
|
-
|
|
198
|
+
_setNewMainPath: (path: string) => void,
|
|
137
199
|
) => {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const currentTab = activeTabs().find((t) => t.tabId === activeTabId());
|
|
141
|
-
if (oldPathPart === newPathPart || (currentTab && currentTab.path === newPath)) {
|
|
142
|
-
// nothing to do
|
|
143
|
-
setNewMainPath(newPath);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
200
|
+
navigateTo(newPath);
|
|
201
|
+
};
|
|
146
202
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Rewrite stored tabs onto canonical routes.
|
|
205
|
+
*
|
|
206
|
+
* Contacts, Groups, and Devices tabs from before the Identity app become one
|
|
207
|
+
* Identity tab. The active tab is kept when it is one of those duplicates.
|
|
208
|
+
*
|
|
209
|
+
* @param tabs - Persisted tabs.
|
|
210
|
+
* @param activeId - Persisted active tab id.
|
|
211
|
+
*/
|
|
212
|
+
export function restoreNavigationState(
|
|
213
|
+
tabs: TabState[],
|
|
214
|
+
activeId: string,
|
|
215
|
+
): { tabs: TabState[]; activeId: string } {
|
|
216
|
+
const migrated = (tabs.length > 0 ? tabs : [launcherApp]).map(migrateStoredTab);
|
|
217
|
+
const merged = mergeIdentityTabs(migrated, activeId);
|
|
218
|
+
const activeExists = merged.tabs.some((tab) => tab.tabId === merged.activeId);
|
|
219
|
+
return {
|
|
220
|
+
tabs: merged.tabs,
|
|
221
|
+
activeId: activeExists ? merged.activeId : merged.tabs[0].tabId,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Map a stored recent-app path onto the current app root.
|
|
227
|
+
*
|
|
228
|
+
* @param path - Persisted recent path, possibly a legacy alias.
|
|
229
|
+
*/
|
|
230
|
+
export function migrateRecentPath(path: string): string {
|
|
231
|
+
const canonical = canonicalizeRoute(path);
|
|
232
|
+
if (isIdentityPath(canonical)) {
|
|
233
|
+
return IDENTITY_APP_PATH;
|
|
157
234
|
}
|
|
158
|
-
|
|
235
|
+
return canonical;
|
|
236
|
+
}
|
|
159
237
|
|
|
160
238
|
type AppInfo = { navItem: IAppNav; package: IPackageWithNavs };
|
|
161
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Resolve the app that owns a route.
|
|
242
|
+
*
|
|
243
|
+
* @param path - Route, with or without a query string.
|
|
244
|
+
* @returns The matching app, the launcher for an empty path, or `undefined`.
|
|
245
|
+
*/
|
|
162
246
|
export function determineAppFromPath(path: string): AppInfo | undefined {
|
|
163
247
|
const launcherInfo = {
|
|
164
248
|
navItem: {
|
|
@@ -230,7 +314,138 @@ export function determineAppFromPath(path: string): AppInfo | undefined {
|
|
|
230
314
|
return undefined;
|
|
231
315
|
}
|
|
232
316
|
|
|
233
|
-
|
|
317
|
+
function appKey(app: AppInfo): string {
|
|
318
|
+
const nav = app.navItem.navigationPath || app.navItem.name.toLowerCase();
|
|
319
|
+
return `${app.package.packageId}:${nav}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function sameApp(left: AppInfo, right: AppInfo): boolean {
|
|
323
|
+
return appKey(left) === appKey(right);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function isAppRootRequest(pathname: string, app: AppInfo): boolean {
|
|
327
|
+
const nav = (app.navItem.navigationPath || app.navItem.name).toLowerCase();
|
|
328
|
+
return pathname.toLowerCase() === nav;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function findTabForApp(app: AppInfo): TabState | undefined {
|
|
332
|
+
const key = appKey(app);
|
|
333
|
+
return activeTabs().find((tab) => {
|
|
334
|
+
const info = determineAppFromPath(tab.path);
|
|
335
|
+
return info ? appKey(info) === key : false;
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Replace a tab's path immutably so the memoized tab body re-renders.
|
|
341
|
+
* Activating a different tab lets the tab subscription publish the path.
|
|
342
|
+
*/
|
|
343
|
+
function showTab(tabId: string, path: string) {
|
|
344
|
+
const tabs = activeTabs();
|
|
345
|
+
const index = tabs.findIndex((tab) => tab.tabId === tabId);
|
|
346
|
+
if (index < 0) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const current = tabs[index];
|
|
350
|
+
if (current.path !== path) {
|
|
351
|
+
const nextTabs = [...tabs];
|
|
352
|
+
nextTabs[index] = { ...current, path };
|
|
353
|
+
activeTabs(nextTabs);
|
|
354
|
+
}
|
|
355
|
+
if (activeTabId() !== tabId) {
|
|
356
|
+
activeTabId(tabId);
|
|
357
|
+
} else if (_mainContentPath() !== path) {
|
|
358
|
+
_mainContentPath(path);
|
|
359
|
+
}
|
|
360
|
+
rememberRecentApp(path);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function openUnresolved(path: string) {
|
|
364
|
+
const current = activeTabs().find((tab) => tab.tabId === activeTabId());
|
|
365
|
+
if (current && current.packageId === "system-apps") {
|
|
366
|
+
showTab(current.tabId, path);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
openNewTab({
|
|
370
|
+
path,
|
|
371
|
+
title: "New Tab",
|
|
372
|
+
packageId: "system-apps",
|
|
373
|
+
iconClassName: "bi-question-circle",
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function rememberRecentApp(path: string) {
|
|
378
|
+
const app = determineAppFromPath(canonicalizeRoute(path));
|
|
379
|
+
if (!app || app.package.packageId === "launcher") {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const recentPath =
|
|
383
|
+
app.package.packageId === systemPackage.packageId ? app.navItem.navigationPath || path : path;
|
|
384
|
+
if (!recentPath) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const recent = recentlyUsedApps();
|
|
388
|
+
const filtered = recent.filter((entry) => entry !== recentPath);
|
|
389
|
+
const next = [recentPath, ...filtered].slice(0, 8);
|
|
390
|
+
if (next.length === recent.length && next.every((entry, index) => entry === recent[index])) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
recentlyUsedApps(next);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function migrateStoredTab(tab: TabState): TabState {
|
|
397
|
+
const path = canonicalizeRoute(tab.path);
|
|
398
|
+
if (path === tab.path) {
|
|
399
|
+
return tab;
|
|
400
|
+
}
|
|
401
|
+
const app = determineAppFromPath(path);
|
|
402
|
+
const identity = app?.navItem.navigationPath === IDENTITY_APP_PATH;
|
|
403
|
+
return {
|
|
404
|
+
...tab,
|
|
405
|
+
path,
|
|
406
|
+
packageId: app?.package.packageId ?? tab.packageId,
|
|
407
|
+
title: identity ? app?.navItem.displayName || app?.navItem.name || tab.title : tab.title,
|
|
408
|
+
iconClassName: identity ? app?.navItem.iconClassName || tab.iconClassName : tab.iconClassName,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function mergeIdentityTabs(
|
|
413
|
+
tabs: TabState[],
|
|
414
|
+
activeId: string,
|
|
415
|
+
): { tabs: TabState[]; activeId: string } {
|
|
416
|
+
const identityTabs = tabs.filter((tab) => isIdentityPath(tab.path));
|
|
417
|
+
if (identityTabs.length <= 1) {
|
|
418
|
+
return { tabs, activeId };
|
|
419
|
+
}
|
|
420
|
+
const keeper = identityTabs.find((tab) => tab.tabId === activeId) ?? identityTabs[0];
|
|
421
|
+
const remove = new Set(
|
|
422
|
+
identityTabs.filter((tab) => tab.tabId !== keeper.tabId).map((tab) => tab.tabId),
|
|
423
|
+
);
|
|
424
|
+
return {
|
|
425
|
+
tabs: tabs.filter((tab) => !remove.has(tab.tabId)),
|
|
426
|
+
activeId: remove.has(activeId) ? keeper.tabId : activeId,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function migrateRecentPaths(paths: string[]): string[] {
|
|
431
|
+
const seen = new Set<string>();
|
|
432
|
+
const next: string[] = [];
|
|
433
|
+
for (const path of paths) {
|
|
434
|
+
const migrated = migrateRecentPath(path);
|
|
435
|
+
if (!migrated || seen.has(migrated)) {
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
seen.add(migrated);
|
|
439
|
+
next.push(migrated);
|
|
440
|
+
}
|
|
441
|
+
return next.slice(0, 8);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Set the active tab's title.
|
|
446
|
+
*
|
|
447
|
+
* @param newTitle - Title shown on the tab strip.
|
|
448
|
+
*/
|
|
234
449
|
export const updateActiveTabTitle = (newTitle: string) => {
|
|
235
450
|
// TODO Fix this to use a more reliable update mechanism
|
|
236
451
|
// using setTimeout to get around a race condition for setting title on new tabs
|
|
@@ -261,7 +476,12 @@ export const setSingleTabMode = (enabled: boolean) => {
|
|
|
261
476
|
singleTabModeEnabled = enabled;
|
|
262
477
|
};
|
|
263
478
|
|
|
264
|
-
|
|
479
|
+
/**
|
|
480
|
+
* Open a tab, or activate an existing tab with the same package and path.
|
|
481
|
+
*
|
|
482
|
+
* @param tab - Tab contents. `tabId` is assigned here.
|
|
483
|
+
* @param forceNew - When true, always append a tab instead of reusing one.
|
|
484
|
+
*/
|
|
265
485
|
export const openNewTab = (tab: Omit<TabState, "tabId">, forceNew = false) => {
|
|
266
486
|
const newTab: TabState = {
|
|
267
487
|
...tab,
|
|
@@ -296,15 +516,12 @@ export const openNewTab = (tab: Omit<TabState, "tabId">, forceNew = false) => {
|
|
|
296
516
|
activeTabId(newTab.tabId);
|
|
297
517
|
}
|
|
298
518
|
|
|
299
|
-
// Update recently used (track by path instead of packageId)
|
|
300
519
|
if (tab.path && tab.packageId !== "launcher") {
|
|
301
|
-
|
|
302
|
-
const filtered = recent.filter((path) => path !== tab.path);
|
|
303
|
-
recentlyUsedApps([tab.path, ...filtered].slice(0, 8));
|
|
520
|
+
rememberRecentApp(tab.path);
|
|
304
521
|
}
|
|
305
522
|
};
|
|
306
523
|
|
|
307
|
-
|
|
524
|
+
/** Close the active tab. The launcher tab stays open. */
|
|
308
525
|
export const closeCurrentTab = () => {
|
|
309
526
|
const currentTabs = activeTabs();
|
|
310
527
|
const _activeTabId = activeTabId();
|
|
@@ -326,7 +543,7 @@ export const closeCurrentTab = () => {
|
|
|
326
543
|
}
|
|
327
544
|
};
|
|
328
545
|
|
|
329
|
-
|
|
546
|
+
/** Activate the next open tab, wrapping around. */
|
|
330
547
|
export const switchToNextTab = () => {
|
|
331
548
|
const currentTabs = activeTabs();
|
|
332
549
|
const activeTab = activeTabId();
|
|
@@ -338,7 +555,7 @@ export const switchToNextTab = () => {
|
|
|
338
555
|
activeTabId(currentTabs[nextIndex].tabId);
|
|
339
556
|
};
|
|
340
557
|
|
|
341
|
-
|
|
558
|
+
/** Activate the previous open tab, wrapping around. */
|
|
342
559
|
export const switchToPreviousTab = () => {
|
|
343
560
|
const currentTabs = activeTabs();
|
|
344
561
|
const activeTab = activeTabId();
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/** Screen props supplied by the router. */
|
|
2
|
-
export interface InvitesScreenProps {
|
|
3
|
-
/** When `true`, the Accept panel starts open (the `invites/accept` route). */
|
|
4
|
-
accept?: boolean;
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Invites hub: accept a pasted/scanned/deep-linked invite and review everything pending.
|
|
8
|
-
*/
|
|
9
|
-
export declare function InvitesScreen(props: InvitesScreenProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.InvitesScreen = InvitesScreen;
|
|
4
|
-
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
-
const react_1 = require("react");
|
|
6
|
-
const ui_loader_1 = require("../../ui-router/ui-loader");
|
|
7
|
-
const accept_invite_1 = require("./accept-invite");
|
|
8
|
-
const invite_capture_1 = require("./invite-capture");
|
|
9
|
-
const pending_invites_list_1 = require("./pending-invites-list");
|
|
10
|
-
/**
|
|
11
|
-
* Invites hub: accept a pasted/scanned/deep-linked invite and review everything pending.
|
|
12
|
-
*/
|
|
13
|
-
function InvitesScreen(props) {
|
|
14
|
-
const [accepting, setAccepting] = (0, react_1.useState)(Boolean(props.accept || (0, invite_capture_1.peekPendingInvite)()));
|
|
15
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid p-3", style: { maxWidth: 720 }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center mb-3", children: [(0, jsx_runtime_1.jsxs)("h4", { className: "mb-0", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-envelope-paper me-2" }), "Invites"] }), !accepting ? ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-primary btn-sm", onClick: () => setAccepting(true), children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-arrow-in-right me-1" }), "Accept an invite"] })) : null] }), accepting ? ((0, jsx_runtime_1.jsx)("div", { className: "card mb-4", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsx)("h6", { className: "card-title", children: "Accept an invite" }), (0, jsx_runtime_1.jsx)(accept_invite_1.AcceptInvite, { onDone: () => setAccepting(false) })] }) })) : null, (0, jsx_runtime_1.jsx)(pending_invites_list_1.PendingInvitesList, { title: "Waiting on you or others" }), (0, jsx_runtime_1.jsxs)("details", { className: "mt-3", children: [(0, jsx_runtime_1.jsx)("summary", { className: "small text-muted", style: { cursor: "pointer" }, children: "Recent history" }), (0, jsx_runtime_1.jsx)("div", { className: "mt-2", children: (0, jsx_runtime_1.jsx)(pending_invites_list_1.PendingInvitesList, { statuses: ["accepted", "declined", "expired", "revoked"] }) })] })] }));
|
|
16
|
-
}
|
|
17
|
-
(0, ui_loader_1.registerInternalPeersUI)({
|
|
18
|
-
peersUIId: "00invites0screen000000001",
|
|
19
|
-
component: InvitesScreen,
|
|
20
|
-
routes: [
|
|
21
|
-
{
|
|
22
|
-
isMatch: (props, context) => {
|
|
23
|
-
if (context.path === "invites") {
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
if (context.path === invite_capture_1.ACCEPT_INVITE_PATH) {
|
|
27
|
-
props.accept = true;
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
return false;
|
|
31
|
-
},
|
|
32
|
-
uiCategory: "screen",
|
|
33
|
-
priority: 2,
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { registerInternalPeersUI } from "../../ui-router/ui-loader";
|
|
3
|
-
import { AcceptInvite } from "./accept-invite";
|
|
4
|
-
import { ACCEPT_INVITE_PATH, peekPendingInvite } from "./invite-capture";
|
|
5
|
-
import { PendingInvitesList } from "./pending-invites-list";
|
|
6
|
-
|
|
7
|
-
/** Screen props supplied by the router. */
|
|
8
|
-
export interface InvitesScreenProps {
|
|
9
|
-
/** When `true`, the Accept panel starts open (the `invites/accept` route). */
|
|
10
|
-
accept?: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Invites hub: accept a pasted/scanned/deep-linked invite and review everything pending.
|
|
15
|
-
*/
|
|
16
|
-
export function InvitesScreen(props: InvitesScreenProps) {
|
|
17
|
-
const [accepting, setAccepting] = useState(Boolean(props.accept || peekPendingInvite()));
|
|
18
|
-
return (
|
|
19
|
-
<div className="container-fluid p-3" style={{ maxWidth: 720 }}>
|
|
20
|
-
<div className="d-flex justify-content-between align-items-center mb-3">
|
|
21
|
-
<h4 className="mb-0">
|
|
22
|
-
<i className="bi bi-envelope-paper me-2" />
|
|
23
|
-
Invites
|
|
24
|
-
</h4>
|
|
25
|
-
{!accepting ? (
|
|
26
|
-
<button
|
|
27
|
-
type="button"
|
|
28
|
-
className="btn btn-primary btn-sm"
|
|
29
|
-
onClick={() => setAccepting(true)}
|
|
30
|
-
>
|
|
31
|
-
<i className="bi bi-box-arrow-in-right me-1" />
|
|
32
|
-
Accept an invite
|
|
33
|
-
</button>
|
|
34
|
-
) : null}
|
|
35
|
-
</div>
|
|
36
|
-
|
|
37
|
-
{accepting ? (
|
|
38
|
-
<div className="card mb-4">
|
|
39
|
-
<div className="card-body">
|
|
40
|
-
<h6 className="card-title">Accept an invite</h6>
|
|
41
|
-
<AcceptInvite onDone={() => setAccepting(false)} />
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
44
|
-
) : null}
|
|
45
|
-
|
|
46
|
-
<PendingInvitesList title="Waiting on you or others" />
|
|
47
|
-
<details className="mt-3">
|
|
48
|
-
<summary className="small text-muted" style={{ cursor: "pointer" }}>
|
|
49
|
-
Recent history
|
|
50
|
-
</summary>
|
|
51
|
-
<div className="mt-2">
|
|
52
|
-
<PendingInvitesList statuses={["accepted", "declined", "expired", "revoked"]} />
|
|
53
|
-
</div>
|
|
54
|
-
</details>
|
|
55
|
-
</div>
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
registerInternalPeersUI({
|
|
60
|
-
peersUIId: "00invites0screen000000001",
|
|
61
|
-
component: InvitesScreen,
|
|
62
|
-
routes: [
|
|
63
|
-
{
|
|
64
|
-
isMatch: (props, context) => {
|
|
65
|
-
if (context.path === "invites") {
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
if (context.path === ACCEPT_INVITE_PATH) {
|
|
69
|
-
props.accept = true;
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
return false;
|
|
73
|
-
},
|
|
74
|
-
uiCategory: "screen",
|
|
75
|
-
priority: 2,
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
});
|