@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
|
@@ -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.3",
|
|
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.3",
|
|
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.3",
|
|
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
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { ResponsiveSectionNav } from "./responsive-section-nav";
|
|
4
|
+
|
|
5
|
+
jest.mock("../globals", () => {
|
|
6
|
+
const { observable: createObservable } =
|
|
7
|
+
jest.requireActual<typeof import("@peers-app/peers-sdk")>("@peers-app/peers-sdk");
|
|
8
|
+
return { isDesktop: createObservable(true) };
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
import { isDesktop } from "../globals";
|
|
12
|
+
|
|
13
|
+
const sections = [
|
|
14
|
+
{ id: "activity", label: "Activity", iconClassName: "bi-inbox", badge: 2 },
|
|
15
|
+
{ id: "people", label: "People", iconClassName: "bi-person" },
|
|
16
|
+
{ id: "groups", label: "Groups" },
|
|
17
|
+
{ id: "devices", label: "Devices" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
describe("ResponsiveSectionNav", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
isDesktop(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("uses a vertical sidebar on desktop and marks the selected section", () => {
|
|
26
|
+
render(
|
|
27
|
+
<ResponsiveSectionNav
|
|
28
|
+
label="Identity"
|
|
29
|
+
sections={sections}
|
|
30
|
+
activeSectionId="people"
|
|
31
|
+
onSelect={() => undefined}
|
|
32
|
+
toolbar={<div>Quick actions</div>}
|
|
33
|
+
>
|
|
34
|
+
<div>People body</div>
|
|
35
|
+
</ResponsiveSectionNav>,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
expect(screen.getByRole("navigation", { name: "Identity" }).querySelector("ul")).toHaveClass(
|
|
39
|
+
"nav-pills",
|
|
40
|
+
"flex-column",
|
|
41
|
+
);
|
|
42
|
+
expect(screen.getByRole("button", { name: /People/ })).toHaveAttribute("aria-current", "page");
|
|
43
|
+
expect(screen.getByText("People body")).toBeInTheDocument();
|
|
44
|
+
expect(screen.getByText("Quick actions")).toBeInTheDocument();
|
|
45
|
+
expect(screen.getByText("Quick actions").closest("[data-section-scroll-container]")).toBeNull();
|
|
46
|
+
expect(screen.getByText("People body").closest("[data-section-scroll-container]")).toHaveClass(
|
|
47
|
+
"overflow-auto",
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("uses a non-wrapping horizontal strip on a narrow screen", () => {
|
|
52
|
+
isDesktop(false);
|
|
53
|
+
render(
|
|
54
|
+
<ResponsiveSectionNav
|
|
55
|
+
label="Identity"
|
|
56
|
+
sections={sections}
|
|
57
|
+
activeSectionId="activity"
|
|
58
|
+
onSelect={() => undefined}
|
|
59
|
+
>
|
|
60
|
+
<div>Activity body</div>
|
|
61
|
+
</ResponsiveSectionNav>,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(screen.getByRole("navigation", { name: "Identity" }).querySelector("ul")).toHaveClass(
|
|
65
|
+
"nav-tabs",
|
|
66
|
+
"flex-nowrap",
|
|
67
|
+
"overflow-x-auto",
|
|
68
|
+
"overflow-y-hidden",
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("reports the section chosen from the keyboard-focusable button", async () => {
|
|
73
|
+
const onSelect = jest.fn();
|
|
74
|
+
render(
|
|
75
|
+
<ResponsiveSectionNav
|
|
76
|
+
label="Identity"
|
|
77
|
+
sections={sections}
|
|
78
|
+
activeSectionId="activity"
|
|
79
|
+
onSelect={onSelect}
|
|
80
|
+
>
|
|
81
|
+
<div>Activity body</div>
|
|
82
|
+
</ResponsiveSectionNav>,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
await userEvent.click(screen.getByRole("button", { name: /Groups/ }));
|
|
86
|
+
expect(onSelect).toHaveBeenCalledWith("groups");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { isDesktop } from "../globals";
|
|
3
|
+
import { useObservable } from "../hooks";
|
|
4
|
+
|
|
5
|
+
/** One destination in {@link ResponsiveSectionNav}. */
|
|
6
|
+
export interface ISectionNavItem {
|
|
7
|
+
/** Stable id passed to {@link ResponsiveSectionNavProps.onSelect}. */
|
|
8
|
+
id: string;
|
|
9
|
+
/** Visible label. */
|
|
10
|
+
label: string;
|
|
11
|
+
/** Bootstrap icon class, without a leading markup wrapper. */
|
|
12
|
+
iconClassName?: string;
|
|
13
|
+
/** Optional count. Hidden when zero or omitted. */
|
|
14
|
+
badge?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Props for {@link ResponsiveSectionNav}. */
|
|
18
|
+
export interface ResponsiveSectionNavProps {
|
|
19
|
+
/** Accessible name for the section navigation. */
|
|
20
|
+
label: string;
|
|
21
|
+
/** Sections in display order. */
|
|
22
|
+
sections: ISectionNavItem[];
|
|
23
|
+
/** Id of the selected section. */
|
|
24
|
+
activeSectionId: string;
|
|
25
|
+
/** Called when a section button is activated. */
|
|
26
|
+
onSelect: (sectionId: string) => void;
|
|
27
|
+
/** Optional controls that remain visible above the scrolling section body. */
|
|
28
|
+
toolbar?: ReactNode;
|
|
29
|
+
/** Selected section body. */
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Section navigation that uses a left sidebar on a wide screen and a
|
|
35
|
+
* non-wrapping horizontal strip on a narrow one.
|
|
36
|
+
*
|
|
37
|
+
* The parent owns the route. This component only reports the selected id.
|
|
38
|
+
*/
|
|
39
|
+
export function ResponsiveSectionNav(props: ResponsiveSectionNavProps) {
|
|
40
|
+
const [desktop] = useObservable(isDesktop);
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
className={`d-flex h-100 overflow-hidden ${desktop ? "flex-row" : "flex-column"}`}
|
|
44
|
+
style={{ minHeight: 0, minWidth: 0 }}
|
|
45
|
+
>
|
|
46
|
+
<nav
|
|
47
|
+
aria-label={props.label}
|
|
48
|
+
className={desktop ? "border-end p-3 overflow-auto" : "border-bottom px-2 pt-2 pb-1"}
|
|
49
|
+
style={desktop ? { flex: "0 0 220px", width: 220 } : undefined}
|
|
50
|
+
>
|
|
51
|
+
<ul
|
|
52
|
+
className={`nav ${desktop ? "nav-pills flex-column" : "nav-tabs flex-nowrap overflow-x-auto overflow-y-hidden"}`}
|
|
53
|
+
>
|
|
54
|
+
{props.sections.map((section) => {
|
|
55
|
+
const selected = section.id === props.activeSectionId;
|
|
56
|
+
return (
|
|
57
|
+
<li className="nav-item" key={section.id}>
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
className={`nav-link d-flex align-items-center gap-2 w-100 text-start ${selected ? "active" : ""}`}
|
|
61
|
+
aria-current={selected ? "page" : undefined}
|
|
62
|
+
onClick={() => props.onSelect(section.id)}
|
|
63
|
+
>
|
|
64
|
+
{section.iconClassName ? <i className={section.iconClassName} /> : null}
|
|
65
|
+
<span>{section.label}</span>
|
|
66
|
+
{section.badge ? (
|
|
67
|
+
<span className={`badge ${selected ? "bg-light text-primary" : "bg-primary"}`}>
|
|
68
|
+
{section.badge}
|
|
69
|
+
</span>
|
|
70
|
+
) : null}
|
|
71
|
+
</button>
|
|
72
|
+
</li>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
</ul>
|
|
76
|
+
</nav>
|
|
77
|
+
<div
|
|
78
|
+
className="d-flex flex-column flex-grow-1 overflow-hidden"
|
|
79
|
+
style={{ minHeight: 0, minWidth: 0 }}
|
|
80
|
+
>
|
|
81
|
+
{props.toolbar ? <div className="flex-shrink-0">{props.toolbar}</div> : null}
|
|
82
|
+
<div
|
|
83
|
+
className="flex-grow-1 overflow-auto"
|
|
84
|
+
style={{ minHeight: 0, minWidth: 0 }}
|
|
85
|
+
data-section-scroll-container
|
|
86
|
+
>
|
|
87
|
+
{props.children}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as globals from "../globals";
|
|
2
2
|
import { useObservable } from "../hooks";
|
|
3
|
-
import { AccountScreen } from "../screens/account/account-screen";
|
|
4
3
|
import { AssistantDetails } from "../screens/assistants/assistant-details";
|
|
5
4
|
import { AssistantList } from "../screens/assistants/assistant-list";
|
|
5
|
+
import { canonicalizeRoute } from "../screens/identity/identity-routes";
|
|
6
6
|
import { PackageDetails } from "../screens/packages/package-details";
|
|
7
7
|
import { PackageList } from "../screens/packages/package-list";
|
|
8
8
|
import { PackageNewLocal } from "../screens/packages/package-new-local";
|
|
@@ -23,19 +23,19 @@ import { UIRouter } from "../ui-router/ui-loader";
|
|
|
23
23
|
import { ChannelMessages } from "./messages/channel-view";
|
|
24
24
|
import { ThreadMessageList } from "./messages/thread-message-list";
|
|
25
25
|
import "../screens/console-logs/console-logs-list";
|
|
26
|
-
import "../screens/
|
|
27
|
-
import "../screens/contacts";
|
|
28
|
-
import "../screens/invites";
|
|
26
|
+
import "../screens/identity";
|
|
29
27
|
import "../screens/network-viewer";
|
|
30
28
|
import "../screens/data-explorer";
|
|
31
29
|
|
|
30
|
+
/** Render the built-in or registered UI that owns a canonicalized route. */
|
|
32
31
|
export function Router({ path: providedPath }: { path?: string } = {}) {
|
|
33
32
|
// Hooks must run unconditionally (same order every render)
|
|
34
33
|
const [globalPath] = useObservable(globals.mainContentPath);
|
|
35
34
|
|
|
36
35
|
try {
|
|
37
|
-
// Use provided path or fall back to global mainContentPath
|
|
38
|
-
|
|
36
|
+
// Use provided path or fall back to global mainContentPath.
|
|
37
|
+
// Legacy contacts, groups, devices, invites, and account paths render as Identity.
|
|
38
|
+
let path = canonicalizeRoute(providedPath ?? globalPath);
|
|
39
39
|
const originalPath = path;
|
|
40
40
|
path = path.toLowerCase().trim();
|
|
41
41
|
path = path.split("?")[0]; // remove query string
|
|
@@ -55,9 +55,6 @@ export function Router({ path: providedPath }: { path?: string } = {}) {
|
|
|
55
55
|
if (path === "settings" || path === "profile") {
|
|
56
56
|
return <SettingsPage />;
|
|
57
57
|
}
|
|
58
|
-
if (path === "account") {
|
|
59
|
-
return <AccountScreen />;
|
|
60
|
-
}
|
|
61
58
|
if (path === "threads" || path === "") {
|
|
62
59
|
return <ChannelMessages />;
|
|
63
60
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
jest.mock("./tabs-layout/tabs-state", () => ({
|
|
2
|
+
handleMainPathChanged: (_oldPath: string, newPath: string, storage: (value: string) => void) =>
|
|
3
|
+
storage(newPath),
|
|
4
|
+
openNewTab: jest.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
import { initializeHashNavigation, mainContentPath } from "./globals";
|
|
8
|
+
|
|
9
|
+
describe("initial hash navigation", () => {
|
|
10
|
+
it("lets an incoming legacy deep link win and canonicalizes it", () => {
|
|
11
|
+
mainContentPath("identity/activity");
|
|
12
|
+
window.history.replaceState(null, "", "/#contacts/abcdefghijklmnopqrstuvwxy?from=link");
|
|
13
|
+
|
|
14
|
+
initializeHashNavigation();
|
|
15
|
+
|
|
16
|
+
expect(mainContentPath()).toBe("identity/people/abcdefghijklmnopqrstuvwxy?from=link");
|
|
17
|
+
expect(window.location.hash).toBe("#identity/people/abcdefghijklmnopqrstuvwxy?from=link");
|
|
18
|
+
});
|
|
19
|
+
});
|
package/src/globals.tsx
CHANGED
|
@@ -13,13 +13,17 @@ import {
|
|
|
13
13
|
rpcServerCalls,
|
|
14
14
|
sleep,
|
|
15
15
|
} from "@peers-app/peers-sdk";
|
|
16
|
+
import { canonicalizeRoute } from "./screens/identity/identity-routes";
|
|
16
17
|
import { handleMainPathChanged, openNewTab } from "./tabs-layout/tabs-state";
|
|
17
18
|
import { stripMentions } from "./utils";
|
|
18
19
|
|
|
20
|
+
/** Counter incremented when installed package UI should reload. */
|
|
19
21
|
export const packageReloaded = observable(0);
|
|
20
22
|
|
|
23
|
+
/** Persisted route for the active data context. Prefer {@link mainContentPath}. */
|
|
21
24
|
export const _mainContentPath = groupUserVar("mainContentPath", { defaultValue: "" });
|
|
22
25
|
|
|
26
|
+
/** Canonical navigation observable backed by the active tab state. */
|
|
23
27
|
export const mainContentPath = computed<string>({
|
|
24
28
|
read: () => {
|
|
25
29
|
return _mainContentPath();
|
|
@@ -52,6 +56,7 @@ const queryParams = computed<{ [key: string]: string }>({
|
|
|
52
56
|
},
|
|
53
57
|
});
|
|
54
58
|
|
|
59
|
+
/** Read or update one query parameter on {@link mainContentPath}. */
|
|
55
60
|
export function queryParam(name: string, value?: string) {
|
|
56
61
|
const params = queryParams();
|
|
57
62
|
if (value !== undefined) {
|
|
@@ -68,10 +73,12 @@ export function queryParam(name: string, value?: string) {
|
|
|
68
73
|
return params[name];
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
/** Open a message thread in its owning app tab. */
|
|
71
77
|
export async function openThread(thread: string | IMessage) {
|
|
72
78
|
openThreadInTab(thread);
|
|
73
79
|
}
|
|
74
80
|
|
|
81
|
+
/** Resolve a message and open its root thread in a new tab. */
|
|
75
82
|
export async function openThreadInTab(thread: string | IMessage) {
|
|
76
83
|
const threadId = typeof thread === "string" ? thread : thread.messageId;
|
|
77
84
|
const message = await Messages().get(threadId);
|
|
@@ -109,24 +116,70 @@ rpcClientCalls.openThread = async (threadId: string) => {
|
|
|
109
116
|
openThread(threadId);
|
|
110
117
|
};
|
|
111
118
|
|
|
119
|
+
/** Signed profile for the active user after {@link loadGlobals}. */
|
|
112
120
|
export let me: IUser = null as unknown as IUser;
|
|
113
121
|
|
|
122
|
+
/** Current browser viewport width. */
|
|
114
123
|
export const windowWidth = observable(window.innerWidth); //.extend({ rateLimit: 500 });
|
|
124
|
+
/** Current browser viewport height. */
|
|
115
125
|
export const windowHeight = observable(window.innerHeight); //.extend({ rateLimit: 500 });
|
|
116
126
|
window.addEventListener("resize", (_ev) => {
|
|
117
127
|
windowWidth(window.innerWidth);
|
|
118
128
|
windowHeight(window.innerHeight);
|
|
119
129
|
});
|
|
120
130
|
|
|
131
|
+
/** True when the viewport uses wide-screen navigation. */
|
|
121
132
|
export const isDesktop = computed(() => {
|
|
122
133
|
// return windowWidth() > 1200;
|
|
123
134
|
return windowWidth() > 992;
|
|
124
135
|
// return windowWidth() > 768;
|
|
125
136
|
});
|
|
126
137
|
|
|
138
|
+
/** Enabled groups available to the active user. */
|
|
127
139
|
export const groups = observable<IGroup[]>([]);
|
|
128
140
|
|
|
129
141
|
let globalsLoaded = false;
|
|
142
|
+
let hashNavigationInitialized = false;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Start two-way hash synchronization after routes and persisted tabs are restored.
|
|
146
|
+
*
|
|
147
|
+
* A non-empty startup hash wins over persisted navigation so canonical and legacy
|
|
148
|
+
* deep links open the destination the user requested.
|
|
149
|
+
*/
|
|
150
|
+
export function initializeHashNavigation() {
|
|
151
|
+
if (hashNavigationInitialized) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
hashNavigationInitialized = true;
|
|
155
|
+
|
|
156
|
+
const updateWindowHash = () => {
|
|
157
|
+
const currentPath = window.location.hash.substring(1);
|
|
158
|
+
const expectedPath = mainContentPath() || "";
|
|
159
|
+
if (currentPath !== expectedPath) {
|
|
160
|
+
window.location.hash = expectedPath;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const navigateFromHash = () => {
|
|
164
|
+
const requestedPath = canonicalizeRoute(window.location.hash.substring(1));
|
|
165
|
+
if (requestedPath !== mainContentPath()) {
|
|
166
|
+
mainContentPath(requestedPath);
|
|
167
|
+
} else {
|
|
168
|
+
updateWindowHash();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
mainContentPath.subscribe(updateWindowHash);
|
|
173
|
+
window.addEventListener("hashchange", navigateFromHash);
|
|
174
|
+
|
|
175
|
+
if (window.location.hash.substring(1)) {
|
|
176
|
+
navigateFromHash();
|
|
177
|
+
} else {
|
|
178
|
+
updateWindowHash();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Load global user state required before the root layout renders. */
|
|
130
183
|
export async function loadGlobals() {
|
|
131
184
|
if (globalsLoaded) return true;
|
|
132
185
|
globalsLoaded = true;
|
|
@@ -143,19 +196,6 @@ export async function loadGlobals() {
|
|
|
143
196
|
sleep(100),
|
|
144
197
|
]);
|
|
145
198
|
|
|
146
|
-
function updateWindowHash() {
|
|
147
|
-
const currentPath = window.location.hash.substring(1);
|
|
148
|
-
const expectedPath = mainContentPath() || "";
|
|
149
|
-
// console.log({ expectedPath, currentPath });
|
|
150
|
-
if (currentPath !== expectedPath) {
|
|
151
|
-
window.location.hash = expectedPath;
|
|
152
|
-
}
|
|
153
|
-
mainContentPath(expectedPath);
|
|
154
|
-
}
|
|
155
|
-
mainContentPath.subscribe(updateWindowHash);
|
|
156
|
-
updateWindowHash();
|
|
157
|
-
window.addEventListener("hashchange", () => mainContentPath(window.location.hash.substring(1)));
|
|
158
|
-
|
|
159
199
|
return true;
|
|
160
200
|
}
|
|
161
201
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type IContractConsumerBase,
|
|
3
|
+
type IHostContractConsumer,
|
|
4
|
+
subscribeHostContractTransport,
|
|
5
|
+
} from "@peers-app/peers-sdk";
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
|
|
8
|
+
/** Live consumer for one host contract, plus the last failure to obtain it. */
|
|
9
|
+
export interface IUseHostContractResult<T extends IContractConsumerBase> {
|
|
10
|
+
/** Current consumer, or `undefined` while the host transport is down. */
|
|
11
|
+
consumer: T | undefined;
|
|
12
|
+
/** Set when `get` rejects, for example when the transport wait times out. */
|
|
13
|
+
error: Error | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a cached local-host contract consumer and keep it current across reconnects.
|
|
18
|
+
*
|
|
19
|
+
* The handle owns caching and disposal. This hook re-reads it when the host transport
|
|
20
|
+
* changes. Read `consumer.observables.*` with `useObservable`.
|
|
21
|
+
*
|
|
22
|
+
* @param accessor Handle from `hostContractConsumer`, stable for the lifetime of the screen module.
|
|
23
|
+
* @returns The current consumer and the last error.
|
|
24
|
+
*/
|
|
25
|
+
export function useHostContract<T extends IContractConsumerBase>(
|
|
26
|
+
accessor: IHostContractConsumer<T>,
|
|
27
|
+
): IUseHostContractResult<T> {
|
|
28
|
+
const [consumer, setConsumer] = useState<T | undefined>();
|
|
29
|
+
const [error, setError] = useState<Error | undefined>();
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
let cancelled = false;
|
|
33
|
+
|
|
34
|
+
const apply = (next: T): void => {
|
|
35
|
+
if (cancelled) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
setConsumer(next);
|
|
39
|
+
setError(undefined);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const fail = (cause: unknown): void => {
|
|
43
|
+
if (cancelled) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const load = (): void => {
|
|
50
|
+
accessor.get().then(apply, fail);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
load();
|
|
54
|
+
const unsubscribe = subscribeHostContractTransport(
|
|
55
|
+
(transport) => {
|
|
56
|
+
if (cancelled) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!transport) {
|
|
60
|
+
setConsumer(undefined);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
load();
|
|
64
|
+
},
|
|
65
|
+
{ emitCurrent: false },
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
cancelled = true;
|
|
70
|
+
unsubscribe();
|
|
71
|
+
};
|
|
72
|
+
}, [accessor]);
|
|
73
|
+
|
|
74
|
+
return { consumer, error };
|
|
75
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -3,6 +3,7 @@ export type { IEditorEffects, IMarkdownEditorProps } from "./components/markdown
|
|
|
3
3
|
export { MarkdownEditor } from "./components/markdown-editor/editor";
|
|
4
4
|
export * from "./components/markdown-editor/editor-inline";
|
|
5
5
|
export * from "./components/markdown-with-mentions";
|
|
6
|
+
export * from "./components/responsive-section-nav";
|
|
6
7
|
export * from "./components/sortable-list";
|
|
7
8
|
export * from "./components/tabs";
|
|
8
9
|
export { Typeahead, type TypeaheadItem } from "./components/typeahead";
|
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export const servicesTokenPvar = userVar<string>("PEERS_SERVICES_TOKEN", {
|
|
9
|
-
isSecret: true,
|
|
10
|
-
defaultValue: "",
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
/** Store JWT and local "registered" flag after a successful `registerWithPeersServices` call. */
|
|
14
|
-
export function persistPeersServicesRegistration(token: string): void {
|
|
15
|
-
servicesTokenPvar(token);
|
|
16
|
-
registrationStatusPvar(new Date().toISOString());
|
|
17
|
-
}
|
|
1
|
+
// The pvar definitions live in peers-sdk so headless hosts and UIs share one set of names.
|
|
2
|
+
export {
|
|
3
|
+
persistPeersServicesRegistration,
|
|
4
|
+
registrationStatusPvar,
|
|
5
|
+
servicesTokenPvar,
|
|
6
|
+
} from "@peers-app/peers-sdk";
|