@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,89 +1,117 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type GroupMemberRole,
|
|
3
|
+
GroupMembers,
|
|
2
4
|
Groups,
|
|
3
5
|
getMe,
|
|
6
|
+
getRoleLabel,
|
|
4
7
|
getUserContext,
|
|
5
|
-
type ICursorIterable,
|
|
6
8
|
type IGroup,
|
|
7
9
|
newid,
|
|
8
10
|
newKeys,
|
|
9
|
-
observable,
|
|
10
11
|
rpcServerCalls,
|
|
11
12
|
saveGroupSecretKey,
|
|
12
13
|
setUserTrustLevel,
|
|
13
14
|
TrustLevel,
|
|
15
|
+
type UserContext,
|
|
14
16
|
Users,
|
|
15
17
|
} from "@peers-app/peers-sdk";
|
|
16
|
-
import
|
|
17
|
-
import { useCallback, useEffect, useState } from "react";
|
|
18
|
-
import { Input } from "../../components/input";
|
|
19
|
-
import { LazyList } from "../../components/lazy-list";
|
|
18
|
+
import { useEffect, useMemo, useState } from "react";
|
|
20
19
|
import { LoadingIndicator } from "../../components/loading-indicator";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
20
|
+
import { mainContentPath } from "../../globals";
|
|
21
|
+
import { usePromise } from "../../hooks";
|
|
22
|
+
import { IDENTITY_GROUP_NEW_PATH, IDENTITY_GROUPS_PATH } from "../identity/identity-routes";
|
|
23
|
+
import { PendingWorkLink } from "../invites/activity-view";
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
/** Search, summarize, and create groups owned by the signed-in user. */
|
|
26
|
+
export function GroupList(props: { creating?: boolean } = {}) {
|
|
27
27
|
const userContext = usePromise(getUserContext);
|
|
28
|
-
const [
|
|
29
|
-
const [
|
|
30
|
-
const [
|
|
28
|
+
const [creating, setCreating] = useState(props.creating ?? false);
|
|
29
|
+
const [draftName, setDraftName] = useState("");
|
|
30
|
+
const [searchText, setSearchText] = useState("");
|
|
31
|
+
const [summaries, setSummaries] = useState<IGroupSummary[] | undefined>();
|
|
32
|
+
const [error, setError] = useState("");
|
|
33
|
+
const [creatingBusy, setCreatingBusy] = useState(false);
|
|
34
|
+
const [groupRevision, setGroupRevision] = useState(0);
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!userContext) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let disposed = false;
|
|
41
|
+
const subscriptions: Array<{ unsubscribe: () => void }> = [];
|
|
42
|
+
const refresh = async () => {
|
|
43
|
+
try {
|
|
44
|
+
const next = await loadGroupSummaries(userContext);
|
|
45
|
+
if (!disposed) {
|
|
46
|
+
setSummaries(next);
|
|
47
|
+
setError("");
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (!disposed) {
|
|
51
|
+
setError((err as Error).message || "Could not load groups.");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
41
54
|
};
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
const table = Groups(userContext.userDataContext);
|
|
56
|
+
subscriptions.push(table.dataChanged.subscribe(() => setGroupRevision((value) => value + 1)));
|
|
57
|
+
void table.list({ disabled: { $ne: true } }).then((groups) => {
|
|
58
|
+
if (disposed) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const group of groups) {
|
|
62
|
+
subscriptions.push(
|
|
63
|
+
GroupMembers(userContext.getDataContext(group.groupId)).dataChanged.subscribe(
|
|
64
|
+
() => void refresh(),
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
void refresh();
|
|
45
69
|
});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
70
|
+
return () => {
|
|
71
|
+
disposed = true;
|
|
72
|
+
for (const subscription of subscriptions) {
|
|
73
|
+
subscription.unsubscribe();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}, [groupRevision, userContext]);
|
|
49
77
|
|
|
50
78
|
useEffect(() => {
|
|
51
|
-
|
|
52
|
-
|
|
79
|
+
setCreating(props.creating ?? false);
|
|
80
|
+
}, [props.creating]);
|
|
81
|
+
|
|
82
|
+
const filtered = useMemo(() => {
|
|
83
|
+
const query = searchText.trim().toLocaleLowerCase();
|
|
84
|
+
if (!query) {
|
|
85
|
+
return summaries ?? [];
|
|
53
86
|
}
|
|
54
|
-
|
|
87
|
+
return (summaries ?? []).filter(
|
|
88
|
+
({ group }) =>
|
|
89
|
+
group.name.toLocaleLowerCase().includes(query) ||
|
|
90
|
+
group.description.toLocaleLowerCase().includes(query),
|
|
91
|
+
);
|
|
92
|
+
}, [searchText, summaries]);
|
|
55
93
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
94
|
+
function startCreate() {
|
|
95
|
+
setDraftName(searchText.trim());
|
|
96
|
+
mainContentPath(IDENTITY_GROUP_NEW_PATH);
|
|
59
97
|
}
|
|
60
98
|
|
|
61
|
-
async function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const cursor = cursorObs() || (await newCursor());
|
|
66
|
-
if (!cursor) return [];
|
|
67
|
-
|
|
68
|
-
for await (const nextRecord of cursor) {
|
|
69
|
-
if (existing.some((e) => e.groupId === nextRecord.groupId)) continue;
|
|
70
|
-
if (moreMatches.some((e) => e.groupId === nextRecord.groupId)) continue;
|
|
71
|
-
moreMatches.push(nextRecord);
|
|
72
|
-
if (searchText.length && moreMatches.length > 5) break;
|
|
73
|
-
if (moreMatches.length >= 10) break;
|
|
99
|
+
async function confirmCreate() {
|
|
100
|
+
const name = draftName.trim();
|
|
101
|
+
if (!name) {
|
|
102
|
+
return;
|
|
74
103
|
}
|
|
75
|
-
|
|
76
|
-
|
|
104
|
+
setCreatingBusy(true);
|
|
105
|
+
setError("");
|
|
106
|
+
try {
|
|
107
|
+
setDraftName("");
|
|
108
|
+
setSearchText("");
|
|
109
|
+
await newRecord(name);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
setError((err as Error).message || "Could not create the group.");
|
|
112
|
+
} finally {
|
|
113
|
+
setCreatingBusy(false);
|
|
77
114
|
}
|
|
78
|
-
return moreMatches;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function searchSubmit(evt: React.KeyboardEvent<HTMLInputElement>) {
|
|
82
|
-
if (evt.key !== "Enter") return;
|
|
83
|
-
const name = searchText.trim();
|
|
84
|
-
if (!name) return;
|
|
85
|
-
searchTextObs("");
|
|
86
|
-
await newRecord(name);
|
|
87
115
|
}
|
|
88
116
|
|
|
89
117
|
async function newRecord(name: string) {
|
|
@@ -127,250 +155,161 @@ export function GroupList() {
|
|
|
127
155
|
console.error("Error auto-installing peers-core for group:", err);
|
|
128
156
|
});
|
|
129
157
|
|
|
130
|
-
mainContentPath(
|
|
158
|
+
mainContentPath(`${IDENTITY_GROUPS_PATH}/${group.groupId}`);
|
|
131
159
|
return group;
|
|
132
160
|
}
|
|
133
161
|
|
|
134
|
-
function renderItem(group: IGroup) {
|
|
135
|
-
const groupIcon = group.iconClassName || "bi-people-fill";
|
|
136
|
-
return (
|
|
137
|
-
<div key={group.groupId} className="container-fluid pb-4">
|
|
138
|
-
<i className={groupIcon}></i>
|
|
139
|
-
<a href={`#groups/${group.groupId}`}>{group.name || "<unnamed-group>"}</a>
|
|
140
|
-
<div style={{ paddingLeft: "20px" }}>
|
|
141
|
-
<MarkdownWithMentions content={group.description || ""} />
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
162
|
return (
|
|
148
|
-
<div className="container-
|
|
149
|
-
<div className="
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
onKeyUp={(evt) => searchSubmit(evt)}
|
|
156
|
-
/>
|
|
157
|
-
<ImportGroupButton />
|
|
163
|
+
<div className="container-xxl p-3">
|
|
164
|
+
<div className="mb-3">
|
|
165
|
+
<h4 className="mb-1">
|
|
166
|
+
<i className="bi-people-fill me-2" />
|
|
167
|
+
Groups
|
|
168
|
+
</h4>
|
|
169
|
+
<div className="small text-muted">Shared spaces, membership, and access</div>
|
|
158
170
|
</div>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
171
|
+
{creating ? (
|
|
172
|
+
<div className="card mb-3">
|
|
173
|
+
<div className="card-body">
|
|
174
|
+
<h6 className="card-title">Create a group</h6>
|
|
175
|
+
<form
|
|
176
|
+
className="input-group"
|
|
177
|
+
onSubmit={(event) => {
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
void confirmCreate();
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
<input
|
|
183
|
+
id="new-group-name"
|
|
184
|
+
className="form-control"
|
|
185
|
+
value={draftName}
|
|
186
|
+
onChange={(event) => setDraftName(event.target.value)}
|
|
187
|
+
placeholder="Group name"
|
|
188
|
+
aria-label="Group name"
|
|
189
|
+
/>
|
|
190
|
+
<button
|
|
191
|
+
type="submit"
|
|
192
|
+
className="btn btn-primary"
|
|
193
|
+
disabled={!draftName.trim() || creatingBusy}
|
|
194
|
+
>
|
|
195
|
+
{creatingBusy ? "Creating…" : "Create"}
|
|
196
|
+
</button>
|
|
197
|
+
<a href={`#${IDENTITY_GROUPS_PATH}`} className="btn btn-outline-secondary">
|
|
198
|
+
Cancel
|
|
199
|
+
</a>
|
|
200
|
+
</form>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
) : null}
|
|
204
|
+
<PendingWorkLink type="group" />
|
|
205
|
+
<div className="position-relative my-3">
|
|
206
|
+
<i
|
|
207
|
+
className="bi bi-search position-absolute text-muted"
|
|
208
|
+
style={{ left: 12, top: "50%", transform: "translateY(-50%)" }}
|
|
209
|
+
/>
|
|
210
|
+
<input
|
|
211
|
+
type="search"
|
|
212
|
+
className="form-control ps-5"
|
|
213
|
+
value={searchText}
|
|
214
|
+
onChange={(event) => setSearchText(event.target.value)}
|
|
215
|
+
placeholder="Search groups"
|
|
216
|
+
aria-label="Search groups"
|
|
178
217
|
/>
|
|
179
218
|
</div>
|
|
219
|
+
|
|
220
|
+
{error ? <div className="alert alert-danger">{error}</div> : null}
|
|
221
|
+
{!summaries && !error ? <LoadingIndicator /> : null}
|
|
222
|
+
{summaries && filtered.length === 0 ? (
|
|
223
|
+
<div className="border rounded p-4 text-center">
|
|
224
|
+
<i className="bi bi-people fs-2 text-muted" />
|
|
225
|
+
<h6 className="mt-2">{searchText ? "No matching groups" : "No groups yet"}</h6>
|
|
226
|
+
<p className="small text-muted">
|
|
227
|
+
{searchText
|
|
228
|
+
? "Try a different name or description."
|
|
229
|
+
: "Create a shared space or use an invite to join one."}
|
|
230
|
+
</p>
|
|
231
|
+
{!searchText ? (
|
|
232
|
+
<button type="button" className="btn btn-primary btn-sm" onClick={startCreate}>
|
|
233
|
+
Create your first group
|
|
234
|
+
</button>
|
|
235
|
+
) : null}
|
|
236
|
+
</div>
|
|
237
|
+
) : null}
|
|
238
|
+
{filtered.length > 0 ? (
|
|
239
|
+
<div className="list-group">
|
|
240
|
+
{filtered.map((summary) => (
|
|
241
|
+
<GroupSummaryRow key={summary.group.groupId} summary={summary} />
|
|
242
|
+
))}
|
|
243
|
+
</div>
|
|
244
|
+
) : null}
|
|
180
245
|
</div>
|
|
181
246
|
);
|
|
182
247
|
}
|
|
183
248
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const handleImport = async () => {
|
|
195
|
-
if (!importText.trim()) {
|
|
196
|
-
setImportResult({
|
|
197
|
-
success: false,
|
|
198
|
-
message: "Please paste the group share JSON data",
|
|
199
|
-
});
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
setIsImporting(true);
|
|
204
|
-
setImportResult(null);
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
const groupId = await rpcServerCalls.importGroupShare(importText.trim());
|
|
208
|
-
setImportResult({
|
|
209
|
-
success: true,
|
|
210
|
-
message: `Group imported successfully!`,
|
|
211
|
-
groupId,
|
|
212
|
-
});
|
|
213
|
-
setImportText("");
|
|
249
|
+
/** Data shown for one group in the Identity list. */
|
|
250
|
+
export interface IGroupSummary {
|
|
251
|
+
/** Signed group record. */
|
|
252
|
+
group: IGroup;
|
|
253
|
+
/** Number of active memberships. */
|
|
254
|
+
memberCount: number;
|
|
255
|
+
/** Signed-in user's role in this group. */
|
|
256
|
+
myRole?: GroupMemberRole;
|
|
257
|
+
}
|
|
214
258
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
} catch (error: unknown) {
|
|
222
|
-
setImportResult({
|
|
223
|
-
success: false,
|
|
224
|
-
message: `Import failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
259
|
+
async function loadGroupSummaries(userContext: UserContext): Promise<IGroupSummary[]> {
|
|
260
|
+
const groups = await Groups(userContext.userDataContext).list({ disabled: { $ne: true } });
|
|
261
|
+
const summaries = await Promise.all(
|
|
262
|
+
groups.map(async (group) => {
|
|
263
|
+
const members = await GroupMembers(userContext.getDataContext(group.groupId)).list({
|
|
264
|
+
deleted: { $ne: true },
|
|
225
265
|
});
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
setImportText(clipboardText);
|
|
236
|
-
}
|
|
237
|
-
} catch (_error) {
|
|
238
|
-
console.log("Failed to read from clipboard - user can paste manually");
|
|
239
|
-
}
|
|
240
|
-
};
|
|
266
|
+
return {
|
|
267
|
+
group,
|
|
268
|
+
memberCount: members.length,
|
|
269
|
+
myRole: members.find((member) => member.userId === userContext.userId)?.role,
|
|
270
|
+
};
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
return summaries.sort((left, right) => left.group.name.localeCompare(right.group.name));
|
|
274
|
+
}
|
|
241
275
|
|
|
276
|
+
/** Compact, fully clickable group summary row. */
|
|
277
|
+
export function GroupSummaryRow({ summary }: { summary: IGroupSummary }) {
|
|
278
|
+
const { group } = summary;
|
|
242
279
|
return (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
280
|
+
<a
|
|
281
|
+
href={`#${IDENTITY_GROUPS_PATH}/${group.groupId}`}
|
|
282
|
+
className="list-group-item list-group-item-action d-flex align-items-center gap-3 py-3"
|
|
283
|
+
>
|
|
284
|
+
<span
|
|
285
|
+
className="rounded-3 bg-body-secondary d-inline-flex align-items-center justify-content-center flex-shrink-0"
|
|
286
|
+
style={{ width: 44, height: 44 }}
|
|
287
|
+
aria-hidden="true"
|
|
249
288
|
>
|
|
250
|
-
<i className="bi-
|
|
251
|
-
</
|
|
252
|
-
<
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
className="
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
<h6 className="mb-0">
|
|
269
|
-
<i className="bi-download me-2"></i>
|
|
270
|
-
Import Shared Group
|
|
271
|
-
</h6>
|
|
272
|
-
<button
|
|
273
|
-
className="btn-close"
|
|
274
|
-
onClick={() => {
|
|
275
|
-
setShowImportDialog(false);
|
|
276
|
-
setImportText("");
|
|
277
|
-
setImportResult(null);
|
|
278
|
-
}}
|
|
279
|
-
aria-label="Close"
|
|
280
|
-
></button>
|
|
281
|
-
</div>
|
|
282
|
-
<div className="card-body">
|
|
283
|
-
<p className="mb-3">
|
|
284
|
-
Paste the group share JSON data below to import a shared group with its admin
|
|
285
|
-
members.
|
|
286
|
-
</p>
|
|
287
|
-
|
|
288
|
-
<div className="mb-3">
|
|
289
|
-
<label className="form-label">Group Share JSON:</label>
|
|
290
|
-
<div className="d-flex gap-2 mb-2">
|
|
291
|
-
<button
|
|
292
|
-
className="btn btn-secondary btn-sm"
|
|
293
|
-
onClick={handlePasteFromClipboard}
|
|
294
|
-
title="Paste from clipboard"
|
|
295
|
-
>
|
|
296
|
-
<i className="bi-clipboard me-1"></i>
|
|
297
|
-
Paste from Clipboard
|
|
298
|
-
</button>
|
|
299
|
-
</div>
|
|
300
|
-
<textarea
|
|
301
|
-
className="form-control font-monospace"
|
|
302
|
-
rows={8}
|
|
303
|
-
value={importText}
|
|
304
|
-
onChange={(e) => setImportText(e.target.value)}
|
|
305
|
-
placeholder="Paste the group share JSON here..."
|
|
306
|
-
style={{ fontSize: "0.9em" }}
|
|
307
|
-
/>
|
|
308
|
-
</div>
|
|
309
|
-
|
|
310
|
-
{importResult && (
|
|
311
|
-
<div className={`alert alert-${importResult.success ? "success" : "danger"} mb-3`}>
|
|
312
|
-
<i
|
|
313
|
-
className={`bi-${importResult.success ? "check-circle" : "exclamation-triangle"} me-2`}
|
|
314
|
-
></i>
|
|
315
|
-
{importResult.message}
|
|
316
|
-
{importResult.success && importResult.groupId && (
|
|
317
|
-
<div className="mt-2">
|
|
318
|
-
<small>Redirecting to group in 2 seconds...</small>
|
|
319
|
-
</div>
|
|
320
|
-
)}
|
|
321
|
-
</div>
|
|
322
|
-
)}
|
|
323
|
-
|
|
324
|
-
<div className="text-end">
|
|
325
|
-
<button
|
|
326
|
-
className="btn btn-secondary me-2"
|
|
327
|
-
onClick={() => {
|
|
328
|
-
setShowImportDialog(false);
|
|
329
|
-
setImportText("");
|
|
330
|
-
setImportResult(null);
|
|
331
|
-
}}
|
|
332
|
-
disabled={isImporting}
|
|
333
|
-
>
|
|
334
|
-
Cancel
|
|
335
|
-
</button>
|
|
336
|
-
<button
|
|
337
|
-
className="btn btn-primary"
|
|
338
|
-
onClick={handleImport}
|
|
339
|
-
disabled={isImporting || !importText.trim()}
|
|
340
|
-
>
|
|
341
|
-
{isImporting ? (
|
|
342
|
-
<>
|
|
343
|
-
<span
|
|
344
|
-
className="spinner-border spinner-border-sm me-1"
|
|
345
|
-
role="status"
|
|
346
|
-
aria-hidden="true"
|
|
347
|
-
></span>
|
|
348
|
-
Importing...
|
|
349
|
-
</>
|
|
350
|
-
) : (
|
|
351
|
-
<>
|
|
352
|
-
<i className="bi-download me-1"></i>
|
|
353
|
-
Import Group
|
|
354
|
-
</>
|
|
355
|
-
)}
|
|
356
|
-
</button>
|
|
357
|
-
</div>
|
|
358
|
-
</div>
|
|
359
|
-
</div>
|
|
360
|
-
</div>
|
|
361
|
-
)}
|
|
362
|
-
</>
|
|
289
|
+
<i className={group.iconClassName || "bi bi-people-fill"} />
|
|
290
|
+
</span>
|
|
291
|
+
<span className="flex-grow-1 overflow-hidden">
|
|
292
|
+
<span className="fw-semibold d-block text-truncate">{group.name || "Unnamed group"}</span>
|
|
293
|
+
<span className="small text-muted d-block text-truncate">
|
|
294
|
+
{plainDescription(group.description) || "No description yet"}
|
|
295
|
+
</span>
|
|
296
|
+
</span>
|
|
297
|
+
<span className="d-flex align-items-center gap-2 flex-wrap justify-content-end">
|
|
298
|
+
<span className="badge text-bg-light border">
|
|
299
|
+
{summary.memberCount} member{summary.memberCount === 1 ? "" : "s"}
|
|
300
|
+
</span>
|
|
301
|
+
{summary.myRole !== undefined ? (
|
|
302
|
+
<span className="badge text-bg-secondary">{getRoleLabel(summary.myRole)}</span>
|
|
303
|
+
) : null}
|
|
304
|
+
</span>
|
|
305
|
+
<i className="bi bi-chevron-right text-muted" aria-hidden="true" />
|
|
306
|
+
</a>
|
|
363
307
|
);
|
|
364
|
-
}
|
|
308
|
+
}
|
|
365
309
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
uiCategory: "screen",
|
|
373
|
-
priority: 2,
|
|
374
|
-
},
|
|
375
|
-
],
|
|
376
|
-
});
|
|
310
|
+
function plainDescription(markdown: string): string {
|
|
311
|
+
return markdown
|
|
312
|
+
.replace(/[#_*`>[\]()!-]/g, " ")
|
|
313
|
+
.replace(/\s+/g, " ")
|
|
314
|
+
.trim();
|
|
315
|
+
}
|
|
@@ -24,6 +24,7 @@ interface GroupMembersUIProps {
|
|
|
24
24
|
userContext: UserContext;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** List group memberships and expose role-appropriate invite and member controls. */
|
|
27
28
|
export const GroupMembersUI = (props: GroupMembersUIProps) => {
|
|
28
29
|
const { groupId, userContext } = props;
|
|
29
30
|
|
|
@@ -202,7 +203,7 @@ export const GroupMembersUI = (props: GroupMembersUIProps) => {
|
|
|
202
203
|
{/* Founder info - only show if current user is NOT the founder */}
|
|
203
204
|
{founderData && founderData.founderUserId !== currentUserId && (
|
|
204
205
|
<a
|
|
205
|
-
href={`#
|
|
206
|
+
href={`#identity/people/${founderData.founderUserId}`}
|
|
206
207
|
className="alert alert-warning py-2 px-3 mb-0 d-flex align-items-center text-decoration-none"
|
|
207
208
|
style={{ cursor: "pointer" }}
|
|
208
209
|
>
|
|
@@ -273,7 +274,7 @@ export const GroupMembersUI = (props: GroupMembersUIProps) => {
|
|
|
273
274
|
|
|
274
275
|
{/* View button - Always visible */}
|
|
275
276
|
<a
|
|
276
|
-
href={`#
|
|
277
|
+
href={`#identity/people/${member.userId}`}
|
|
277
278
|
className="btn btn-outline-primary btn-sm me-2"
|
|
278
279
|
>
|
|
279
280
|
<i className="bi-eye me-1" />
|