@peers-app/peers-ui 0.27.2 → 0.27.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.
Files changed (309) hide show
  1. package/dist/screens/network-viewer/network-viewer-ipc.d.ts +3 -1
  2. package/dist/screens/network-viewer/network-viewer-ipc.js +6 -0
  3. package/dist/screens/network-viewer/sync-recovery.js +103 -1
  4. package/dist/screens/variables/variable-details.js +2 -1
  5. package/dist/tabs-layout/network-status-indicator.js +26 -12
  6. package/dist/tabs-layout/use-network-status.d.ts +6 -1
  7. package/dist/tabs-layout/use-network-status.js +27 -4
  8. package/package.json +9 -3
  9. package/.claude/settings.local.json +0 -34
  10. package/.github/README.md +0 -52
  11. package/.github/workflows/delete-all-artifacts.yml +0 -49
  12. package/.github/workflows/publish.yml +0 -51
  13. package/babel.config.js +0 -7
  14. package/biome.json +0 -198
  15. package/dist/app-updates/app-updates-consumer.test.d.ts +0 -1
  16. package/dist/app-updates/app-updates-consumer.test.js +0 -56
  17. package/dist/app-updates/app-updates-settings.test.d.ts +0 -1
  18. package/dist/app-updates/app-updates-settings.test.js +0 -93
  19. package/dist/app-updates/remote-app-update-panel.test.d.ts +0 -1
  20. package/dist/app-updates/remote-app-update-panel.test.js +0 -165
  21. package/dist/components/access-level-field.test.d.ts +0 -1
  22. package/dist/components/access-level-field.test.js +0 -28
  23. package/dist/components/message-logs/tool-access-approval-card.test.d.ts +0 -1
  24. package/dist/components/message-logs/tool-access-approval-card.test.js +0 -102
  25. package/dist/components/responsive-section-nav.test.d.ts +0 -1
  26. package/dist/components/responsive-section-nav.test.js +0 -45
  27. package/dist/globals-hash.test.d.ts +0 -1
  28. package/dist/globals-hash.test.js +0 -16
  29. package/dist/hooks/use-observable-state.test.d.ts +0 -1
  30. package/dist/hooks/use-observable-state.test.js +0 -64
  31. package/dist/hooks/use-observable.test.d.ts +0 -1
  32. package/dist/hooks/use-observable.test.js +0 -191
  33. package/dist/hooks/use-rows.test.d.ts +0 -1
  34. package/dist/hooks/use-rows.test.js +0 -175
  35. package/dist/hooks/use-tool.test.d.ts +0 -1
  36. package/dist/hooks/use-tool.test.js +0 -190
  37. package/dist/host-shell/host-shell-consumer.test.d.ts +0 -1
  38. package/dist/host-shell/host-shell-consumer.test.js +0 -28
  39. package/dist/root-layout/root-layout-host.test.d.ts +0 -1
  40. package/dist/root-layout/root-layout-host.test.js +0 -86
  41. package/dist/screens/account/account-screen.test.d.ts +0 -1
  42. package/dist/screens/account/account-screen.test.js +0 -51
  43. package/dist/screens/assistants/assistant-config.test.d.ts +0 -1
  44. package/dist/screens/assistants/assistant-config.test.js +0 -57
  45. package/dist/screens/contacts/contact-list.test.d.ts +0 -1
  46. package/dist/screens/contacts/contact-list.test.js +0 -48
  47. package/dist/screens/device-pairing/device-pairing.test.d.ts +0 -1
  48. package/dist/screens/device-pairing/device-pairing.test.js +0 -119
  49. package/dist/screens/devices/devices-list.test.d.ts +0 -1
  50. package/dist/screens/devices/devices-list.test.js +0 -53
  51. package/dist/screens/groups/group-list.test.d.ts +0 -1
  52. package/dist/screens/groups/group-list.test.js +0 -38
  53. package/dist/screens/identity/identity-routes.test.d.ts +0 -1
  54. package/dist/screens/identity/identity-routes.test.js +0 -51
  55. package/dist/screens/identity/identity-screen.test.d.ts +0 -1
  56. package/dist/screens/identity/identity-screen.test.js +0 -103
  57. package/dist/screens/invites/accept-invite.test.d.ts +0 -1
  58. package/dist/screens/invites/accept-invite.test.js +0 -103
  59. package/dist/screens/invites/activity-view.test.d.ts +0 -1
  60. package/dist/screens/invites/activity-view.test.js +0 -108
  61. package/dist/screens/invites/invite-classify.test.d.ts +0 -1
  62. package/dist/screens/invites/invite-classify.test.js +0 -115
  63. package/dist/screens/network-viewer/network-viewer-ipc.test.d.ts +0 -1
  64. package/dist/screens/network-viewer/network-viewer-ipc.test.js +0 -61
  65. package/dist/screens/network-viewer/network-viewer.section.test.d.ts +0 -1
  66. package/dist/screens/network-viewer/network-viewer.section.test.js +0 -63
  67. package/dist/screens/network-viewer/sync-recovery.test.d.ts +0 -1
  68. package/dist/screens/network-viewer/sync-recovery.test.js +0 -104
  69. package/dist/screens/packages/package-helpers.test.d.ts +0 -1
  70. package/dist/screens/packages/package-helpers.test.js +0 -53
  71. package/dist/screens/packages/package-runtime-status.test.d.ts +0 -1
  72. package/dist/screens/packages/package-runtime-status.test.js +0 -50
  73. package/dist/screens/packages/packages-ipc.test.d.ts +0 -1
  74. package/dist/screens/packages/packages-ipc.test.js +0 -43
  75. package/dist/screens/settings/settings-page.test.d.ts +0 -1
  76. package/dist/screens/settings/settings-page.test.js +0 -19
  77. package/dist/screens/tools/tool-access-ipc.test.d.ts +0 -1
  78. package/dist/screens/tools/tool-access-ipc.test.js +0 -40
  79. package/dist/screens/tools/tool-catalog-ipc.test.d.ts +0 -1
  80. package/dist/screens/tools/tool-catalog-ipc.test.js +0 -52
  81. package/dist/tabs-layout/network-status-indicator.test.d.ts +0 -1
  82. package/dist/tabs-layout/network-status-indicator.test.js +0 -665
  83. package/dist/tabs-layout/tabs-state.test.d.ts +0 -1
  84. package/dist/tabs-layout/tabs-state.test.js +0 -110
  85. package/dist/tabs-layout/use-network-status.test.d.ts +0 -1
  86. package/dist/tabs-layout/use-network-status.test.js +0 -45
  87. package/dist/ui-router/ui-loader.test.d.ts +0 -1
  88. package/dist/ui-router/ui-loader.test.js +0 -91
  89. package/jest.config.js +0 -25
  90. package/release-guard.js +0 -10
  91. package/src/app-updates/app-update-controls.tsx +0 -73
  92. package/src/app-updates/app-updates-consumer.test.ts +0 -83
  93. package/src/app-updates/app-updates-consumer.ts +0 -121
  94. package/src/app-updates/app-updates-settings.test.tsx +0 -109
  95. package/src/app-updates/app-updates-settings.tsx +0 -136
  96. package/src/app-updates/remote-app-update-panel.test.tsx +0 -205
  97. package/src/app-updates/remote-app-update-panel.tsx +0 -210
  98. package/src/app-updates/use-app-update-status.ts +0 -16
  99. package/src/command-palette/command-palette-ui.tsx +0 -564
  100. package/src/command-palette/command-palette.ts +0 -242
  101. package/src/components/access-level-field.test.tsx +0 -32
  102. package/src/components/access-level-field.tsx +0 -68
  103. package/src/components/checkbox.tsx +0 -25
  104. package/src/components/group-switcher.tsx +0 -526
  105. package/src/components/input-date.tsx +0 -29
  106. package/src/components/input-number.tsx +0 -83
  107. package/src/components/input.tsx +0 -25
  108. package/src/components/inverse-lazy-list.tsx +0 -107
  109. package/src/components/io-schema-values.tsx +0 -103
  110. package/src/components/io-schema.tsx +0 -259
  111. package/src/components/lazy-list.tsx +0 -115
  112. package/src/components/list-screen.tsx +0 -126
  113. package/src/components/loading-indicator.tsx +0 -7
  114. package/src/components/markdown-editor/autolink-plugin.tsx +0 -29
  115. package/src/components/markdown-editor/editor-inline.tsx +0 -9
  116. package/src/components/markdown-editor/editor.tsx +0 -158
  117. package/src/components/markdown-editor/markdown-plugin.tsx +0 -202
  118. package/src/components/markdown-editor/mention-node.ts +0 -204
  119. package/src/components/markdown-editor/mentions-plugin.tsx +0 -354
  120. package/src/components/markdown-editor/move-line-plugin.tsx +0 -186
  121. package/src/components/markdown-editor/select-line-boundary-plugin.tsx +0 -95
  122. package/src/components/markdown-editor/theme.ts +0 -46
  123. package/src/components/markdown-editor/toolbar.tsx +0 -269
  124. package/src/components/markdown-with-mentions.tsx +0 -198
  125. package/src/components/message-logs/message-logs.tsx +0 -523
  126. package/src/components/message-logs/tool-access-approval-card.test.tsx +0 -127
  127. package/src/components/message-logs/tool-access-approval-card.tsx +0 -178
  128. package/src/components/messages/avatar.tsx +0 -171
  129. package/src/components/messages/channel-message-list.tsx +0 -189
  130. package/src/components/messages/channel-view.tsx +0 -92
  131. package/src/components/messages/message-compose.tsx +0 -192
  132. package/src/components/messages/message-display.tsx +0 -231
  133. package/src/components/messages/thread-message-list.tsx +0 -135
  134. package/src/components/qr-scanner.tsx +0 -180
  135. package/src/components/responsive-section-nav.test.tsx +0 -88
  136. package/src/components/responsive-section-nav.tsx +0 -92
  137. package/src/components/router.tsx +0 -175
  138. package/src/components/save-button.tsx +0 -134
  139. package/src/components/sortable-list.tsx +0 -130
  140. package/src/components/tabs.tsx +0 -76
  141. package/src/components/tooltip.tsx +0 -43
  142. package/src/components/trust-level-badge.tsx +0 -44
  143. package/src/components/trust-level-dropdown.tsx +0 -136
  144. package/src/components/typeahead.tsx +0 -357
  145. package/src/globals-hash.test.ts +0 -19
  146. package/src/globals.tsx +0 -226
  147. package/src/hooks/index.ts +0 -8
  148. package/src/hooks/use-host-contract.ts +0 -75
  149. package/src/hooks/use-observable-state.test.tsx +0 -101
  150. package/src/hooks/use-observable-state.ts +0 -29
  151. package/src/hooks/use-observable.test.tsx +0 -258
  152. package/src/hooks/use-observable.ts +0 -67
  153. package/src/hooks/use-on-screen.ts +0 -24
  154. package/src/hooks/use-promise.ts +0 -34
  155. package/src/hooks/use-rows.test.tsx +0 -196
  156. package/src/hooks/use-rows.ts +0 -120
  157. package/src/hooks/use-subscription.ts +0 -24
  158. package/src/hooks/use-tool.test.tsx +0 -225
  159. package/src/hooks/use-tool.ts +0 -116
  160. package/src/host-shell/host-shell-consumer.test.ts +0 -32
  161. package/src/host-shell/host-shell-consumer.ts +0 -53
  162. package/src/index.tsx +0 -42
  163. package/src/mention-configs.ts +0 -185
  164. package/src/operator-console/operator-console-state.ts +0 -119
  165. package/src/operator-console/operator-console.tsx +0 -233
  166. package/src/peers-services-registration.ts +0 -6
  167. package/src/root-layout/global-overlays.ts +0 -50
  168. package/src/root-layout/layout-picker.tsx +0 -47
  169. package/src/root-layout/layouts/full-screen-layout.tsx +0 -144
  170. package/src/root-layout/layouts/tabs-with-console-layout.tsx +0 -260
  171. package/src/root-layout/root-layout-host.test.tsx +0 -108
  172. package/src/root-layout/root-layout-host.tsx +0 -147
  173. package/src/root-layout/root-layout-registry.ts +0 -51
  174. package/src/root-layout/root-layout-state.ts +0 -55
  175. package/src/root-layout/root-layout.ts +0 -32
  176. package/src/screens/account/account-screen.test.tsx +0 -65
  177. package/src/screens/account/account-screen.tsx +0 -942
  178. package/src/screens/assistants/assistant-config.test.tsx +0 -54
  179. package/src/screens/assistants/assistant-config.tsx +0 -121
  180. package/src/screens/assistants/assistant-details.tsx +0 -89
  181. package/src/screens/assistants/assistant-info.tsx +0 -66
  182. package/src/screens/assistants/assistant-list.tsx +0 -145
  183. package/src/screens/assistants/assistant-tools.tsx +0 -82
  184. package/src/screens/console-logs/console-logs-list.tsx +0 -374
  185. package/src/screens/console-logs/log-display.tsx +0 -137
  186. package/src/screens/console-logs/log-filters.tsx +0 -64
  187. package/src/screens/console-logs/mobile-log-card.tsx +0 -161
  188. package/src/screens/console-logs/resizable-table-header.tsx +0 -110
  189. package/src/screens/contacts/contact-details.tsx +0 -123
  190. package/src/screens/contacts/contact-list.test.tsx +0 -59
  191. package/src/screens/contacts/contact-list.tsx +0 -286
  192. package/src/screens/contacts/index.ts +0 -2
  193. package/src/screens/data-explorer/data-explorer.tsx +0 -633
  194. package/src/screens/data-explorer/index.ts +0 -6
  195. package/src/screens/data-explorer/query-executor.tsx +0 -520
  196. package/src/screens/device-pairing/device-pairing.test.tsx +0 -143
  197. package/src/screens/device-pairing/device-pairing.tsx +0 -461
  198. package/src/screens/device-pairing/identity-ipc.ts +0 -25
  199. package/src/screens/device-pairing/index.ts +0 -2
  200. package/src/screens/device-pairing/use-device-pairing-state.ts +0 -15
  201. package/src/screens/devices/devices-list.test.tsx +0 -68
  202. package/src/screens/devices/devices-list.tsx +0 -365
  203. package/src/screens/devices/index.ts +0 -1
  204. package/src/screens/groups/group-details.tsx +0 -160
  205. package/src/screens/groups/group-invite-panel.tsx +0 -311
  206. package/src/screens/groups/group-list.test.tsx +0 -44
  207. package/src/screens/groups/group-list.tsx +0 -317
  208. package/src/screens/groups/group-members.tsx +0 -343
  209. package/src/screens/groups/index.ts +0 -3
  210. package/src/screens/identity/identity-routes.test.ts +0 -70
  211. package/src/screens/identity/identity-routes.ts +0 -301
  212. package/src/screens/identity/identity-screen.test.tsx +0 -138
  213. package/src/screens/identity/identity-screen.tsx +0 -282
  214. package/src/screens/identity/index.ts +0 -2
  215. package/src/screens/identity/invite-contact-panel.tsx +0 -45
  216. package/src/screens/identity/profile-invite.tsx +0 -93
  217. package/src/screens/invites/accept-invite.test.tsx +0 -115
  218. package/src/screens/invites/accept-invite.tsx +0 -318
  219. package/src/screens/invites/activity-view.test.tsx +0 -127
  220. package/src/screens/invites/activity-view.tsx +0 -162
  221. package/src/screens/invites/index.ts +0 -6
  222. package/src/screens/invites/invite-capture.ts +0 -104
  223. package/src/screens/invites/invite-classify.test.ts +0 -129
  224. package/src/screens/invites/invite-share.tsx +0 -104
  225. package/src/screens/invites/invites-ipc.ts +0 -25
  226. package/src/screens/invites/pending-invites-list.tsx +0 -256
  227. package/src/screens/network-viewer/connection-troubleshooter.tsx +0 -395
  228. package/src/screens/network-viewer/cpu-usage-graph.tsx +0 -294
  229. package/src/screens/network-viewer/device-details-modal.tsx +0 -339
  230. package/src/screens/network-viewer/group-details-modal.tsx +0 -421
  231. package/src/screens/network-viewer/index.ts +0 -8
  232. package/src/screens/network-viewer/network-viewer-ipc.test.ts +0 -72
  233. package/src/screens/network-viewer/network-viewer-ipc.ts +0 -39
  234. package/src/screens/network-viewer/network-viewer-section.ts +0 -12
  235. package/src/screens/network-viewer/network-viewer.section.test.tsx +0 -68
  236. package/src/screens/network-viewer/network-viewer.tsx +0 -708
  237. package/src/screens/network-viewer/remote-logs-panel.tsx +0 -190
  238. package/src/screens/network-viewer/sync-recovery.test.tsx +0 -114
  239. package/src/screens/network-viewer/sync-recovery.tsx +0 -461
  240. package/src/screens/network-viewer/usage-graph.tsx +0 -455
  241. package/src/screens/packages/local-folder-grants-panel.tsx +0 -290
  242. package/src/screens/packages/local-folders-ipc.ts +0 -28
  243. package/src/screens/packages/package-details.tsx +0 -166
  244. package/src/screens/packages/package-helpers.test.ts +0 -57
  245. package/src/screens/packages/package-helpers.ts +0 -137
  246. package/src/screens/packages/package-info.tsx +0 -354
  247. package/src/screens/packages/package-list.tsx +0 -519
  248. package/src/screens/packages/package-network-panel.tsx +0 -82
  249. package/src/screens/packages/package-new-local.tsx +0 -100
  250. package/src/screens/packages/package-runtime-status.test.tsx +0 -58
  251. package/src/screens/packages/package-runtime-status.tsx +0 -68
  252. package/src/screens/packages/package-versions.tsx +0 -298
  253. package/src/screens/packages/packages-ipc.test.ts +0 -47
  254. package/src/screens/packages/packages-ipc.ts +0 -125
  255. package/src/screens/peer-types/peer-type-details.tsx +0 -172
  256. package/src/screens/peer-types/peer-type-list.tsx +0 -64
  257. package/src/screens/search/global-search.tsx +0 -406
  258. package/src/screens/settings/color-mode-dropdown.tsx +0 -82
  259. package/src/screens/settings/settings-page.test.tsx +0 -24
  260. package/src/screens/settings/settings-page.tsx +0 -426
  261. package/src/screens/setup-user.tsx +0 -355
  262. package/src/screens/tools/tool-access-ipc.test.ts +0 -55
  263. package/src/screens/tools/tool-access-ipc.ts +0 -27
  264. package/src/screens/tools/tool-catalog-ipc.test.ts +0 -52
  265. package/src/screens/tools/tool-catalog-ipc.ts +0 -51
  266. package/src/screens/tools/tool-details.tsx +0 -52
  267. package/src/screens/tools/tool-info.tsx +0 -45
  268. package/src/screens/tools/tool-list.tsx +0 -106
  269. package/src/screens/tools/tool-schema.tsx +0 -44
  270. package/src/screens/tools/tool-test-details.tsx +0 -89
  271. package/src/screens/tools/tool-test-list.tsx +0 -73
  272. package/src/screens/variables/variable-details.tsx +0 -289
  273. package/src/screens/variables/variable-list.tsx +0 -110
  274. package/src/screens/welcome-modal.tsx +0 -294
  275. package/src/screens/workflows/workflow-details.tsx +0 -115
  276. package/src/screens/workflows/workflow-info.tsx +0 -22
  277. package/src/screens/workflows/workflow-instructions.tsx +0 -134
  278. package/src/screens/workflows/workflow-list.tsx +0 -117
  279. package/src/setupTests.ts +0 -38
  280. package/src/system-apps/assistants.app.ts +0 -7
  281. package/src/system-apps/console-logs.app.ts +0 -7
  282. package/src/system-apps/data-explorer.app.ts +0 -8
  283. package/src/system-apps/identity.app.ts +0 -9
  284. package/src/system-apps/index.ts +0 -77
  285. package/src/system-apps/mobile-settings.app.ts +0 -7
  286. package/src/system-apps/network-viewer.app.ts +0 -8
  287. package/src/system-apps/packages.app.ts +0 -7
  288. package/src/system-apps/search.app.ts +0 -8
  289. package/src/system-apps/settings.app.ts +0 -7
  290. package/src/system-apps/threads.app.ts +0 -7
  291. package/src/system-apps/tools.app.ts +0 -7
  292. package/src/system-apps/types.app.ts +0 -7
  293. package/src/system-apps/variables.app.ts +0 -7
  294. package/src/system-apps/workflows.app.ts +0 -7
  295. package/src/tabs-layout/network-status-indicator.test.tsx +0 -799
  296. package/src/tabs-layout/network-status-indicator.tsx +0 -756
  297. package/src/tabs-layout/tabs-layout.tsx +0 -857
  298. package/src/tabs-layout/tabs-state.test.ts +0 -152
  299. package/src/tabs-layout/tabs-state.ts +0 -587
  300. package/src/tabs-layout/use-network-status.test.tsx +0 -58
  301. package/src/tabs-layout/use-network-status.ts +0 -68
  302. package/src/ui-defaults/index.ts +0 -2
  303. package/src/ui-defaults/list-screen.tsx +0 -94
  304. package/src/ui-defaults/markdown-field.tsx +0 -87
  305. package/src/ui-router/routes-loader.ts +0 -193
  306. package/src/ui-router/ui-loader.test.tsx +0 -108
  307. package/src/ui-router/ui-loader.tsx +0 -606
  308. package/src/utils.ts +0 -95
  309. package/tsconfig.json +0 -18
