@peers-app/peers-ui 0.25.2 → 0.25.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/components/markdown-editor/mention-node.d.ts +4 -0
- package/dist/components/markdown-editor/mention-node.js +4 -1
- package/dist/components/responsive-section-nav.d.ts +34 -0
- package/dist/components/responsive-section-nav.js +19 -0
- package/dist/components/responsive-section-nav.test.d.ts +1 -0
- package/dist/components/responsive-section-nav.test.js +45 -0
- package/dist/components/router.d.ts +2 -3
- package/dist/components/router.js +6 -9
- package/dist/globals-hash.test.d.ts +1 -0
- package/dist/globals-hash.test.js +16 -0
- package/dist/globals.d.ts +19 -0
- package/dist/globals.js +51 -12
- package/dist/hooks/use-host-contract.d.ts +18 -0
- package/dist/hooks/use-host-contract.js +53 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/peers-services-registration.d.ts +1 -5
- package/dist/peers-services-registration.js +6 -16
- package/dist/root-layout/root-layout-host.js +7 -4
- package/dist/root-layout/root-layout-host.test.js +3 -6
- package/dist/screens/account/account-screen.d.ts +1 -0
- package/dist/screens/account/account-screen.js +29 -53
- package/dist/screens/account/account-screen.test.d.ts +1 -0
- package/dist/screens/account/account-screen.test.js +47 -0
- package/dist/screens/contacts/contact-details.js +2 -23
- package/dist/screens/contacts/contact-list.d.ts +24 -1
- package/dist/screens/contacts/contact-list.js +128 -143
- package/dist/screens/contacts/contact-list.test.d.ts +1 -0
- package/dist/screens/contacts/contact-list.test.js +48 -0
- package/dist/screens/contacts/index.d.ts +0 -2
- package/dist/screens/contacts/index.js +0 -3
- package/dist/screens/device-pairing/device-pairing.js +17 -16
- package/dist/screens/device-pairing/device-pairing.test.js +4 -9
- package/dist/screens/device-pairing/identity-ipc.d.ts +11 -0
- package/dist/screens/device-pairing/identity-ipc.js +20 -0
- package/dist/screens/device-pairing/use-device-pairing-state.d.ts +2 -2
- package/dist/screens/device-pairing/use-device-pairing-state.js +7 -25
- package/dist/screens/devices/devices-list.d.ts +18 -0
- package/dist/screens/devices/devices-list.js +173 -0
- package/dist/screens/devices/devices-list.test.d.ts +1 -0
- package/dist/screens/devices/devices-list.test.js +53 -0
- package/dist/screens/devices/index.d.ts +1 -0
- package/dist/screens/devices/index.js +5 -0
- package/dist/screens/groups/group-details.d.ts +1 -0
- package/dist/screens/groups/group-details.js +4 -63
- package/dist/screens/groups/group-invite-panel.js +2 -2
- package/dist/screens/groups/group-list.d.ts +18 -1
- package/dist/screens/groups/group-list.js +107 -136
- package/dist/screens/groups/group-list.test.d.ts +1 -0
- package/dist/screens/groups/group-list.test.js +38 -0
- package/dist/screens/groups/group-members.d.ts +1 -0
- package/dist/screens/groups/group-members.js +2 -1
- package/dist/screens/groups/index.d.ts +0 -3
- package/dist/screens/groups/index.js +0 -4
- package/dist/screens/identity/identity-routes.d.ts +107 -0
- package/dist/screens/identity/identity-routes.js +245 -0
- package/dist/screens/identity/identity-routes.test.d.ts +1 -0
- package/dist/screens/identity/identity-routes.test.js +51 -0
- package/dist/screens/identity/identity-screen.d.ts +9 -0
- package/dist/screens/identity/identity-screen.js +143 -0
- package/dist/screens/identity/identity-screen.test.d.ts +1 -0
- package/dist/screens/identity/identity-screen.test.js +103 -0
- package/dist/screens/identity/index.d.ts +2 -0
- package/dist/screens/identity/index.js +6 -0
- package/dist/screens/identity/invite-contact-panel.d.ts +4 -0
- package/dist/screens/identity/invite-contact-panel.js +30 -0
- package/dist/screens/identity/profile-invite.d.ts +7 -0
- package/dist/screens/identity/profile-invite.js +62 -0
- package/dist/screens/invites/accept-invite.js +44 -2
- package/dist/screens/invites/accept-invite.test.d.ts +1 -0
- package/dist/screens/invites/accept-invite.test.js +103 -0
- package/dist/screens/invites/activity-view.d.ts +24 -0
- package/dist/screens/invites/activity-view.js +64 -0
- package/dist/screens/invites/activity-view.test.d.ts +1 -0
- package/dist/screens/invites/activity-view.test.js +108 -0
- package/dist/screens/invites/index.d.ts +1 -2
- package/dist/screens/invites/index.js +1 -3
- package/dist/screens/invites/invite-capture.d.ts +0 -2
- package/dist/screens/invites/invite-capture.js +1 -3
- package/dist/screens/invites/invite-classify.test.d.ts +1 -0
- package/dist/screens/invites/invite-classify.test.js +115 -0
- package/dist/screens/invites/invites-ipc.d.ts +1 -1
- package/dist/screens/invites/invites-ipc.js +5 -45
- package/dist/screens/invites/pending-invites-list.d.ts +35 -16
- package/dist/screens/invites/pending-invites-list.js +75 -30
- package/dist/screens/network-viewer/network-viewer-ipc.js +4 -59
- package/dist/screens/search/global-search.d.ts +1 -0
- package/dist/screens/search/global-search.js +2 -1
- package/dist/screens/settings/settings-page.js +25 -58
- package/dist/screens/setup-user.js +5 -5
- package/dist/screens/tools/tool-access-ipc.d.ts +2 -0
- package/dist/screens/tools/tool-access-ipc.js +8 -55
- package/dist/screens/welcome-modal.js +3 -2
- package/dist/system-apps/identity.app.d.ts +3 -0
- package/dist/system-apps/identity.app.js +10 -0
- package/dist/system-apps/index.d.ts +1 -4
- package/dist/system-apps/index.js +5 -17
- package/dist/tabs-layout/network-status-indicator.js +10 -3
- package/dist/tabs-layout/network-status-indicator.test.js +15 -0
- package/dist/tabs-layout/tabs-layout.js +3 -1
- package/dist/tabs-layout/tabs-state.d.ts +79 -1
- package/dist/tabs-layout/tabs-state.js +256 -79
- package/dist/tabs-layout/tabs-state.test.d.ts +1 -0
- package/dist/tabs-layout/tabs-state.test.js +110 -0
- package/package.json +3 -3
- package/src/components/markdown-editor/mention-node.ts +5 -1
- package/src/components/responsive-section-nav.test.tsx +88 -0
- package/src/components/responsive-section-nav.tsx +92 -0
- package/src/components/router.tsx +6 -9
- package/src/globals-hash.test.ts +19 -0
- package/src/globals.tsx +53 -13
- package/src/hooks/use-host-contract.ts +75 -0
- package/src/index.tsx +1 -0
- package/src/peers-services-registration.ts +6 -17
- package/src/root-layout/root-layout-host.test.tsx +4 -7
- package/src/root-layout/root-layout-host.tsx +9 -11
- package/src/screens/account/account-screen.test.tsx +61 -0
- package/src/screens/account/account-screen.tsx +116 -95
- package/src/screens/contacts/contact-details.tsx +4 -26
- package/src/screens/contacts/contact-list.test.tsx +59 -0
- package/src/screens/contacts/contact-list.tsx +246 -260
- package/src/screens/contacts/index.ts +0 -4
- package/src/screens/device-pairing/device-pairing.test.tsx +7 -9
- package/src/screens/device-pairing/device-pairing.tsx +19 -23
- package/src/screens/device-pairing/identity-ipc.ts +25 -0
- package/src/screens/device-pairing/use-device-pairing-state.ts +10 -33
- package/src/screens/devices/devices-list.test.tsx +68 -0
- package/src/screens/devices/devices-list.tsx +361 -0
- package/src/screens/devices/index.ts +1 -0
- package/src/screens/groups/group-details.tsx +8 -142
- package/src/screens/groups/group-invite-panel.tsx +2 -7
- package/src/screens/groups/group-list.test.tsx +44 -0
- package/src/screens/groups/group-list.tsx +227 -288
- package/src/screens/groups/group-members.tsx +3 -2
- package/src/screens/groups/index.ts +0 -5
- package/src/screens/identity/identity-routes.test.ts +70 -0
- package/src/screens/identity/identity-routes.ts +301 -0
- package/src/screens/identity/identity-screen.test.tsx +138 -0
- package/src/screens/identity/identity-screen.tsx +282 -0
- package/src/screens/identity/index.ts +2 -0
- package/src/screens/identity/invite-contact-panel.tsx +45 -0
- package/src/screens/identity/profile-invite.tsx +93 -0
- package/src/screens/invites/accept-invite.test.tsx +115 -0
- package/src/screens/invites/accept-invite.tsx +50 -3
- package/src/screens/invites/activity-view.test.tsx +127 -0
- package/src/screens/invites/activity-view.tsx +162 -0
- package/src/screens/invites/index.ts +1 -4
- package/src/screens/invites/invite-capture.ts +0 -3
- package/src/screens/invites/invite-classify.test.ts +129 -0
- package/src/screens/invites/invites-ipc.ts +6 -55
- package/src/screens/invites/pending-invites-list.tsx +94 -62
- package/src/screens/network-viewer/network-viewer-ipc.ts +8 -82
- package/src/screens/search/global-search.tsx +2 -1
- package/src/screens/settings/settings-page.tsx +53 -126
- package/src/screens/setup-user.tsx +5 -6
- package/src/screens/tools/tool-access-ipc.ts +13 -74
- package/src/screens/welcome-modal.tsx +3 -3
- package/src/system-apps/identity.app.ts +9 -0
- package/src/system-apps/index.ts +3 -12
- package/src/tabs-layout/network-status-indicator.test.tsx +22 -0
- package/src/tabs-layout/network-status-indicator.tsx +31 -0
- package/src/tabs-layout/tabs-layout.tsx +7 -1
- package/src/tabs-layout/tabs-state.test.ts +152 -0
- package/src/tabs-layout/tabs-state.ts +300 -83
- package/dist/screens/invites/invites-screen.d.ts +0 -9
- package/dist/screens/invites/invites-screen.js +0 -36
- package/dist/system-apps/account.app.d.ts +0 -2
- package/dist/system-apps/account.app.js +0 -8
- package/dist/system-apps/contacts.app.d.ts +0 -2
- package/dist/system-apps/contacts.app.js +0 -9
- package/dist/system-apps/groups.app.d.ts +0 -2
- package/dist/system-apps/groups.app.js +0 -9
- package/dist/system-apps/invites.app.d.ts +0 -2
- package/dist/system-apps/invites.app.js +0 -9
- package/src/screens/invites/invites-screen.tsx +0 -78
- package/src/system-apps/account.app.ts +0 -7
- package/src/system-apps/contacts.app.ts +0 -8
- package/src/system-apps/groups.app.ts +0 -8
- package/src/system-apps/invites.app.ts +0 -8
|
@@ -1,300 +1,286 @@
|
|
|
1
1
|
import {
|
|
2
|
+
GroupMembers,
|
|
3
|
+
Groups,
|
|
2
4
|
getUserContext,
|
|
3
|
-
|
|
4
|
-
type
|
|
5
|
-
type IUserWithSource,
|
|
6
|
-
observable,
|
|
7
|
-
setUserTrustLevel,
|
|
5
|
+
getUserTrustLevel,
|
|
6
|
+
type IUser,
|
|
8
7
|
TrustLevel,
|
|
8
|
+
type UserContext,
|
|
9
9
|
Users,
|
|
10
|
-
usersCursor,
|
|
11
10
|
} from "@peers-app/peers-sdk";
|
|
12
|
-
import
|
|
13
|
-
import { useCallback, useEffect, useState } from "react";
|
|
14
|
-
import { Input } from "../../components/input";
|
|
15
|
-
import { LazyList } from "../../components/lazy-list";
|
|
11
|
+
import { useEffect, useMemo, useState } from "react";
|
|
16
12
|
import { LoadingIndicator } from "../../components/loading-indicator";
|
|
17
13
|
import { TrustLevelBadge } from "../../components/trust-level-badge";
|
|
18
14
|
import { mainContentPath } from "../../globals";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import { PendingInvitesList } from "../invites/pending-invites-list";
|
|
15
|
+
import { usePromise } from "../../hooks";
|
|
16
|
+
import { IDENTITY_PEOPLE_PATH, IDENTITY_PEOPLE_SHARE_PATH } from "../identity/identity-routes";
|
|
17
|
+
import { InviteContactPanel } from "../identity/invite-contact-panel";
|
|
18
|
+
import { ProfileInviteSection } from "../identity/profile-invite";
|
|
19
|
+
import { PendingWorkLink } from "../invites/activity-view";
|
|
25
20
|
|
|
26
|
-
/**
|
|
27
|
-
function
|
|
28
|
-
const
|
|
21
|
+
/** People directory and route-backed invite/profile actions. */
|
|
22
|
+
export function ContactList(props: { action?: "share" | "invite" } = {}) {
|
|
23
|
+
const userContext = usePromise(getUserContext);
|
|
24
|
+
const [rows, setRows] = useState<IPersonSummary[] | undefined>();
|
|
29
25
|
const [error, setError] = useState("");
|
|
26
|
+
const [searchText, setSearchText] = useState("");
|
|
27
|
+
const [groupRevision, setGroupRevision] = useState(0);
|
|
30
28
|
|
|
31
29
|
useEffect(() => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
.then((tools) => tools.createContactInvite({}))
|
|
35
|
-
.then(({ invite: created }) => {
|
|
36
|
-
if (!cancelled) setInvite(created);
|
|
37
|
-
})
|
|
38
|
-
.catch((err) => {
|
|
39
|
-
if (!cancelled) setError((err as Error).message || "Could not create an invite.");
|
|
40
|
-
});
|
|
41
|
-
return () => {
|
|
42
|
-
cancelled = true;
|
|
43
|
-
};
|
|
44
|
-
}, []);
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<div className="card mb-3">
|
|
48
|
-
<div className="card-body">
|
|
49
|
-
<h6 className="card-title">Invite a contact</h6>
|
|
50
|
-
{error ? <div className="alert alert-danger py-2">{error}</div> : null}
|
|
51
|
-
{invite ? (
|
|
52
|
-
<InviteShare
|
|
53
|
-
invite={invite}
|
|
54
|
-
description="Send this link or show the QR. When they accept, you are connected — no need to be online at the same time."
|
|
55
|
-
shareText="Connect with me on Peers:"
|
|
56
|
-
onDone={onClose}
|
|
57
|
-
/>
|
|
58
|
-
) : !error ? (
|
|
59
|
-
<LoadingIndicator />
|
|
60
|
-
) : null}
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function ContactList() {
|
|
67
|
-
const [inviting, setInviting] = useState(false);
|
|
68
|
-
const [searchTextObs] = useState(() => observable(""));
|
|
69
|
-
const [searchText] = useObservable(searchTextObs);
|
|
70
|
-
const [cursorObs] = useState(() => observable<ICursorIterable<IUserWithSource> | undefined>());
|
|
71
|
-
const [existingContacts, setExistingContacts] = useState<IUserWithSource[]>([]);
|
|
72
|
-
|
|
73
|
-
const newCursor = useCallback(async () => {
|
|
74
|
-
const cursor = await usersCursor({ name: { $matchWords: searchText } });
|
|
75
|
-
cursorObs(cursor);
|
|
76
|
-
return cursor;
|
|
77
|
-
}, [searchText, cursorObs]);
|
|
78
|
-
|
|
79
|
-
useEffect(() => {
|
|
80
|
-
void newCursor();
|
|
81
|
-
setExistingContacts([]); // Reset existing contacts when search changes
|
|
82
|
-
}, [newCursor]);
|
|
83
|
-
|
|
84
|
-
async function loadMore(existing: IUserWithSource[]): Promise<IUserWithSource[]> {
|
|
85
|
-
const moreMatches: IUserWithSource[] = [];
|
|
86
|
-
const cursor = cursorObs() || (await newCursor());
|
|
87
|
-
for await (const nextContact of cursor) {
|
|
88
|
-
if (existing.some((e) => e.userId === nextContact.userId)) continue;
|
|
89
|
-
if (moreMatches.some((e) => e.userId === nextContact.userId)) continue;
|
|
90
|
-
moreMatches.push(nextContact);
|
|
91
|
-
if (searchText.length && moreMatches.length > 5) break;
|
|
92
|
-
if (moreMatches.length >= 10) break;
|
|
30
|
+
if (!userContext) {
|
|
31
|
+
return;
|
|
93
32
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
const userContext = await getUserContext();
|
|
109
|
-
const usersTable = Users(userContext.userDataContext);
|
|
110
|
-
|
|
111
|
-
const newContact = await usersTable.initRecord({
|
|
112
|
-
userId: userId.trim(),
|
|
113
|
-
name: "New User",
|
|
114
|
-
publicKey: "",
|
|
115
|
-
publicBoxKey: "",
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
await usersTable.save(newContact);
|
|
119
|
-
|
|
120
|
-
// Set initial trust level
|
|
121
|
-
await setUserTrustLevel(userId.trim(), TrustLevel.NewUser, userContext.userDataContext);
|
|
122
|
-
|
|
123
|
-
// Navigate to the details screen for the new contact
|
|
124
|
-
mainContentPath(`contacts/${userId.trim()}`);
|
|
125
|
-
} catch (error) {
|
|
126
|
-
console.error("Error creating contact:", error);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function handleSearchSubmit(evt: React.KeyboardEvent<HTMLInputElement>) {
|
|
131
|
-
if (evt.key !== "Enter") return;
|
|
132
|
-
|
|
133
|
-
const input = searchText.trim();
|
|
134
|
-
if (!input) return;
|
|
135
|
-
|
|
136
|
-
// Check if there are any existing results
|
|
137
|
-
if (existingContacts.length === 0) {
|
|
138
|
-
// No results found, check if it's a valid user ID
|
|
139
|
-
if (isValidUserId(input)) {
|
|
140
|
-
// Clear search and create new contact with the ID
|
|
141
|
-
searchTextObs("");
|
|
142
|
-
await createContact(input);
|
|
33
|
+
let disposed = false;
|
|
34
|
+
const subscriptions: Array<{ unsubscribe: () => void }> = [];
|
|
35
|
+
const refresh = async () => {
|
|
36
|
+
try {
|
|
37
|
+
const next = await loadPeople(userContext);
|
|
38
|
+
if (!disposed) {
|
|
39
|
+
setRows(next);
|
|
40
|
+
setError("");
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (!disposed) {
|
|
44
|
+
setError((err as Error).message || "Could not load people.");
|
|
45
|
+
}
|
|
143
46
|
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
)}
|
|
160
|
-
{contact.source === "currentDataContext" && <h5>Users in this group</h5>}
|
|
161
|
-
{contact.source === "otherDataContexts" && <h5>Users from other groups</h5>}
|
|
162
|
-
</div>,
|
|
163
|
-
);
|
|
164
|
-
lastSource = contact.source;
|
|
47
|
+
};
|
|
48
|
+
const personalUsers = Users(userContext.userDataContext);
|
|
49
|
+
const personalGroups = Groups(userContext.userDataContext);
|
|
50
|
+
subscriptions.push(personalUsers.dataChanged.subscribe(() => void refresh()));
|
|
51
|
+
subscriptions.push(
|
|
52
|
+
personalGroups.dataChanged.subscribe(() => setGroupRevision((value) => value + 1)),
|
|
53
|
+
);
|
|
54
|
+
void personalGroups.list({ disabled: { $ne: true } }).then((groups) => {
|
|
55
|
+
if (disposed) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const group of groups) {
|
|
59
|
+
const context = userContext.getDataContext(group.groupId);
|
|
60
|
+
subscriptions.push(Users(context).dataChanged.subscribe(() => void refresh()));
|
|
61
|
+
subscriptions.push(GroupMembers(context).dataChanged.subscribe(() => void refresh()));
|
|
165
62
|
}
|
|
166
|
-
|
|
63
|
+
void refresh();
|
|
167
64
|
});
|
|
65
|
+
return () => {
|
|
66
|
+
disposed = true;
|
|
67
|
+
for (const subscription of subscriptions) {
|
|
68
|
+
subscription.unsubscribe();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}, [groupRevision, userContext]);
|
|
168
72
|
|
|
169
|
-
|
|
170
|
-
|
|
73
|
+
const filteredRows = useMemo(() => {
|
|
74
|
+
const query = searchText.trim().toLocaleLowerCase();
|
|
75
|
+
if (!query) {
|
|
76
|
+
return rows ?? [];
|
|
77
|
+
}
|
|
78
|
+
return (rows ?? []).filter(
|
|
79
|
+
(row) =>
|
|
80
|
+
row.user.name.toLocaleLowerCase().includes(query) ||
|
|
81
|
+
row.sharedGroups.some((group) => group.name.toLocaleLowerCase().includes(query)),
|
|
82
|
+
);
|
|
83
|
+
}, [rows, searchText]);
|
|
84
|
+
const duplicateNames = useMemo(() => {
|
|
85
|
+
const counts = new Map<string, number>();
|
|
86
|
+
for (const row of rows ?? []) {
|
|
87
|
+
const key = row.user.name.trim().toLocaleLowerCase();
|
|
88
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
89
|
+
}
|
|
90
|
+
return counts;
|
|
91
|
+
}, [rows]);
|
|
171
92
|
|
|
172
93
|
return (
|
|
173
|
-
<div className="container-
|
|
94
|
+
<div className="container-xxl p-3">
|
|
174
95
|
<div className="d-flex justify-content-between align-items-center mb-3">
|
|
175
|
-
<
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
<
|
|
181
|
-
<i className="bi-qr-code-scan me-1" />
|
|
182
|
-
Accept invite
|
|
183
|
-
</a>
|
|
184
|
-
<button
|
|
185
|
-
type="button"
|
|
186
|
-
className="btn btn-primary btn-sm"
|
|
187
|
-
onClick={() => setInviting(true)}
|
|
188
|
-
disabled={inviting}
|
|
189
|
-
>
|
|
190
|
-
<i className="bi-person-plus me-1" />
|
|
191
|
-
Invite
|
|
192
|
-
</button>
|
|
96
|
+
<div>
|
|
97
|
+
<h4 className="mb-1">
|
|
98
|
+
<i className="bi-person-fill-check me-2" />
|
|
99
|
+
People
|
|
100
|
+
</h4>
|
|
101
|
+
<div className="small text-muted">Contacts and people you know through groups</div>
|
|
193
102
|
</div>
|
|
103
|
+
<a href={`#${IDENTITY_PEOPLE_SHARE_PATH}`} className="btn btn-outline-primary btn-sm">
|
|
104
|
+
<i className="bi bi-qr-code me-1" />
|
|
105
|
+
Share my profile
|
|
106
|
+
</a>
|
|
194
107
|
</div>
|
|
195
108
|
|
|
196
|
-
{
|
|
197
|
-
|
|
109
|
+
{props.action ? (
|
|
110
|
+
<div className="mb-3">
|
|
111
|
+
<div className="d-flex justify-content-end mb-2">
|
|
112
|
+
<a href={`#${IDENTITY_PEOPLE_PATH}`} className="btn btn-link btn-sm">
|
|
113
|
+
Close
|
|
114
|
+
</a>
|
|
115
|
+
</div>
|
|
116
|
+
{props.action === "share" ? (
|
|
117
|
+
<ProfileInviteSection />
|
|
118
|
+
) : (
|
|
119
|
+
<InviteContactPanel onClose={() => mainContentPath(IDENTITY_PEOPLE_PATH)} />
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
) : null}
|
|
123
|
+
<PendingWorkLink type="contact" />
|
|
198
124
|
|
|
199
|
-
<div className="
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
placeholder="Search or create contacts"
|
|
204
|
-
autoFocus={true}
|
|
205
|
-
onKeyUp={handleSearchSubmit}
|
|
125
|
+
<div className="position-relative mb-3">
|
|
126
|
+
<i
|
|
127
|
+
className="bi bi-search position-absolute text-muted"
|
|
128
|
+
style={{ left: 12, top: "50%", transform: "translateY(-50%)" }}
|
|
206
129
|
/>
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
renderItems={renderItems}
|
|
215
|
-
loadingIndicator={
|
|
216
|
-
<div className="d-flex justify-content-center" style={{ height: 200 }}>
|
|
217
|
-
<LoadingIndicator />
|
|
218
|
-
</div>
|
|
219
|
-
}
|
|
220
|
-
endOfList={
|
|
221
|
-
<div className="d-flex justify-content-center" style={{ height: 200 }}>
|
|
222
|
-
{/* End of contacts */}
|
|
223
|
-
</div>
|
|
224
|
-
}
|
|
130
|
+
<input
|
|
131
|
+
type="search"
|
|
132
|
+
className="form-control ps-5"
|
|
133
|
+
value={searchText}
|
|
134
|
+
onChange={(event) => setSearchText(event.target.value)}
|
|
135
|
+
placeholder="Search people or shared groups"
|
|
136
|
+
aria-label="Search people"
|
|
225
137
|
/>
|
|
226
138
|
</div>
|
|
139
|
+
|
|
140
|
+
{error ? <div className="alert alert-danger">{error}</div> : null}
|
|
141
|
+
{!rows && !error ? <LoadingIndicator /> : null}
|
|
142
|
+
{rows && filteredRows.length === 0 ? (
|
|
143
|
+
<div className="border rounded p-4 text-center">
|
|
144
|
+
<i className="bi bi-people fs-2 text-muted" />
|
|
145
|
+
<h6 className="mt-2">{searchText ? "No matching people" : "No people yet"}</h6>
|
|
146
|
+
<p className="small text-muted mb-0">
|
|
147
|
+
{searchText
|
|
148
|
+
? "Try another name or group."
|
|
149
|
+
: "Invite someone or join a group to start building your network."}
|
|
150
|
+
</p>
|
|
151
|
+
</div>
|
|
152
|
+
) : null}
|
|
153
|
+
{filteredRows.length > 0 ? (
|
|
154
|
+
<div className="list-group">
|
|
155
|
+
{filteredRows.map((row) => (
|
|
156
|
+
<PersonRow
|
|
157
|
+
key={row.user.userId}
|
|
158
|
+
row={row}
|
|
159
|
+
duplicateName={
|
|
160
|
+
(duplicateNames.get(row.user.name.trim().toLocaleLowerCase()) ?? 0) > 1
|
|
161
|
+
}
|
|
162
|
+
/>
|
|
163
|
+
))}
|
|
164
|
+
</div>
|
|
165
|
+
) : null}
|
|
227
166
|
</div>
|
|
228
167
|
);
|
|
229
168
|
}
|
|
230
169
|
|
|
231
|
-
|
|
232
|
-
|
|
170
|
+
/** One person plus the relationship context shown in the directory. */
|
|
171
|
+
export interface IPersonSummary {
|
|
172
|
+
/** Signed user profile. */
|
|
173
|
+
user: IUser;
|
|
174
|
+
/** Whether the person is saved in the personal context. */
|
|
175
|
+
isContact: boolean;
|
|
176
|
+
/** Personal trust assignment when this is a contact. */
|
|
177
|
+
trustLevel?: TrustLevel;
|
|
178
|
+
/** Enabled groups both users belong to. */
|
|
179
|
+
sharedGroups: Array<{ groupId: string; name: string }>;
|
|
233
180
|
}
|
|
234
181
|
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
case "otherDataContexts":
|
|
243
|
-
return "Groups";
|
|
244
|
-
default:
|
|
245
|
-
return source;
|
|
182
|
+
async function loadPeople(userContext: UserContext): Promise<IPersonSummary[]> {
|
|
183
|
+
const personalUsers = await Users(userContext.userDataContext).list();
|
|
184
|
+
const personalById = new Map(personalUsers.map((user) => [user.userId, user]));
|
|
185
|
+
const summaries = new Map<string, IPersonSummary>();
|
|
186
|
+
for (const user of personalUsers) {
|
|
187
|
+
if (user.userId === userContext.userId) {
|
|
188
|
+
continue;
|
|
246
189
|
}
|
|
247
|
-
|
|
190
|
+
summaries.set(user.userId, {
|
|
191
|
+
user,
|
|
192
|
+
isContact: true,
|
|
193
|
+
trustLevel: await getUserTrustLevel(userContext.userDataContext, user.userId),
|
|
194
|
+
sharedGroups: [],
|
|
195
|
+
});
|
|
196
|
+
}
|
|
248
197
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
198
|
+
const groups = await Groups(userContext.userDataContext).list({ disabled: { $ne: true } });
|
|
199
|
+
for (const group of groups) {
|
|
200
|
+
const context = userContext.getDataContext(group.groupId);
|
|
201
|
+
const members = await GroupMembers(context).list({ deleted: { $ne: true } });
|
|
202
|
+
for (const member of members) {
|
|
203
|
+
if (member.userId === userContext.userId) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const existing = summaries.get(member.userId);
|
|
207
|
+
const user = personalById.get(member.userId) ?? (await Users(context).get(member.userId));
|
|
208
|
+
if (!user) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const summary = existing ?? {
|
|
212
|
+
user,
|
|
213
|
+
isContact: false,
|
|
214
|
+
sharedGroups: [],
|
|
215
|
+
};
|
|
216
|
+
if (!summary.sharedGroups.some((entry) => entry.groupId === group.groupId)) {
|
|
217
|
+
summary.sharedGroups.push({ groupId: group.groupId, name: group.name });
|
|
218
|
+
}
|
|
219
|
+
summaries.set(member.userId, summary);
|
|
259
220
|
}
|
|
260
|
-
}
|
|
221
|
+
}
|
|
261
222
|
|
|
262
|
-
return (
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
<div>
|
|
267
|
-
<strong>{contact.name}</strong>
|
|
268
|
-
<div className="d-flex align-items-center">
|
|
269
|
-
<small className="text-muted">{contact.userId}</small>
|
|
270
|
-
<span className="mx-1 text-muted">•</span>
|
|
271
|
-
<small className="text-info d-flex align-items-center">
|
|
272
|
-
<i className={`${getSourceIcon(contact.source)} me-1`} />
|
|
273
|
-
{getSourceLabel(contact.source)}
|
|
274
|
-
</small>
|
|
275
|
-
</div>
|
|
276
|
-
</div>
|
|
277
|
-
</div>
|
|
223
|
+
return [...summaries.values()].sort((left, right) =>
|
|
224
|
+
left.user.name.localeCompare(right.user.name),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
278
227
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
228
|
+
/** Compact, fully clickable row in the People directory. */
|
|
229
|
+
export function PersonRow(props: { row: IPersonSummary; duplicateName: boolean }) {
|
|
230
|
+
const { row } = props;
|
|
231
|
+
const groups = row.sharedGroups;
|
|
232
|
+
const relationship = row.isContact
|
|
233
|
+
? groups.length > 0
|
|
234
|
+
? `Contact · ${sharedGroupLabel(groups)}`
|
|
235
|
+
: "Contact"
|
|
236
|
+
: sharedGroupLabel(groups);
|
|
237
|
+
return (
|
|
238
|
+
<a
|
|
239
|
+
href={`#${IDENTITY_PEOPLE_PATH}/${row.user.userId}`}
|
|
240
|
+
className="list-group-item list-group-item-action d-flex align-items-center gap-3 py-3"
|
|
241
|
+
>
|
|
242
|
+
<span
|
|
243
|
+
className="rounded-circle bg-body-secondary d-inline-flex align-items-center justify-content-center flex-shrink-0 fw-semibold"
|
|
244
|
+
style={{ width: 42, height: 42 }}
|
|
245
|
+
aria-hidden="true"
|
|
246
|
+
>
|
|
247
|
+
{initials(row.user.name)}
|
|
248
|
+
</span>
|
|
249
|
+
<span className="flex-grow-1 overflow-hidden">
|
|
250
|
+
<span className="d-flex align-items-center gap-2 flex-wrap">
|
|
251
|
+
<span className="fw-semibold text-truncate">{row.user.name || "Unnamed person"}</span>
|
|
252
|
+
{props.duplicateName ? (
|
|
253
|
+
<code className="small text-muted">…{row.user.userId.slice(-6)}</code>
|
|
254
|
+
) : null}
|
|
255
|
+
</span>
|
|
256
|
+
<span className="small text-muted text-truncate d-block">{relationship}</span>
|
|
257
|
+
</span>
|
|
258
|
+
{row.isContact ? (
|
|
259
|
+
<TrustLevelBadge level={row.trustLevel ?? TrustLevel.Unknown} />
|
|
260
|
+
) : (
|
|
261
|
+
<span className="badge text-bg-light border">Shared group</span>
|
|
262
|
+
)}
|
|
263
|
+
<i className="bi bi-chevron-right text-muted" aria-hidden="true" />
|
|
264
|
+
</a>
|
|
287
265
|
);
|
|
288
|
-
}
|
|
266
|
+
}
|
|
289
267
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
268
|
+
function sharedGroupLabel(groups: Array<{ name: string }>): string {
|
|
269
|
+
if (groups.length === 0) {
|
|
270
|
+
return "Known through a shared group";
|
|
271
|
+
}
|
|
272
|
+
if (groups.length === 1) {
|
|
273
|
+
return `Shared group: ${groups[0].name}`;
|
|
274
|
+
}
|
|
275
|
+
return `${groups.length} shared groups`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function initials(name: string): string {
|
|
279
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
280
|
+
return (
|
|
281
|
+
parts
|
|
282
|
+
.slice(0, 2)
|
|
283
|
+
.map((part) => part[0]?.toLocaleUpperCase())
|
|
284
|
+
.join("") || "?"
|
|
285
|
+
);
|
|
286
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { DevicePairingState } from "@peers-app/peers-sdk";
|
|
2
2
|
import { fireEvent, render, waitFor } from "@testing-library/react";
|
|
3
3
|
import { DestinationDevicePairing, SourceDevicePairing } from "./device-pairing";
|
|
4
4
|
|
|
@@ -11,13 +11,9 @@ const mockRpc = {
|
|
|
11
11
|
};
|
|
12
12
|
let mockPairingState: DevicePairingState = { phase: "idle" };
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
mockRpc.startDestination as typeof rpcServerCalls.devicePairingStartDestination;
|
|
18
|
-
rpcServerCalls.devicePairingApprove = mockRpc.approve as typeof rpcServerCalls.devicePairingApprove;
|
|
19
|
-
rpcServerCalls.devicePairingReject = mockRpc.reject as typeof rpcServerCalls.devicePairingReject;
|
|
20
|
-
rpcServerCalls.devicePairingCancel = mockRpc.cancel as typeof rpcServerCalls.devicePairingCancel;
|
|
14
|
+
jest.mock("./identity-ipc", () => ({
|
|
15
|
+
identityTools: async () => mockRpc,
|
|
16
|
+
}));
|
|
21
17
|
|
|
22
18
|
jest.mock("./use-device-pairing-state", () => ({
|
|
23
19
|
useDevicePairingState: () => mockPairingState,
|
|
@@ -39,7 +35,9 @@ describe("device pairing UI lifecycle", () => {
|
|
|
39
35
|
);
|
|
40
36
|
const view = render(<DestinationDevicePairing onBack={jest.fn()} />);
|
|
41
37
|
|
|
42
|
-
|
|
38
|
+
await waitFor(() =>
|
|
39
|
+
expect(mockRpc.startDestination).toHaveBeenCalledWith({ deviceType: "browser" }),
|
|
40
|
+
);
|
|
43
41
|
view.unmount();
|
|
44
42
|
resolveStart({
|
|
45
43
|
sessionId: "00mh0wlipkdbeaw8tests0001",
|