@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
|
@@ -8,56 +8,7 @@ const loading_indicator_1 = require("../../components/loading-indicator");
|
|
|
8
8
|
const tooltip_1 = require("../../components/tooltip");
|
|
9
9
|
const hooks_1 = require("../../hooks");
|
|
10
10
|
const peers_services_registration_1 = require("../../peers-services-registration");
|
|
11
|
-
const
|
|
12
|
-
const invites_ipc_1 = require("../invites/invites-ipc");
|
|
13
|
-
const PROFILE_QR_INFO = `### Your profile QR
|
|
14
|
-
|
|
15
|
-
Anyone who scans this (or opens the link) can send you a contact request. Requests show up under **Contacts → Pending** and nothing happens until you confirm.
|
|
16
|
-
|
|
17
|
-
### Regenerate
|
|
18
|
-
|
|
19
|
-
Regenerating invalidates this QR **and every other invite link you have issued**. Use it if the QR leaked somewhere you did not intend.`;
|
|
20
|
-
/** Reusable profile invite with a regenerate control that bumps the invite epoch. */
|
|
21
|
-
function ProfileInviteSection() {
|
|
22
|
-
const [invite, setInvite] = (0, react_1.useState)(undefined);
|
|
23
|
-
const [busy, setBusy] = (0, react_1.useState)(false);
|
|
24
|
-
const [error, setError] = (0, react_1.useState)(null);
|
|
25
|
-
(0, react_1.useEffect)(() => {
|
|
26
|
-
let cancelled = false;
|
|
27
|
-
void (0, invites_ipc_1.inviteTools)()
|
|
28
|
-
.then((tools) => tools.createContactInvite({ profile: true }))
|
|
29
|
-
.then(({ invite: created }) => {
|
|
30
|
-
if (!cancelled)
|
|
31
|
-
setInvite(created);
|
|
32
|
-
})
|
|
33
|
-
.catch((err) => {
|
|
34
|
-
if (!cancelled)
|
|
35
|
-
setError(err.message || "Could not load your profile QR.");
|
|
36
|
-
});
|
|
37
|
-
return () => {
|
|
38
|
-
cancelled = true;
|
|
39
|
-
};
|
|
40
|
-
}, []);
|
|
41
|
-
const regenerate = async () => {
|
|
42
|
-
if (!confirm("Regenerate your profile QR? Every invite link you have issued so far will stop working.")) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
setBusy(true);
|
|
46
|
-
setError(null);
|
|
47
|
-
try {
|
|
48
|
-
const tools = await (0, invites_ipc_1.inviteTools)();
|
|
49
|
-
const { invite: created } = await tools.regenerateProfileInvite();
|
|
50
|
-
setInvite(created);
|
|
51
|
-
}
|
|
52
|
-
catch (err) {
|
|
53
|
-
setError(err.message || "Could not regenerate.");
|
|
54
|
-
}
|
|
55
|
-
finally {
|
|
56
|
-
setBusy(false);
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
return ((0, jsx_runtime_1.jsx)("div", { className: "card mb-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsxs)("h5", { className: "card-title d-flex align-items-center gap-2", children: ["Profile QR", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: PROFILE_QR_INFO })] }), error ? (0, jsx_runtime_1.jsx)("div", { className: "alert alert-danger py-2", children: error }) : null, invite ? ((0, jsx_runtime_1.jsx)(invite_share_1.InviteShare, { invite: invite, description: "Let people connect with you by scanning this or opening the link. You confirm each request.", shareText: "Connect with me on Peers:" })) : !error ? ((0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {})) : null, (0, jsx_runtime_1.jsx)("div", { className: "text-center mt-3", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-danger btn-sm", onClick: () => void regenerate(), disabled: busy || !invite, children: busy ? "Regenerating…" : "Regenerate (revokes all invites)" }) })] }) }));
|
|
60
|
-
}
|
|
11
|
+
const identity_ipc_1 = require("../device-pairing/identity-ipc");
|
|
61
12
|
const INFO_TEXT = `### What does registering do?
|
|
62
13
|
|
|
63
14
|
Registering your account links your **public key** with the Peers cloud service. This enables cloud-powered features like:
|
|
@@ -74,8 +25,11 @@ Your **secret key never leaves your device**. Only your public key is shared wit
|
|
|
74
25
|
### Do I have to register?
|
|
75
26
|
|
|
76
27
|
No. Peers works fully in local/peer-to-peer mode without registration. Cloud features simply won't be available until you register.`;
|
|
28
|
+
/** Identity profile, public identifiers, device shortcuts, and service registration. */
|
|
77
29
|
function AccountScreen() {
|
|
78
30
|
const [registrationStatus] = (0, hooks_1.useObservable)(peers_services_registration_1.registrationStatusPvar);
|
|
31
|
+
const [deviceId] = (0, hooks_1.useObservable)(peers_sdk_1.thisDeviceId);
|
|
32
|
+
const me = (0, hooks_1.usePromise)(peers_sdk_1.getMe);
|
|
79
33
|
const [isRegistering, setIsRegistering] = (0, react_1.useState)(false);
|
|
80
34
|
const [error, setError] = (0, react_1.useState)(null);
|
|
81
35
|
const [success, setSuccess] = (0, react_1.useState)(null);
|
|
@@ -85,8 +39,8 @@ function AccountScreen() {
|
|
|
85
39
|
setError(null);
|
|
86
40
|
setSuccess(null);
|
|
87
41
|
try {
|
|
88
|
-
const token = await
|
|
89
|
-
(0, peers_services_registration_1.persistPeersServicesRegistration)(token);
|
|
42
|
+
const { token } = await (await (0, identity_ipc_1.identityTools)()).registerWithPeersServices();
|
|
43
|
+
await (0, peers_services_registration_1.persistPeersServicesRegistration)(token);
|
|
90
44
|
setSuccess("Account registered successfully.");
|
|
91
45
|
}
|
|
92
46
|
catch (err) {
|
|
@@ -96,9 +50,31 @@ function AccountScreen() {
|
|
|
96
50
|
setIsRegistering(false);
|
|
97
51
|
}
|
|
98
52
|
};
|
|
99
|
-
|
|
53
|
+
if (!me) {
|
|
54
|
+
return (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {});
|
|
55
|
+
}
|
|
56
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "container-xxl p-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("h4", { className: "mb-1 d-flex align-items-center gap-2", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-person-circle" }), "Account"] }), (0, jsx_runtime_1.jsx)("div", { className: "small text-muted", children: "Your public identity and account-level services" })] }), error && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-danger alert-dismissible", role: "alert", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-exclamation-triangle me-2" }), error, (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn-close", onClick: () => setError(null), "aria-label": "Close" })] })), success && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-success alert-dismissible", role: "alert", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-check-circle me-2" }), success, (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn-close", onClick: () => setSuccess(null), "aria-label": "Close" })] })), (0, jsx_runtime_1.jsx)("div", { className: "card mb-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body d-flex align-items-center gap-3 flex-wrap", children: [(0, jsx_runtime_1.jsx)("span", { className: "rounded-circle bg-primary-subtle text-primary-emphasis d-inline-flex align-items-center justify-content-center fs-4 fw-semibold", style: { width: 58, height: 58 }, "aria-hidden": "true", children: profileInitials(me.name) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex-grow-1", children: [(0, jsx_runtime_1.jsx)("h5", { className: "card-title mb-1", children: me.name || "Unnamed user" }), (0, jsx_runtime_1.jsx)("div", { className: "small text-muted", children: "Peers identity" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2 flex-wrap", children: [(0, jsx_runtime_1.jsx)("a", { href: "#settings", className: "btn btn-outline-secondary btn-sm", children: "Edit display name" }), (0, jsx_runtime_1.jsx)("a", { href: "#identity/devices", className: "btn btn-outline-primary btn-sm", children: "Manage devices" }), (0, jsx_runtime_1.jsx)("a", { href: "#identity/devices/pair", className: "btn btn-primary btn-sm", children: "Add device" })] })] }) }), (0, jsx_runtime_1.jsx)("div", { className: "card mb-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsx)("h5", { className: "card-title", children: "Technical identity" }), (0, jsx_runtime_1.jsx)("p", { className: "small text-muted", children: "Share these values only when diagnosing identity or connection problems." }), (0, jsx_runtime_1.jsx)(TechnicalValue, { label: "User ID", value: me.userId, help: "Your stable identifier across Peers." }), (0, jsx_runtime_1.jsx)(TechnicalValue, { label: "Signing key", value: me.publicKey, help: "Other people use this public key to verify your signatures." }), (0, jsx_runtime_1.jsx)(TechnicalValue, { label: "Encryption key", value: me.publicBoxKey, help: "Other people use this public key to encrypt data for you." }), (0, jsx_runtime_1.jsx)(TechnicalValue, { label: "Current device ID", value: deviceId || "Not available", help: "Identifies this installation on Peers networks." })] }) }), (0, jsx_runtime_1.jsx)("div", { className: "card", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsxs)("h5", { className: "card-title d-flex align-items-center gap-2", children: ["Peers Services Registration", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: INFO_TEXT })] }), isRegistered ? ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-2 mb-2", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-check-circle-fill text-success", style: { fontSize: "1.2em" } }), (0, jsx_runtime_1.jsx)("span", { children: "Registered" })] }), (0, jsx_runtime_1.jsxs)("small", { className: "text-muted d-block mb-3", children: ["Registered on", " ", new Date(registrationStatus).toLocaleDateString(undefined, {
|
|
100
57
|
year: "numeric",
|
|
101
58
|
month: "long",
|
|
102
59
|
day: "numeric",
|
|
103
60
|
})] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary btn-sm", onClick: handleRegister, disabled: isRegistering, children: isRegistering ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-2" }), "Refreshing..."] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-arrow-clockwise me-2" }), "Refresh Token"] })) })] })) : ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "text-muted mb-3", children: "Register your account to enable cloud-powered features like the AI assistant." }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-primary", onClick: handleRegister, disabled: isRegistering, children: isRegistering ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-2" }), "Registering..."] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-cloud-upload me-2" }), "Register Account"] })) })] }))] }) })] }));
|
|
104
61
|
}
|
|
62
|
+
function TechnicalValue(props) {
|
|
63
|
+
const [copied, setCopied] = (0, react_1.useState)(false);
|
|
64
|
+
const copy = async () => {
|
|
65
|
+
if (await (0, peers_sdk_1.copyToClipboard)(props.value)) {
|
|
66
|
+
setCopied(true);
|
|
67
|
+
window.setTimeout(() => setCopied(false), 1500);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "border-top py-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-2 mb-1", children: [(0, jsx_runtime_1.jsx)("strong", { className: "small", children: props.label }), (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: props.help }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-link btn-sm ms-auto py-0", onClick: () => void copy(), "aria-label": `Copy ${props.label}`, children: [(0, jsx_runtime_1.jsx)("i", { className: `bi ${copied ? "bi-check-lg" : "bi-copy"} me-1` }), copied ? "Copied" : "Copy"] })] }), (0, jsx_runtime_1.jsx)("code", { className: "small text-break user-select-all", children: props.value })] }));
|
|
71
|
+
}
|
|
72
|
+
function profileInitials(name) {
|
|
73
|
+
return (name
|
|
74
|
+
.trim()
|
|
75
|
+
.split(/\s+/)
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.slice(0, 2)
|
|
78
|
+
.map((part) => part[0]?.toLocaleUpperCase())
|
|
79
|
+
.join("") || "?");
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
|
+
const react_1 = require("@testing-library/react");
|
|
5
|
+
const account_screen_1 = require("./account-screen");
|
|
6
|
+
jest.mock("../../components/tooltip", () => ({
|
|
7
|
+
Tooltip: () => null,
|
|
8
|
+
}));
|
|
9
|
+
jest.mock("../../hooks", () => ({
|
|
10
|
+
useObservable: (value) => value === "device-observable" ? ["device0000000000000000001"] : [undefined],
|
|
11
|
+
usePromise: () => ({
|
|
12
|
+
userId: "user00000000000000000001",
|
|
13
|
+
name: "Taylor Example",
|
|
14
|
+
publicKey: "public-signing-key",
|
|
15
|
+
publicBoxKey: "public-encryption-key",
|
|
16
|
+
}),
|
|
17
|
+
}));
|
|
18
|
+
jest.mock("../../peers-services-registration", () => ({
|
|
19
|
+
persistPeersServicesRegistration: jest.fn(),
|
|
20
|
+
registrationStatusPvar: "registration-observable",
|
|
21
|
+
}));
|
|
22
|
+
jest.mock("../device-pairing/identity-ipc", () => ({
|
|
23
|
+
identityTools: jest.fn(),
|
|
24
|
+
}));
|
|
25
|
+
jest.mock("@peers-app/peers-sdk", () => {
|
|
26
|
+
const actual = jest.requireActual("@peers-app/peers-sdk");
|
|
27
|
+
return {
|
|
28
|
+
...actual,
|
|
29
|
+
copyToClipboard: jest.fn(async () => true),
|
|
30
|
+
getMe: jest.fn(),
|
|
31
|
+
thisDeviceId: "device-observable",
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
describe("AccountScreen", () => {
|
|
35
|
+
it("shows profile actions and copyable identity diagnostics", () => {
|
|
36
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(account_screen_1.AccountScreen, {}));
|
|
37
|
+
expect(react_1.screen.getByText("Taylor Example")).toBeInTheDocument();
|
|
38
|
+
expect(react_1.screen.getByText("user00000000000000000001")).toBeInTheDocument();
|
|
39
|
+
expect(react_1.screen.getByText("public-signing-key")).toBeInTheDocument();
|
|
40
|
+
expect(react_1.screen.getByText("public-encryption-key")).toBeInTheDocument();
|
|
41
|
+
expect(react_1.screen.getByText("device0000000000000000001")).toBeInTheDocument();
|
|
42
|
+
expect(react_1.screen.getByRole("link", { name: "Edit display name" })).toHaveAttribute("href", "#settings");
|
|
43
|
+
expect(react_1.screen.getByRole("link", { name: "Manage devices" })).toHaveAttribute("href", "#identity/devices");
|
|
44
|
+
expect(react_1.screen.getByRole("link", { name: "Add device" })).toHaveAttribute("href", "#identity/devices/pair");
|
|
45
|
+
expect(react_1.screen.getByRole("button", { name: "Copy User ID" })).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -9,8 +9,6 @@ const save_button_1 = require("../../components/save-button");
|
|
|
9
9
|
const tabs_1 = require("../../components/tabs");
|
|
10
10
|
const trust_level_dropdown_1 = require("../../components/trust-level-dropdown");
|
|
11
11
|
const hooks_1 = require("../../hooks");
|
|
12
|
-
const tabs_state_1 = require("../../tabs-layout/tabs-state");
|
|
13
|
-
const ui_loader_1 = require("../../ui-router/ui-loader");
|
|
14
12
|
/** Edit a contact's signed profile fields and personal-context trust assignment. */
|
|
15
13
|
const ContactDetails = (props) => {
|
|
16
14
|
const contact = (0, hooks_1.usePromise)(async () => {
|
|
@@ -19,16 +17,15 @@ const ContactDetails = (props) => {
|
|
|
19
17
|
if (!contact) {
|
|
20
18
|
throw new Error("Contact not found");
|
|
21
19
|
}
|
|
22
|
-
(0, tabs_state_1.updateActiveTabTitle)(contact.name || "Contact");
|
|
23
20
|
return (0, peers_sdk_1.Users)(userContext.userDataContext).initDoc(contact);
|
|
24
21
|
}, undefined, [props.userId]);
|
|
25
22
|
if (!contact) {
|
|
26
23
|
return (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {});
|
|
27
24
|
}
|
|
28
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "container-
|
|
25
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "container-xxl p-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex", children: [(0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("h4", { children: (0, jsx_runtime_1.jsx)("i", { className: "bi-person-fill-check me-2" }) }) }), (0, jsx_runtime_1.jsx)("div", { className: "flex-grow-1", children: (0, jsx_runtime_1.jsx)("h4", { children: (0, jsx_runtime_1.jsx)(input_1.Input, { className: "border border-0", style: { width: "100%", outline: "none", backgroundColor: "transparent" }, value: contact.qs.name }, contact.userId) }) }), (0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)(save_button_1.SaveButton, { doc: contact }, contact.userId) })] }), (0, jsx_runtime_1.jsx)(tabs_1.Tabs, { tabs: [
|
|
29
26
|
{
|
|
30
27
|
name: "Info",
|
|
31
|
-
content: ((0, jsx_runtime_1.jsx)(
|
|
28
|
+
content: ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)(ContactInfo, { contact: contact }) })),
|
|
32
29
|
},
|
|
33
30
|
] }, contact.userId)] }));
|
|
34
31
|
};
|
|
@@ -37,21 +34,3 @@ const ContactInfo = (props) => {
|
|
|
37
34
|
const { contact } = props;
|
|
38
35
|
return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: "mb-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex-grow-1", children: [(0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block mb-1", children: "Name" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: contact.qs.name, className: "form-control mb-3 ps-2", placeholder: "Contact name", title: "Contact name" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "border-start ps-3", children: [(0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block mb-1", children: "User ID" }), (0, jsx_runtime_1.jsx)("code", { className: "small", children: contact.userId })] }), (0, jsx_runtime_1.jsxs)("div", { className: "border-start ps-3", children: [(0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block mb-1", children: "Trust Level" }), (0, jsx_runtime_1.jsx)(trust_level_dropdown_1.TrustLevelDropdown, { userId: contact.userId }), (0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block mt-1", children: "Self grants this user full remote contract access." })] })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsx)("small", { children: "Public Key:" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: contact.qs.publicKey, className: "form-control mb-3 p-0 ps-2", placeholder: "Public key", title: "Public key" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsx)("small", { children: "Public Box Key:" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: contact.qs.publicBoxKey, className: "form-control mb-3 p-0 ps-2", placeholder: "Public box key", title: "Public box key" })] })] }));
|
|
39
36
|
};
|
|
40
|
-
(0, ui_loader_1.registerInternalPeersUI)({
|
|
41
|
-
peersUIId: "00contact00details00component",
|
|
42
|
-
component: exports.ContactDetails,
|
|
43
|
-
routes: [
|
|
44
|
-
{
|
|
45
|
-
isMatch: (props, context) => {
|
|
46
|
-
const match = context.path.match(/^contacts\/([a-zA-Z0-9]+)$/);
|
|
47
|
-
if (match) {
|
|
48
|
-
props.userId = match[1];
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
return false;
|
|
52
|
-
},
|
|
53
|
-
uiCategory: "screen",
|
|
54
|
-
priority: 2,
|
|
55
|
-
},
|
|
56
|
-
],
|
|
57
|
-
});
|
|
@@ -1 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
import { type IUser, TrustLevel } from "@peers-app/peers-sdk";
|
|
2
|
+
/** People directory and route-backed invite/profile actions. */
|
|
3
|
+
export declare function ContactList(props?: {
|
|
4
|
+
action?: "share" | "invite";
|
|
5
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
/** One person plus the relationship context shown in the directory. */
|
|
7
|
+
export interface IPersonSummary {
|
|
8
|
+
/** Signed user profile. */
|
|
9
|
+
user: IUser;
|
|
10
|
+
/** Whether the person is saved in the personal context. */
|
|
11
|
+
isContact: boolean;
|
|
12
|
+
/** Personal trust assignment when this is a contact. */
|
|
13
|
+
trustLevel?: TrustLevel;
|
|
14
|
+
/** Enabled groups both users belong to. */
|
|
15
|
+
sharedGroups: Array<{
|
|
16
|
+
groupId: string;
|
|
17
|
+
name: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
/** Compact, fully clickable row in the People directory. */
|
|
21
|
+
export declare function PersonRow(props: {
|
|
22
|
+
row: IPersonSummary;
|
|
23
|
+
duplicateName: boolean;
|
|
24
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,167 +1,152 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ContactList = ContactList;
|
|
4
|
+
exports.PersonRow = PersonRow;
|
|
4
5
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
6
|
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
6
7
|
const react_1 = require("react");
|
|
7
|
-
const input_1 = require("../../components/input");
|
|
8
|
-
const lazy_list_1 = require("../../components/lazy-list");
|
|
9
8
|
const loading_indicator_1 = require("../../components/loading-indicator");
|
|
10
9
|
const trust_level_badge_1 = require("../../components/trust-level-badge");
|
|
11
10
|
const globals_1 = require("../../globals");
|
|
12
11
|
const hooks_1 = require("../../hooks");
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const [
|
|
12
|
+
const identity_routes_1 = require("../identity/identity-routes");
|
|
13
|
+
const invite_contact_panel_1 = require("../identity/invite-contact-panel");
|
|
14
|
+
const profile_invite_1 = require("../identity/profile-invite");
|
|
15
|
+
const activity_view_1 = require("../invites/activity-view");
|
|
16
|
+
/** People directory and route-backed invite/profile actions. */
|
|
17
|
+
function ContactList(props = {}) {
|
|
18
|
+
const userContext = (0, hooks_1.usePromise)(peers_sdk_1.getUserContext);
|
|
19
|
+
const [rows, setRows] = (0, react_1.useState)();
|
|
21
20
|
const [error, setError] = (0, react_1.useState)("");
|
|
21
|
+
const [searchText, setSearchText] = (0, react_1.useState)("");
|
|
22
|
+
const [groupRevision, setGroupRevision] = (0, react_1.useState)(0);
|
|
22
23
|
(0, react_1.useEffect)(() => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
if (!userContext) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
let disposed = false;
|
|
28
|
+
const subscriptions = [];
|
|
29
|
+
const refresh = async () => {
|
|
30
|
+
try {
|
|
31
|
+
const next = await loadPeople(userContext);
|
|
32
|
+
if (!disposed) {
|
|
33
|
+
setRows(next);
|
|
34
|
+
setError("");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (!disposed) {
|
|
39
|
+
setError(err.message || "Could not load people.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const personalUsers = (0, peers_sdk_1.Users)(userContext.userDataContext);
|
|
44
|
+
const personalGroups = (0, peers_sdk_1.Groups)(userContext.userDataContext);
|
|
45
|
+
subscriptions.push(personalUsers.dataChanged.subscribe(() => void refresh()));
|
|
46
|
+
subscriptions.push(personalGroups.dataChanged.subscribe(() => setGroupRevision((value) => value + 1)));
|
|
47
|
+
void personalGroups.list({ disabled: { $ne: true } }).then((groups) => {
|
|
48
|
+
if (disposed) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
for (const group of groups) {
|
|
52
|
+
const context = userContext.getDataContext(group.groupId);
|
|
53
|
+
subscriptions.push((0, peers_sdk_1.Users)(context).dataChanged.subscribe(() => void refresh()));
|
|
54
|
+
subscriptions.push((0, peers_sdk_1.GroupMembers)(context).dataChanged.subscribe(() => void refresh()));
|
|
55
|
+
}
|
|
56
|
+
void refresh();
|
|
33
57
|
});
|
|
34
58
|
return () => {
|
|
35
|
-
|
|
59
|
+
disposed = true;
|
|
60
|
+
for (const subscription of subscriptions) {
|
|
61
|
+
subscription.unsubscribe();
|
|
62
|
+
}
|
|
36
63
|
};
|
|
37
|
-
}, []);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const [searchTextObs] = (0, react_1.useState)(() => (0, peers_sdk_1.observable)(""));
|
|
43
|
-
const [searchText] = (0, hooks_1.useObservable)(searchTextObs);
|
|
44
|
-
const [cursorObs] = (0, react_1.useState)(() => (0, peers_sdk_1.observable)());
|
|
45
|
-
const [existingContacts, setExistingContacts] = (0, react_1.useState)([]);
|
|
46
|
-
const newCursor = (0, react_1.useCallback)(async () => {
|
|
47
|
-
const cursor = await (0, peers_sdk_1.usersCursor)({ name: { $matchWords: searchText } });
|
|
48
|
-
cursorObs(cursor);
|
|
49
|
-
return cursor;
|
|
50
|
-
}, [searchText, cursorObs]);
|
|
51
|
-
(0, react_1.useEffect)(() => {
|
|
52
|
-
void newCursor();
|
|
53
|
-
setExistingContacts([]); // Reset existing contacts when search changes
|
|
54
|
-
}, [newCursor]);
|
|
55
|
-
async function loadMore(existing) {
|
|
56
|
-
const moreMatches = [];
|
|
57
|
-
const cursor = cursorObs() || (await newCursor());
|
|
58
|
-
for await (const nextContact of cursor) {
|
|
59
|
-
if (existing.some((e) => e.userId === nextContact.userId))
|
|
60
|
-
continue;
|
|
61
|
-
if (moreMatches.some((e) => e.userId === nextContact.userId))
|
|
62
|
-
continue;
|
|
63
|
-
moreMatches.push(nextContact);
|
|
64
|
-
if (searchText.length && moreMatches.length > 5)
|
|
65
|
-
break;
|
|
66
|
-
if (moreMatches.length >= 10)
|
|
67
|
-
break;
|
|
64
|
+
}, [groupRevision, userContext]);
|
|
65
|
+
const filteredRows = (0, react_1.useMemo)(() => {
|
|
66
|
+
const query = searchText.trim().toLocaleLowerCase();
|
|
67
|
+
if (!query) {
|
|
68
|
+
return rows ?? [];
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
return (rows ?? []).filter((row) => row.user.name.toLocaleLowerCase().includes(query) ||
|
|
71
|
+
row.sharedGroups.some((group) => group.name.toLocaleLowerCase().includes(query)));
|
|
72
|
+
}, [rows, searchText]);
|
|
73
|
+
const duplicateNames = (0, react_1.useMemo)(() => {
|
|
74
|
+
const counts = new Map();
|
|
75
|
+
for (const row of rows ?? []) {
|
|
76
|
+
const key = row.user.name.trim().toLocaleLowerCase();
|
|
77
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
71
78
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
name: "New User",
|
|
86
|
-
publicKey: "",
|
|
87
|
-
publicBoxKey: "",
|
|
88
|
-
});
|
|
89
|
-
await usersTable.save(newContact);
|
|
90
|
-
// Set initial trust level
|
|
91
|
-
await (0, peers_sdk_1.setUserTrustLevel)(userId.trim(), peers_sdk_1.TrustLevel.NewUser, userContext.userDataContext);
|
|
92
|
-
// Navigate to the details screen for the new contact
|
|
93
|
-
(0, globals_1.mainContentPath)(`contacts/${userId.trim()}`);
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
console.error("Error creating contact:", error);
|
|
79
|
+
return counts;
|
|
80
|
+
}, [rows]);
|
|
81
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "container-xxl p-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center mb-3", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("h4", { className: "mb-1", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-person-fill-check me-2" }), "People"] }), (0, jsx_runtime_1.jsx)("div", { className: "small text-muted", children: "Contacts and people you know through groups" })] }), (0, jsx_runtime_1.jsxs)("a", { href: `#${identity_routes_1.IDENTITY_PEOPLE_SHARE_PATH}`, className: "btn btn-outline-primary btn-sm", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-qr-code me-1" }), "Share my profile"] })] }), props.action ? ((0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsx)("div", { className: "d-flex justify-content-end mb-2", children: (0, jsx_runtime_1.jsx)("a", { href: `#${identity_routes_1.IDENTITY_PEOPLE_PATH}`, className: "btn btn-link btn-sm", children: "Close" }) }), props.action === "share" ? ((0, jsx_runtime_1.jsx)(profile_invite_1.ProfileInviteSection, {})) : ((0, jsx_runtime_1.jsx)(invite_contact_panel_1.InviteContactPanel, { onClose: () => (0, globals_1.mainContentPath)(identity_routes_1.IDENTITY_PEOPLE_PATH) }))] })) : null, (0, jsx_runtime_1.jsx)(activity_view_1.PendingWorkLink, { type: "contact" }), (0, jsx_runtime_1.jsxs)("div", { className: "position-relative mb-3", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-search position-absolute text-muted", style: { left: 12, top: "50%", transform: "translateY(-50%)" } }), (0, jsx_runtime_1.jsx)("input", { type: "search", className: "form-control ps-5", value: searchText, onChange: (event) => setSearchText(event.target.value), placeholder: "Search people or shared groups", "aria-label": "Search people" })] }), error ? (0, jsx_runtime_1.jsx)("div", { className: "alert alert-danger", children: error }) : null, !rows && !error ? (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {}) : null, rows && filteredRows.length === 0 ? ((0, jsx_runtime_1.jsxs)("div", { className: "border rounded p-4 text-center", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-people fs-2 text-muted" }), (0, jsx_runtime_1.jsx)("h6", { className: "mt-2", children: searchText ? "No matching people" : "No people yet" }), (0, jsx_runtime_1.jsx)("p", { className: "small text-muted mb-0", children: searchText
|
|
82
|
+
? "Try another name or group."
|
|
83
|
+
: "Invite someone or join a group to start building your network." })] })) : null, filteredRows.length > 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "list-group", children: filteredRows.map((row) => ((0, jsx_runtime_1.jsx)(PersonRow, { row: row, duplicateName: (duplicateNames.get(row.user.name.trim().toLocaleLowerCase()) ?? 0) > 1 }, row.user.userId))) })) : null] }));
|
|
84
|
+
}
|
|
85
|
+
async function loadPeople(userContext) {
|
|
86
|
+
const personalUsers = await (0, peers_sdk_1.Users)(userContext.userDataContext).list();
|
|
87
|
+
const personalById = new Map(personalUsers.map((user) => [user.userId, user]));
|
|
88
|
+
const summaries = new Map();
|
|
89
|
+
for (const user of personalUsers) {
|
|
90
|
+
if (user.userId === userContext.userId) {
|
|
91
|
+
continue;
|
|
97
92
|
}
|
|
93
|
+
summaries.set(user.userId, {
|
|
94
|
+
user,
|
|
95
|
+
isContact: true,
|
|
96
|
+
trustLevel: await (0, peers_sdk_1.getUserTrustLevel)(userContext.userDataContext, user.userId),
|
|
97
|
+
sharedGroups: [],
|
|
98
|
+
});
|
|
98
99
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (existingContacts.length === 0) {
|
|
107
|
-
// No results found, check if it's a valid user ID
|
|
108
|
-
if (isValidUserId(input)) {
|
|
109
|
-
// Clear search and create new contact with the ID
|
|
110
|
-
searchTextObs("");
|
|
111
|
-
await createContact(input);
|
|
100
|
+
const groups = await (0, peers_sdk_1.Groups)(userContext.userDataContext).list({ disabled: { $ne: true } });
|
|
101
|
+
for (const group of groups) {
|
|
102
|
+
const context = userContext.getDataContext(group.groupId);
|
|
103
|
+
const members = await (0, peers_sdk_1.GroupMembers)(context).list({ deleted: { $ne: true } });
|
|
104
|
+
for (const member of members) {
|
|
105
|
+
if (member.userId === userContext.userId) {
|
|
106
|
+
continue;
|
|
112
107
|
}
|
|
108
|
+
const existing = summaries.get(member.userId);
|
|
109
|
+
const user = personalById.get(member.userId) ?? (await (0, peers_sdk_1.Users)(context).get(member.userId));
|
|
110
|
+
if (!user) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const summary = existing ?? {
|
|
114
|
+
user,
|
|
115
|
+
isContact: false,
|
|
116
|
+
sharedGroups: [],
|
|
117
|
+
};
|
|
118
|
+
if (!summary.sharedGroups.some((entry) => entry.groupId === group.groupId)) {
|
|
119
|
+
summary.sharedGroups.push({ groupId: group.groupId, name: group.name });
|
|
120
|
+
}
|
|
121
|
+
summaries.set(member.userId, summary);
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
return [...summaries.values()].sort((left, right) => left.user.name.localeCompare(right.user.name));
|
|
125
|
+
}
|
|
126
|
+
/** Compact, fully clickable row in the People directory. */
|
|
127
|
+
function PersonRow(props) {
|
|
128
|
+
const { row } = props;
|
|
129
|
+
const groups = row.sharedGroups;
|
|
130
|
+
const relationship = row.isContact
|
|
131
|
+
? groups.length > 0
|
|
132
|
+
? `Contact · ${sharedGroupLabel(groups)}`
|
|
133
|
+
: "Contact"
|
|
134
|
+
: sharedGroupLabel(groups);
|
|
135
|
+
return ((0, jsx_runtime_1.jsxs)("a", { href: `#${identity_routes_1.IDENTITY_PEOPLE_PATH}/${row.user.userId}`, className: "list-group-item list-group-item-action d-flex align-items-center gap-3 py-3", children: [(0, jsx_runtime_1.jsx)("span", { className: "rounded-circle bg-body-secondary d-inline-flex align-items-center justify-content-center flex-shrink-0 fw-semibold", style: { width: 42, height: 42 }, "aria-hidden": "true", children: initials(row.user.name) }), (0, jsx_runtime_1.jsxs)("span", { className: "flex-grow-1 overflow-hidden", children: [(0, jsx_runtime_1.jsxs)("span", { className: "d-flex align-items-center gap-2 flex-wrap", children: [(0, jsx_runtime_1.jsx)("span", { className: "fw-semibold text-truncate", children: row.user.name || "Unnamed person" }), props.duplicateName ? ((0, jsx_runtime_1.jsxs)("code", { className: "small text-muted", children: ["\u2026", row.user.userId.slice(-6)] })) : null] }), (0, jsx_runtime_1.jsx)("span", { className: "small text-muted text-truncate d-block", children: relationship })] }), row.isContact ? ((0, jsx_runtime_1.jsx)(trust_level_badge_1.TrustLevelBadge, { level: row.trustLevel ?? peers_sdk_1.TrustLevel.Unknown })) : ((0, jsx_runtime_1.jsx)("span", { className: "badge text-bg-light border", children: "Shared group" })), (0, jsx_runtime_1.jsx)("i", { className: "bi bi-chevron-right text-muted", "aria-hidden": "true" })] }));
|
|
136
|
+
}
|
|
137
|
+
function sharedGroupLabel(groups) {
|
|
138
|
+
if (groups.length === 0) {
|
|
139
|
+
return "Known through a shared group";
|
|
140
|
+
}
|
|
141
|
+
if (groups.length === 1) {
|
|
142
|
+
return `Shared group: ${groups[0].name}`;
|
|
127
143
|
}
|
|
128
|
-
return
|
|
144
|
+
return `${groups.length} shared groups`;
|
|
145
|
+
}
|
|
146
|
+
function initials(name) {
|
|
147
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
148
|
+
return (parts
|
|
149
|
+
.slice(0, 2)
|
|
150
|
+
.map((part) => part[0]?.toLocaleUpperCase())
|
|
151
|
+
.join("") || "?");
|
|
129
152
|
}
|
|
130
|
-
const ContactCard = ({ contact }) => {
|
|
131
|
-
const getSourceLabel = (source) => {
|
|
132
|
-
switch (source) {
|
|
133
|
-
case "userDataContext":
|
|
134
|
-
return "Personal";
|
|
135
|
-
case "currentDataContext":
|
|
136
|
-
return "Current Context";
|
|
137
|
-
case "otherDataContexts":
|
|
138
|
-
return "Groups";
|
|
139
|
-
default:
|
|
140
|
-
return source;
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
const getSourceIcon = (source) => {
|
|
144
|
-
switch (source) {
|
|
145
|
-
case "userDataContext":
|
|
146
|
-
return "bi-person-fill";
|
|
147
|
-
case "currentDataContext":
|
|
148
|
-
return "bi-house-fill";
|
|
149
|
-
case "otherDataContexts":
|
|
150
|
-
return "bi-people-fill";
|
|
151
|
-
default:
|
|
152
|
-
return "bi-question-circle";
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "list-group-item d-flex align-items-center justify-content-between py-2", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-person-circle me-3", style: { fontSize: "32px", color: "#6c757d" } }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: contact.name }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center", children: [(0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: contact.userId }), (0, jsx_runtime_1.jsx)("span", { className: "mx-1 text-muted", children: "\u2022" }), (0, jsx_runtime_1.jsxs)("small", { className: "text-info d-flex align-items-center", children: [(0, jsx_runtime_1.jsx)("i", { className: `${getSourceIcon(contact.source)} me-1` }), getSourceLabel(contact.source)] })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center", children: [(0, jsx_runtime_1.jsx)(trust_level_badge_1.TrustLevelBadge, { level: contact.trustLevel || peers_sdk_1.TrustLevel.Unknown }), (0, jsx_runtime_1.jsxs)("a", { href: `#contacts/${contact.userId}`, className: "btn btn-outline-primary btn-sm ms-2", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-eye me-1" }), "View"] })] })] }));
|
|
156
|
-
};
|
|
157
|
-
(0, ui_loader_1.registerInternalPeersUI)({
|
|
158
|
-
peersUIId: "00mfc4ql0lgqmjxrpvcd4ufk0",
|
|
159
|
-
component: ContactList,
|
|
160
|
-
routes: [
|
|
161
|
-
{
|
|
162
|
-
isMatch: (_props, context) => context.path === "contacts",
|
|
163
|
-
uiCategory: "screen",
|
|
164
|
-
priority: 2,
|
|
165
|
-
},
|
|
166
|
-
],
|
|
167
|
-
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
5
|
+
const react_1 = require("@testing-library/react");
|
|
6
|
+
const contact_list_1 = require("./contact-list");
|
|
7
|
+
jest.mock("../../globals", () => ({
|
|
8
|
+
mainContentPath: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
jest.mock("../../hooks", () => ({
|
|
11
|
+
usePromise: jest.fn(),
|
|
12
|
+
}));
|
|
13
|
+
jest.mock("../../ui-router/ui-loader", () => ({
|
|
14
|
+
registerInternalPeersUI: jest.fn(),
|
|
15
|
+
}));
|
|
16
|
+
jest.mock("../identity/invite-contact-panel", () => ({
|
|
17
|
+
InviteContactPanel: () => null,
|
|
18
|
+
}));
|
|
19
|
+
jest.mock("../identity/profile-invite", () => ({
|
|
20
|
+
ProfileInviteSection: () => null,
|
|
21
|
+
}));
|
|
22
|
+
jest.mock("../invites/activity-view", () => ({
|
|
23
|
+
PendingWorkLink: () => null,
|
|
24
|
+
}));
|
|
25
|
+
const person = {
|
|
26
|
+
user: {
|
|
27
|
+
userId: "abcdefghijklmnopqrstuvwxy",
|
|
28
|
+
name: "Alex",
|
|
29
|
+
publicKey: "signing-key",
|
|
30
|
+
publicBoxKey: "box-key",
|
|
31
|
+
},
|
|
32
|
+
isContact: true,
|
|
33
|
+
trustLevel: peers_sdk_1.TrustLevel.Known,
|
|
34
|
+
sharedGroups: [{ groupId: "zyxwvutsrqponmlkjihgfedcb", name: "Neighbors" }],
|
|
35
|
+
};
|
|
36
|
+
describe("PersonRow", () => {
|
|
37
|
+
it("shows useful relationship context without exposing an ordinary user id", () => {
|
|
38
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(contact_list_1.PersonRow, { row: person, duplicateName: false }));
|
|
39
|
+
expect(react_1.screen.getByText("Alex")).toBeInTheDocument();
|
|
40
|
+
expect(react_1.screen.getByText("Contact · Shared group: Neighbors")).toBeInTheDocument();
|
|
41
|
+
expect(react_1.screen.queryByText(person.user.userId)).not.toBeInTheDocument();
|
|
42
|
+
expect(react_1.screen.getByRole("link")).toHaveAttribute("href", `#identity/people/${person.user.userId}`);
|
|
43
|
+
});
|
|
44
|
+
it("adds only a short discriminator when names collide", () => {
|
|
45
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(contact_list_1.PersonRow, { row: person, duplicateName: true }));
|
|
46
|
+
expect(react_1.screen.getByText(`…${person.user.userId.slice(-6)}`)).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
});
|