@@ -1,311 +0,0 @@
1
- import {
2
- type GroupInviteMode,
3
- GroupMemberRole,
4
- type ICreatedInvite,
5
- type IUserWithSource,
6
- usersCursor,
7
- } from "@peers-app/peers-sdk";
8
- import { useState } from "react";
9
- import { Typeahead, type TypeaheadItem } from "../../components/typeahead";
10
- import { PendingWorkLink } from "../invites/activity-view";
11
- import { InviteShare } from "../invites/invite-share";
12
- import { inviteTools } from "../invites/invites-ipc";
13
-
14
- /** Props for {@link GroupInvitePanel}. */
15
- export interface GroupInvitePanelProps {
16
- groupId: string;
17
- /** Users who are already members; hidden from the contact picker. */
18
- excludeUserIds: string[];
19
- /** The local user's role; caps the roles offered. */
20
- myRole: GroupMemberRole;
21
- }
22
-
23
- const ROLE_OPTIONS: Array<{ value: GroupMemberRole; label: string }> = [
24
- { value: GroupMemberRole.Reader, label: "Reader" },
25
- { value: GroupMemberRole.Writer, label: "Writer" },
26
- { value: GroupMemberRole.Admin, label: "Admin" },
27
- { value: GroupMemberRole.Owner, label: "Owner" },
28
- ];
29
-
30
- const EXPIRY_OPTIONS: Array<{ ms: number; label: string }> = [
31
- { ms: 60 * 60_000, label: "1 hour" },
32
- { ms: 24 * 60 * 60_000, label: "1 day" },
33
- { ms: 7 * 24 * 60 * 60_000, label: "7 days" },
34
- { ms: 30 * 24 * 60 * 60_000, label: "30 days" },
35
- ];
36
-
37
- function RoleSelect(props: {
38
- value: GroupMemberRole;
39
- max: GroupMemberRole;
40
- disabled?: boolean;
41
- onChange: (role: GroupMemberRole) => void;
42
- }) {
43
- return (
44
- <select
45
- className="form-select"
46
- value={props.value}
47
- disabled={props.disabled}
48
- onChange={(event) => props.onChange(Number(event.target.value) as GroupMemberRole)}
49
- aria-label="Role"
50
- >
51
- {ROLE_OPTIONS.filter((option) => option.value <= props.max).map((option) => (
52
- <option key={option.value} value={option.value}>
53
- {option.label}
54
- </option>
55
- ))}
56
- </select>
57
- );
58
- }
59
-
60
- function ContactPicker(props: {
61
- excludeUserIds: string[];
62
- selected: IUserWithSource[];
63
- disabled: boolean;
64
- onChange: (items: IUserWithSource[]) => void;
65
- }) {
66
- const search = async (query: string): Promise<(IUserWithSource & TypeaheadItem)[]> => {
67
- const cursor = await usersCursor({ name: { $matchWords: query } }, { includeTrustLevel: true });
68
- const results: IUserWithSource[] = [];
69
- for await (const contact of cursor) {
70
- if (!props.excludeUserIds.includes(contact.userId)) {
71
- results.push(contact);
72
- }
73
- if (results.length >= 10) break;
74
- }
75
- return results.map((contact) => ({ ...contact, id: contact.userId }));
76
- };
77
- return (
78
- <Typeahead
79
- placeholder="Search contacts…"
80
- searchFn={search}
81
- onSelectionChange={(items) => props.onChange(items as IUserWithSource[])}
82
- renderItem={(contact) => (
83
- <div className="d-flex align-items-center">
84
- <i className="bi-person-circle me-2" style={{ fontSize: 20 }} />
85
- <div className="flex-grow-1">
86
- <div className="fw-semibold">{(contact as IUserWithSource).name}</div>
87
- <small className="text-muted">{(contact as IUserWithSource).userId}</small>
88
- </div>
89
- </div>
90
- )}
91
- renderBadge={(contact) => <span>{(contact as IUserWithSource).name}</span>}
92
- selectedItems={props.selected.map((contact) => ({ ...contact, id: contact.userId }))}
93
- multiSelect
94
- disabled={props.disabled}
95
- />
96
- );
97
- }
98
-
99
- /** Invite known contacts directly (they get a ready-to-accept invite; no link needed). */
100
- function InviteContactsCard(props: GroupInvitePanelProps) {
101
- const [selected, setSelected] = useState<IUserWithSource[]>([]);
102
- const [role, setRole] = useState<GroupMemberRole>(GroupMemberRole.Writer);
103
- const [busy, setBusy] = useState(false);
104
- const [message, setMessage] = useState<{ ok: boolean; text: string } | undefined>(undefined);
105
-
106
- const send = async () => {
107
- setBusy(true);
108
- setMessage(undefined);
109
- try {
110
- const tools = await inviteTools();
111
- let queued = 0;
112
- for (const contact of selected) {
113
- const { result } = await tools.inviteContactToGroup({
114
- groupId: props.groupId,
115
- userId: contact.userId,
116
- role,
117
- });
118
- if (result.deliveryState === "queued") queued++;
119
- }
120
- setMessage({
121
- ok: true,
122
- text:
123
- queued > 0
124
- ? `Invited ${selected.length}. ${queued} will be delivered when they are reachable.`
125
- : `Invited ${selected.length}. They will appear once they accept.`,
126
- });
127
- setSelected([]);
128
- } catch (err) {
129
- setMessage({ ok: false, text: (err as Error).message || "Could not send the invite." });
130
- } finally {
131
- setBusy(false);
132
- }
133
- };
134
-
135
- return (
136
- <div className="row g-2 align-items-start">
137
- <div className="col-md-6">
138
- <ContactPicker
139
- excludeUserIds={props.excludeUserIds}
140
- selected={selected}
141
- disabled={busy}
142
- onChange={setSelected}
143
- />
144
- </div>
145
- <div className="col-md-3">
146
- <RoleSelect value={role} max={props.myRole} disabled={busy} onChange={setRole} />
147
- </div>
148
- <div className="col-md-3">
149
- <button
150
- type="button"
151
- className="btn btn-primary w-100"
152
- disabled={busy || selected.length === 0}
153
- onClick={() => void send()}
154
- >
155
- {busy
156
- ? "Sending…"
157
- : selected.length > 1
158
- ? `Invite ${selected.length} contacts`
159
- : "Invite contact"}
160
- </button>
161
- </div>
162
- {message ? (
163
- <div className="col-12">
164
- <div className={`alert py-2 mb-0 ${message.ok ? "alert-success" : "alert-danger"}`}>
165
- {message.text}
166
- </div>
167
- </div>
168
- ) : null}
169
- </div>
170
- );
171
- }
172
-
173
- /** Issue a shareable link for people who are not contacts yet. */
174
- function ShareLinkCard(props: GroupInvitePanelProps) {
175
- const [role, setRole] = useState<GroupMemberRole>(GroupMemberRole.Reader);
176
- const [mode, setMode] = useState<GroupInviteMode>("approve");
177
- const [expiresInMs, setExpiresInMs] = useState(EXPIRY_OPTIONS[1].ms);
178
- const [busy, setBusy] = useState(false);
179
- const [error, setError] = useState("");
180
- const [invite, setInvite] = useState<ICreatedInvite | undefined>(undefined);
181
-
182
- const create = async () => {
183
- setBusy(true);
184
- setError("");
185
- try {
186
- const tools = await inviteTools();
187
- const { invite: created } = await tools.createGroupInvite({
188
- groupId: props.groupId,
189
- role,
190
- mode,
191
- expiresInMs,
192
- });
193
- setInvite(created);
194
- } catch (err) {
195
- setError((err as Error).message || "Could not create the link.");
196
- } finally {
197
- setBusy(false);
198
- }
199
- };
200
-
201
- if (invite) {
202
- return (
203
- <InviteShare
204
- invite={invite}
205
- description={
206
- mode === "open"
207
- ? "Anyone with this link joins immediately. Revoke it from the pending list when you are done."
208
- : "People who use this link appear below as join requests for you to approve."
209
- }
210
- shareText={`Join ${invite.token.group?.name ?? "my group"} on Peers:`}
211
- onDone={() => setInvite(undefined)}
212
- />
213
- );
214
- }
215
-
216
- return (
217
- <div className="row g-2 align-items-end">
218
- <div className="col-md-3">
219
- <label htmlFor="link-role" className="form-label small text-muted mb-1">
220
- Role
221
- </label>
222
- <RoleSelect value={role} max={props.myRole} disabled={busy} onChange={setRole} />
223
- </div>
224
- <div className="col-md-3">
225
- <label htmlFor="link-mode" className="form-label small text-muted mb-1">
226
- Approval
227
- </label>
228
- <select
229
- id="link-mode"
230
- className="form-select"
231
- value={mode}
232
- disabled={busy}
233
- onChange={(event) => setMode(event.target.value as GroupInviteMode)}
234
- >
235
- <option value="approve">I approve each join</option>
236
- <option value="open">Anyone with the link joins</option>
237
- </select>
238
- </div>
239
- <div className="col-md-3">
240
- <label htmlFor="link-expiry" className="form-label small text-muted mb-1">
241
- Expires in
242
- </label>
243
- <select
244
- id="link-expiry"
245
- className="form-select"
246
- value={expiresInMs}
247
- disabled={busy}
248
- onChange={(event) => setExpiresInMs(Number(event.target.value))}
249
- >
250
- {EXPIRY_OPTIONS.map((option) => (
251
- <option key={option.ms} value={option.ms}>
252
- {option.label}
253
- </option>
254
- ))}
255
- </select>
256
- </div>
257
- <div className="col-md-3">
258
- <button
259
- type="button"
260
- className="btn btn-outline-primary w-100"
261
- disabled={busy}
262
- onClick={() => void create()}
263
- >
264
- {busy ? "Creating…" : "Create link"}
265
- </button>
266
- </div>
267
- {error ? (
268
- <div className="col-12">
269
- <div className="alert alert-danger py-2 mb-0">{error}</div>
270
- </div>
271
- ) : null}
272
- </div>
273
- );
274
- }
275
-
276
- /**
277
- * Admin controls for growing a group: invite existing contacts first, share a link second,
278
- * and act on pending join requests. Membership changes land when the other side accepts.
279
- */
280
- export function GroupInvitePanel(props: GroupInvitePanelProps) {
281
- const [tab, setTab] = useState<"contacts" | "link">("contacts");
282
- return (
283
- <>
284
- <div className="card mb-3">
285
- <div className="card-header d-flex align-items-center gap-3">
286
- <h6 className="mb-0">Invite people</h6>
287
- <div className="btn-group btn-group-sm ms-auto" role="group" aria-label="Invite method">
288
- <button
289
- type="button"
290
- className={`btn ${tab === "contacts" ? "btn-primary" : "btn-outline-primary"}`}
291
- onClick={() => setTab("contacts")}
292
- >
293
- Contacts
294
- </button>
295
- <button
296
- type="button"
297
- className={`btn ${tab === "link" ? "btn-primary" : "btn-outline-primary"}`}
298
- onClick={() => setTab("link")}
299
- >
300
- Share a link
301
- </button>
302
- </div>
303
- </div>
304
- <div className="card-body">
305
- {tab === "contacts" ? <InviteContactsCard {...props} /> : <ShareLinkCard {...props} />}
306
- </div>
307
- </div>
308
- <PendingWorkLink type="group" groupId={props.groupId} />
309
- </>
310
- );
311
- }
@@ -1,44 +0,0 @@
1
- import { GroupMemberRole } from "@peers-app/peers-sdk";
2
- import { render, screen } from "@testing-library/react";
3
- import { GroupSummaryRow, type IGroupSummary } from "./group-list";
4
-
5
- jest.mock("../../globals", () => ({
6
- mainContentPath: jest.fn(),
7
- }));
8
-
9
- jest.mock("../../hooks", () => ({
10
- usePromise: jest.fn(),
11
- }));
12
-
13
- jest.mock("../invites/activity-view", () => ({
14
- PendingWorkLink: () => null,
15
- }));
16
-
17
- const summary: IGroupSummary = {
18
- group: {
19
- groupId: "zyxwvutsrqponmlkjihgfedcb",
20
- name: "Neighborhood",
21
- description: "**Local** projects and shared plans",
22
- founderUserId: "abcdefghijklmnopqrstuvwxy",
23
- publicKey: "signing-key",
24
- publicBoxKey: "box-key",
25
- signature: "signature",
26
- },
27
- memberCount: 7,
28
- myRole: GroupMemberRole.Admin,
29
- };
30
-
31
- describe("GroupSummaryRow", () => {
32
- it("shows description, member count, and local role in a clickable row", () => {
33
- render(<GroupSummaryRow summary={summary} />);
34
-
35
- expect(screen.getByText("Neighborhood")).toBeInTheDocument();
36
- expect(screen.getByText("Local projects and shared plans")).toBeInTheDocument();
37
- expect(screen.getByText("7 members")).toBeInTheDocument();
38
- expect(screen.getByText("Admin")).toBeInTheDocument();
39
- expect(screen.getByRole("link")).toHaveAttribute(
40
- "href",
41
- `#identity/groups/${summary.group.groupId}`,
42
- );
43
- });
44
- });
@@ -1,317 +0,0 @@
1
- import {
2
- type GroupMemberRole,
3
- GroupMembers,
4
- Groups,
5
- getMe,
6
- getRoleLabel,
7
- getUserContext,
8
- type IGroup,
9
- newid,
10
- newKeys,
11
- saveGroupSecretKey,
12
- setUserTrustLevel,
13
- TrustLevel,
14
- type UserContext,
15
- Users,
16
- } from "@peers-app/peers-sdk";
17
- import { useEffect, useMemo, useState } from "react";
18
- import { LoadingIndicator } from "../../components/loading-indicator";
19
- import { mainContentPath } from "../../globals";
20
- import { usePromise } from "../../hooks";
21
- import { IDENTITY_GROUP_NEW_PATH, IDENTITY_GROUPS_PATH } from "../identity/identity-routes";
22
- import { PendingWorkLink } from "../invites/activity-view";
23
- import { packagesTools } from "../packages/packages-ipc";
24
-
25
- /** Search, summarize, and create groups owned by the signed-in user. */
26
- export function GroupList(props: { creating?: boolean } = {}) {
27
- const userContext = usePromise(getUserContext);
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);
35
-
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
- }
54
- };
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();
69
- });
70
- return () => {
71
- disposed = true;
72
- for (const subscription of subscriptions) {
73
- subscription.unsubscribe();
74
- }
75
- };
76
- }, [groupRevision, userContext]);
77
-
78
- useEffect(() => {
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 ?? [];
86
- }
87
- return (summaries ?? []).filter(
88
- ({ group }) =>
89
- group.name.toLocaleLowerCase().includes(query) ||
90
- group.description.toLocaleLowerCase().includes(query),
91
- );
92
- }, [searchText, summaries]);
93
-
94
- function startCreate() {
95
- setDraftName(searchText.trim());
96
- mainContentPath(IDENTITY_GROUP_NEW_PATH);
97
- }
98
-
99
- async function confirmCreate() {
100
- const name = draftName.trim();
101
- if (!name) {
102
- return;
103
- }
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);
114
- }
115
- }
116
-
117
- async function newRecord(name: string) {
118
- const userContext = await getUserContext();
119
- const groupName = name.trim();
120
- const groupId = newid();
121
-
122
- // Generate cryptographic keys for the group
123
- const groupKeys = newKeys();
124
-
125
- // NOTE: this is relying on the userContext to be able to create a group context for a group that doesn't yet exist
126
- // at time of writing, this is supported but is probably unintuitive behavior so we need to watch for this changing in the future
127
- const groupDataContext = userContext.getDataContext(groupId);
128
-
129
- // Store the validated secret under its deterministic persistent-variable ID.
130
- await saveGroupSecretKey(userContext.userDataContext, groupId, groupKeys.secretKey, groupKeys);
131
-
132
- // add me to the group's users table
133
- const me = await getMe();
134
- await Users(groupDataContext).save(me);
135
-
136
- // Set trust level for current user in the group context
137
- await setUserTrustLevel(me.userId, TrustLevel.Trusted, groupDataContext);
138
-
139
- // create group in both the user's and group's context
140
- const group = await Groups(userContext.userDataContext).signAndSave({
141
- groupId,
142
- name: groupName,
143
- description: "",
144
- iconClassName: "bi-people-fill", // Default icon
145
- founderUserId: me.userId,
146
- disabled: false,
147
- publicKey: groupKeys.publicKey,
148
- publicBoxKey: groupKeys.publicBoxKey,
149
- signature: "", // will be set via signAndSave
150
- });
151
- await Groups(groupDataContext).signAndSave(group);
152
-
153
- // Auto-install peers-core for the new group
154
- await packagesTools(groupId)
155
- .then((tools) => tools.installDefaultPackages())
156
- .catch((err: unknown) => {
157
- console.error("Error auto-installing peers-core for group:", err);
158
- });
159
-
160
- mainContentPath(`${IDENTITY_GROUPS_PATH}/${group.groupId}`);
161
- return group;
162
- }
163
-
164
- return (
165
- <div className="container-xxl p-3">
166
- <div className="mb-3">
167
- <h4 className="mb-1">
168
- <i className="bi-people-fill me-2" />
169
- Groups
170
- </h4>
171
- <div className="small text-muted">Shared spaces, membership, and access</div>
172
- </div>
173
- {creating ? (
174
- <div className="card mb-3">
175
- <div className="card-body">
176
- <h6 className="card-title">Create a group</h6>
177
- <form
178
- className="input-group"
179
- onSubmit={(event) => {
180
- event.preventDefault();
181
- void confirmCreate();
182
- }}
183
- >
184
- <input
185
- id="new-group-name"
186
- className="form-control"
187
- value={draftName}
188
- onChange={(event) => setDraftName(event.target.value)}
189
- placeholder="Group name"
190
- aria-label="Group name"
191
- />
192
- <button
193
- type="submit"
194
- className="btn btn-primary"
195
- disabled={!draftName.trim() || creatingBusy}
196
- >
197
- {creatingBusy ? "Creating…" : "Create"}
198
- </button>
199
- <a href={`#${IDENTITY_GROUPS_PATH}`} className="btn btn-outline-secondary">
200
- Cancel
201
- </a>
202
- </form>
203
- </div>
204
- </div>
205
- ) : null}
206
- <PendingWorkLink type="group" />
207
- <div className="position-relative my-3">
208
- <i
209
- className="bi bi-search position-absolute text-muted"
210
- style={{ left: 12, top: "50%", transform: "translateY(-50%)" }}
211
- />
212
- <input
213
- type="search"
214
- className="form-control ps-5"
215
- value={searchText}
216
- onChange={(event) => setSearchText(event.target.value)}
217
- placeholder="Search groups"
218
- aria-label="Search groups"
219
- />
220
- </div>
221
-
222
- {error ? <div className="alert alert-danger">{error}</div> : null}
223
- {!summaries && !error ? <LoadingIndicator /> : null}
224
- {summaries && filtered.length === 0 ? (
225
- <div className="border rounded p-4 text-center">
226
- <i className="bi bi-people fs-2 text-muted" />
227
- <h6 className="mt-2">{searchText ? "No matching groups" : "No groups yet"}</h6>
228
- <p className="small text-muted">
229
- {searchText
230
- ? "Try a different name or description."
231
- : "Create a shared space or use an invite to join one."}
232
- </p>
233
- {!searchText ? (
234
- <button type="button" className="btn btn-primary btn-sm" onClick={startCreate}>
235
- Create your first group
236
- </button>
237
- ) : null}
238
- </div>
239
- ) : null}
240
- {filtered.length > 0 ? (
241
- <div className="list-group">
242
- {filtered.map((summary) => (
243
- <GroupSummaryRow key={summary.group.groupId} summary={summary} />
244
- ))}
245
- </div>
246
- ) : null}
247
- </div>
248
- );
249
- }
250
-
251
- /** Data shown for one group in the Identity list. */
252
- export interface IGroupSummary {
253
- /** Signed group record. */
254
- group: IGroup;
255
- /** Number of active memberships. */
256
- memberCount: number;
257
- /** Signed-in user's role in this group. */
258
- myRole?: GroupMemberRole;
259
- }
260
-
261
- async function loadGroupSummaries(userContext: UserContext): Promise<IGroupSummary[]> {
262
- const groups = await Groups(userContext.userDataContext).list({ disabled: { $ne: true } });
263
- const summaries = await Promise.all(
264
- groups.map(async (group) => {
265
- const members = await GroupMembers(userContext.getDataContext(group.groupId)).list({
266
- deleted: { $ne: true },
267
- });
268
- return {
269
- group,
270
- memberCount: members.length,
271
- myRole: members.find((member) => member.userId === userContext.userId)?.role,
272
- };
273
- }),
274
- );
275
- return summaries.sort((left, right) => left.group.name.localeCompare(right.group.name));
276
- }
277
-
278
- /** Compact, fully clickable group summary row. */
279
- export function GroupSummaryRow({ summary }: { summary: IGroupSummary }) {
280
- const { group } = summary;
281
- return (
282
- <a
283
- href={`#${IDENTITY_GROUPS_PATH}/${group.groupId}`}
284
- className="list-group-item list-group-item-action d-flex align-items-center gap-3 py-3"
285
- >
286
- <span
287
- className="rounded-3 bg-body-secondary d-inline-flex align-items-center justify-content-center flex-shrink-0"
288
- style={{ width: 44, height: 44 }}
289
- aria-hidden="true"
290
- >
291
- <i className={group.iconClassName || "bi bi-people-fill"} />
292
- </span>
293
- <span className="flex-grow-1 overflow-hidden">
294
- <span className="fw-semibold d-block text-truncate">{group.name || "Unnamed group"}</span>
295
- <span className="small text-muted d-block text-truncate">
296
- {plainDescription(group.description) || "No description yet"}
297
- </span>
298
- </span>
299
- <span className="d-flex align-items-center gap-2 flex-wrap justify-content-end">
300
- <span className="badge text-bg-light border">
301
- {summary.memberCount} member{summary.memberCount === 1 ? "" : "s"}
302
- </span>
303
- {summary.myRole !== undefined ? (
304
- <span className="badge text-bg-secondary">{getRoleLabel(summary.myRole)}</span>
305
- ) : null}
306
- </span>
307
- <i className="bi bi-chevron-right text-muted" aria-hidden="true" />
308
- </a>
309
- );
310
- }
311
-
312
- function plainDescription(markdown: string): string {
313
- return markdown
314
- .replace(/[#_*`>[\]()!-]/g, " ")
315
- .replace(/\s+/g, " ")
316
- .trim();
317
- }