@peers-app/peers-ui 0.27.3 → 0.27.5

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 (311) hide show
  1. package/dist/screens/network-viewer/group-details-modal.d.ts +4 -0
  2. package/dist/screens/network-viewer/group-details-modal.js +6 -46
  3. package/dist/screens/network-viewer/network-viewer-ipc.d.ts +3 -2
  4. package/dist/screens/network-viewer/network-viewer-ipc.js +6 -0
  5. package/dist/screens/network-viewer/sync-recovery.js +103 -1
  6. package/dist/screens/variables/variable-details.js +2 -1
  7. package/dist/tabs-layout/network-status-indicator.js +26 -12
  8. package/dist/tabs-layout/use-network-status.d.ts +6 -1
  9. package/dist/tabs-layout/use-network-status.js +27 -4
  10. package/package.json +9 -3
  11. package/.claude/settings.local.json +0 -34
  12. package/.github/README.md +0 -52
  13. package/.github/workflows/delete-all-artifacts.yml +0 -49
  14. package/.github/workflows/publish.yml +0 -51
  15. package/babel.config.js +0 -7
  16. package/biome.json +0 -198
  17. package/dist/app-updates/app-updates-consumer.test.d.ts +0 -1
  18. package/dist/app-updates/app-updates-consumer.test.js +0 -56
  19. package/dist/app-updates/app-updates-settings.test.d.ts +0 -1
  20. package/dist/app-updates/app-updates-settings.test.js +0 -93
  21. package/dist/app-updates/remote-app-update-panel.test.d.ts +0 -1
  22. package/dist/app-updates/remote-app-update-panel.test.js +0 -165
  23. package/dist/components/access-level-field.test.d.ts +0 -1
  24. package/dist/components/access-level-field.test.js +0 -28
  25. package/dist/components/message-logs/tool-access-approval-card.test.d.ts +0 -1
  26. package/dist/components/message-logs/tool-access-approval-card.test.js +0 -102
  27. package/dist/components/responsive-section-nav.test.d.ts +0 -1
  28. package/dist/components/responsive-section-nav.test.js +0 -45
  29. package/dist/globals-hash.test.d.ts +0 -1
  30. package/dist/globals-hash.test.js +0 -16
  31. package/dist/hooks/use-observable-state.test.d.ts +0 -1
  32. package/dist/hooks/use-observable-state.test.js +0 -64
  33. package/dist/hooks/use-observable.test.d.ts +0 -1
  34. package/dist/hooks/use-observable.test.js +0 -191
  35. package/dist/hooks/use-rows.test.d.ts +0 -1
  36. package/dist/hooks/use-rows.test.js +0 -175
  37. package/dist/hooks/use-tool.test.d.ts +0 -1
  38. package/dist/hooks/use-tool.test.js +0 -190
  39. package/dist/host-shell/host-shell-consumer.test.d.ts +0 -1
  40. package/dist/host-shell/host-shell-consumer.test.js +0 -28
  41. package/dist/root-layout/root-layout-host.test.d.ts +0 -1
  42. package/dist/root-layout/root-layout-host.test.js +0 -86
  43. package/dist/screens/account/account-screen.test.d.ts +0 -1
  44. package/dist/screens/account/account-screen.test.js +0 -51
  45. package/dist/screens/assistants/assistant-config.test.d.ts +0 -1
  46. package/dist/screens/assistants/assistant-config.test.js +0 -57
  47. package/dist/screens/contacts/contact-list.test.d.ts +0 -1
  48. package/dist/screens/contacts/contact-list.test.js +0 -48
  49. package/dist/screens/device-pairing/device-pairing.test.d.ts +0 -1
  50. package/dist/screens/device-pairing/device-pairing.test.js +0 -119
  51. package/dist/screens/devices/devices-list.test.d.ts +0 -1
  52. package/dist/screens/devices/devices-list.test.js +0 -53
  53. package/dist/screens/groups/group-list.test.d.ts +0 -1
  54. package/dist/screens/groups/group-list.test.js +0 -38
  55. package/dist/screens/identity/identity-routes.test.d.ts +0 -1
  56. package/dist/screens/identity/identity-routes.test.js +0 -51
  57. package/dist/screens/identity/identity-screen.test.d.ts +0 -1
  58. package/dist/screens/identity/identity-screen.test.js +0 -103
  59. package/dist/screens/invites/accept-invite.test.d.ts +0 -1
  60. package/dist/screens/invites/accept-invite.test.js +0 -103
  61. package/dist/screens/invites/activity-view.test.d.ts +0 -1
  62. package/dist/screens/invites/activity-view.test.js +0 -108
  63. package/dist/screens/invites/invite-classify.test.d.ts +0 -1
  64. package/dist/screens/invites/invite-classify.test.js +0 -115
  65. package/dist/screens/network-viewer/network-viewer-ipc.test.d.ts +0 -1
  66. package/dist/screens/network-viewer/network-viewer-ipc.test.js +0 -61
  67. package/dist/screens/network-viewer/network-viewer.section.test.d.ts +0 -1
  68. package/dist/screens/network-viewer/network-viewer.section.test.js +0 -63
  69. package/dist/screens/network-viewer/sync-recovery.test.d.ts +0 -1
  70. package/dist/screens/network-viewer/sync-recovery.test.js +0 -104
  71. package/dist/screens/packages/package-helpers.test.d.ts +0 -1
  72. package/dist/screens/packages/package-helpers.test.js +0 -53
  73. package/dist/screens/packages/package-runtime-status.test.d.ts +0 -1
  74. package/dist/screens/packages/package-runtime-status.test.js +0 -50
  75. package/dist/screens/packages/packages-ipc.test.d.ts +0 -1
  76. package/dist/screens/packages/packages-ipc.test.js +0 -43
  77. package/dist/screens/settings/settings-page.test.d.ts +0 -1
  78. package/dist/screens/settings/settings-page.test.js +0 -19
  79. package/dist/screens/tools/tool-access-ipc.test.d.ts +0 -1
  80. package/dist/screens/tools/tool-access-ipc.test.js +0 -40
  81. package/dist/screens/tools/tool-catalog-ipc.test.d.ts +0 -1
  82. package/dist/screens/tools/tool-catalog-ipc.test.js +0 -52
  83. package/dist/tabs-layout/network-status-indicator.test.d.ts +0 -1
  84. package/dist/tabs-layout/network-status-indicator.test.js +0 -665
  85. package/dist/tabs-layout/tabs-state.test.d.ts +0 -1
  86. package/dist/tabs-layout/tabs-state.test.js +0 -110
  87. package/dist/tabs-layout/use-network-status.test.d.ts +0 -1
  88. package/dist/tabs-layout/use-network-status.test.js +0 -45
  89. package/dist/ui-router/ui-loader.test.d.ts +0 -1
  90. package/dist/ui-router/ui-loader.test.js +0 -91
  91. package/jest.config.js +0 -25
  92. package/release-guard.js +0 -10
  93. package/src/app-updates/app-update-controls.tsx +0 -73
  94. package/src/app-updates/app-updates-consumer.test.ts +0 -83
  95. package/src/app-updates/app-updates-consumer.ts +0 -121
  96. package/src/app-updates/app-updates-settings.test.tsx +0 -109
  97. package/src/app-updates/app-updates-settings.tsx +0 -136
  98. package/src/app-updates/remote-app-update-panel.test.tsx +0 -205
  99. package/src/app-updates/remote-app-update-panel.tsx +0 -210
  100. package/src/app-updates/use-app-update-status.ts +0 -16
  101. package/src/command-palette/command-palette-ui.tsx +0 -564
  102. package/src/command-palette/command-palette.ts +0 -242
  103. package/src/components/access-level-field.test.tsx +0 -32
  104. package/src/components/access-level-field.tsx +0 -68
  105. package/src/components/checkbox.tsx +0 -25
  106. package/src/components/group-switcher.tsx +0 -526
  107. package/src/components/input-date.tsx +0 -29
  108. package/src/components/input-number.tsx +0 -83
  109. package/src/components/input.tsx +0 -25
  110. package/src/components/inverse-lazy-list.tsx +0 -107
  111. package/src/components/io-schema-values.tsx +0 -103
  112. package/src/components/io-schema.tsx +0 -259
  113. package/src/components/lazy-list.tsx +0 -115
  114. package/src/components/list-screen.tsx +0 -126
  115. package/src/components/loading-indicator.tsx +0 -7
  116. package/src/components/markdown-editor/autolink-plugin.tsx +0 -29
  117. package/src/components/markdown-editor/editor-inline.tsx +0 -9
  118. package/src/components/markdown-editor/editor.tsx +0 -158
  119. package/src/components/markdown-editor/markdown-plugin.tsx +0 -202
  120. package/src/components/markdown-editor/mention-node.ts +0 -204
  121. package/src/components/markdown-editor/mentions-plugin.tsx +0 -354
  122. package/src/components/markdown-editor/move-line-plugin.tsx +0 -186
  123. package/src/components/markdown-editor/select-line-boundary-plugin.tsx +0 -95
  124. package/src/components/markdown-editor/theme.ts +0 -46
  125. package/src/components/markdown-editor/toolbar.tsx +0 -269
  126. package/src/components/markdown-with-mentions.tsx +0 -198
  127. package/src/components/message-logs/message-logs.tsx +0 -523
  128. package/src/components/message-logs/tool-access-approval-card.test.tsx +0 -127
  129. package/src/components/message-logs/tool-access-approval-card.tsx +0 -178
  130. package/src/components/messages/avatar.tsx +0 -171
  131. package/src/components/messages/channel-message-list.tsx +0 -189
  132. package/src/components/messages/channel-view.tsx +0 -92
  133. package/src/components/messages/message-compose.tsx +0 -192
  134. package/src/components/messages/message-display.tsx +0 -231
  135. package/src/components/messages/thread-message-list.tsx +0 -135
  136. package/src/components/qr-scanner.tsx +0 -180
  137. package/src/components/responsive-section-nav.test.tsx +0 -88
  138. package/src/components/responsive-section-nav.tsx +0 -92
  139. package/src/components/router.tsx +0 -175
  140. package/src/components/save-button.tsx +0 -134
  141. package/src/components/sortable-list.tsx +0 -130
  142. package/src/components/tabs.tsx +0 -76
  143. package/src/components/tooltip.tsx +0 -43
  144. package/src/components/trust-level-badge.tsx +0 -44
  145. package/src/components/trust-level-dropdown.tsx +0 -136
  146. package/src/components/typeahead.tsx +0 -357
  147. package/src/globals-hash.test.ts +0 -19
  148. package/src/globals.tsx +0 -226
  149. package/src/hooks/index.ts +0 -8
  150. package/src/hooks/use-host-contract.ts +0 -75
  151. package/src/hooks/use-observable-state.test.tsx +0 -101
  152. package/src/hooks/use-observable-state.ts +0 -29
  153. package/src/hooks/use-observable.test.tsx +0 -258
  154. package/src/hooks/use-observable.ts +0 -67
  155. package/src/hooks/use-on-screen.ts +0 -24
  156. package/src/hooks/use-promise.ts +0 -34
  157. package/src/hooks/use-rows.test.tsx +0 -196
  158. package/src/hooks/use-rows.ts +0 -120
  159. package/src/hooks/use-subscription.ts +0 -24
  160. package/src/hooks/use-tool.test.tsx +0 -225
  161. package/src/hooks/use-tool.ts +0 -116
  162. package/src/host-shell/host-shell-consumer.test.ts +0 -32
  163. package/src/host-shell/host-shell-consumer.ts +0 -53
  164. package/src/index.tsx +0 -42
  165. package/src/mention-configs.ts +0 -185
  166. package/src/operator-console/operator-console-state.ts +0 -119
  167. package/src/operator-console/operator-console.tsx +0 -233
  168. package/src/peers-services-registration.ts +0 -6
  169. package/src/root-layout/global-overlays.ts +0 -50
  170. package/src/root-layout/layout-picker.tsx +0 -47
  171. package/src/root-layout/layouts/full-screen-layout.tsx +0 -144
  172. package/src/root-layout/layouts/tabs-with-console-layout.tsx +0 -260
  173. package/src/root-layout/root-layout-host.test.tsx +0 -108
  174. package/src/root-layout/root-layout-host.tsx +0 -147
  175. package/src/root-layout/root-layout-registry.ts +0 -51
  176. package/src/root-layout/root-layout-state.ts +0 -55
  177. package/src/root-layout/root-layout.ts +0 -32
  178. package/src/screens/account/account-screen.test.tsx +0 -65
  179. package/src/screens/account/account-screen.tsx +0 -942
  180. package/src/screens/assistants/assistant-config.test.tsx +0 -54
  181. package/src/screens/assistants/assistant-config.tsx +0 -121
  182. package/src/screens/assistants/assistant-details.tsx +0 -89
  183. package/src/screens/assistants/assistant-info.tsx +0 -66
  184. package/src/screens/assistants/assistant-list.tsx +0 -145
  185. package/src/screens/assistants/assistant-tools.tsx +0 -82
  186. package/src/screens/console-logs/console-logs-list.tsx +0 -374
  187. package/src/screens/console-logs/log-display.tsx +0 -137
  188. package/src/screens/console-logs/log-filters.tsx +0 -64
  189. package/src/screens/console-logs/mobile-log-card.tsx +0 -161
  190. package/src/screens/console-logs/resizable-table-header.tsx +0 -110
  191. package/src/screens/contacts/contact-details.tsx +0 -123
  192. package/src/screens/contacts/contact-list.test.tsx +0 -59
  193. package/src/screens/contacts/contact-list.tsx +0 -286
  194. package/src/screens/contacts/index.ts +0 -2
  195. package/src/screens/data-explorer/data-explorer.tsx +0 -633
  196. package/src/screens/data-explorer/index.ts +0 -6
  197. package/src/screens/data-explorer/query-executor.tsx +0 -520
  198. package/src/screens/device-pairing/device-pairing.test.tsx +0 -143
  199. package/src/screens/device-pairing/device-pairing.tsx +0 -461
  200. package/src/screens/device-pairing/identity-ipc.ts +0 -25
  201. package/src/screens/device-pairing/index.ts +0 -2
  202. package/src/screens/device-pairing/use-device-pairing-state.ts +0 -15
  203. package/src/screens/devices/devices-list.test.tsx +0 -68
  204. package/src/screens/devices/devices-list.tsx +0 -365
  205. package/src/screens/devices/index.ts +0 -1
  206. package/src/screens/groups/group-details.tsx +0 -160
  207. package/src/screens/groups/group-invite-panel.tsx +0 -311
  208. package/src/screens/groups/group-list.test.tsx +0 -44
  209. package/src/screens/groups/group-list.tsx +0 -317
  210. package/src/screens/groups/group-members.tsx +0 -343
  211. package/src/screens/groups/index.ts +0 -3
  212. package/src/screens/identity/identity-routes.test.ts +0 -70
  213. package/src/screens/identity/identity-routes.ts +0 -301
  214. package/src/screens/identity/identity-screen.test.tsx +0 -138
  215. package/src/screens/identity/identity-screen.tsx +0 -282
  216. package/src/screens/identity/index.ts +0 -2
  217. package/src/screens/identity/invite-contact-panel.tsx +0 -45
  218. package/src/screens/identity/profile-invite.tsx +0 -93
  219. package/src/screens/invites/accept-invite.test.tsx +0 -115
  220. package/src/screens/invites/accept-invite.tsx +0 -318
  221. package/src/screens/invites/activity-view.test.tsx +0 -127
  222. package/src/screens/invites/activity-view.tsx +0 -162
  223. package/src/screens/invites/index.ts +0 -6
  224. package/src/screens/invites/invite-capture.ts +0 -104
  225. package/src/screens/invites/invite-classify.test.ts +0 -129
  226. package/src/screens/invites/invite-share.tsx +0 -104
  227. package/src/screens/invites/invites-ipc.ts +0 -25
  228. package/src/screens/invites/pending-invites-list.tsx +0 -256
  229. package/src/screens/network-viewer/connection-troubleshooter.tsx +0 -395
  230. package/src/screens/network-viewer/cpu-usage-graph.tsx +0 -294
  231. package/src/screens/network-viewer/device-details-modal.tsx +0 -339
  232. package/src/screens/network-viewer/group-details-modal.tsx +0 -421
  233. package/src/screens/network-viewer/index.ts +0 -8
  234. package/src/screens/network-viewer/network-viewer-ipc.test.ts +0 -72
  235. package/src/screens/network-viewer/network-viewer-ipc.ts +0 -39
  236. package/src/screens/network-viewer/network-viewer-section.ts +0 -12
  237. package/src/screens/network-viewer/network-viewer.section.test.tsx +0 -68
  238. package/src/screens/network-viewer/network-viewer.tsx +0 -708
  239. package/src/screens/network-viewer/remote-logs-panel.tsx +0 -190
  240. package/src/screens/network-viewer/sync-recovery.test.tsx +0 -114
  241. package/src/screens/network-viewer/sync-recovery.tsx +0 -461
  242. package/src/screens/network-viewer/usage-graph.tsx +0 -455
  243. package/src/screens/packages/local-folder-grants-panel.tsx +0 -290
  244. package/src/screens/packages/local-folders-ipc.ts +0 -28
  245. package/src/screens/packages/package-details.tsx +0 -166
  246. package/src/screens/packages/package-helpers.test.ts +0 -57
  247. package/src/screens/packages/package-helpers.ts +0 -137
  248. package/src/screens/packages/package-info.tsx +0 -354
  249. package/src/screens/packages/package-list.tsx +0 -519
  250. package/src/screens/packages/package-network-panel.tsx +0 -82
  251. package/src/screens/packages/package-new-local.tsx +0 -100
  252. package/src/screens/packages/package-runtime-status.test.tsx +0 -58
  253. package/src/screens/packages/package-runtime-status.tsx +0 -68
  254. package/src/screens/packages/package-versions.tsx +0 -298
  255. package/src/screens/packages/packages-ipc.test.ts +0 -47
  256. package/src/screens/packages/packages-ipc.ts +0 -125
  257. package/src/screens/peer-types/peer-type-details.tsx +0 -172
  258. package/src/screens/peer-types/peer-type-list.tsx +0 -64
  259. package/src/screens/search/global-search.tsx +0 -406
  260. package/src/screens/settings/color-mode-dropdown.tsx +0 -82
  261. package/src/screens/settings/settings-page.test.tsx +0 -24
  262. package/src/screens/settings/settings-page.tsx +0 -426
  263. package/src/screens/setup-user.tsx +0 -355
  264. package/src/screens/tools/tool-access-ipc.test.ts +0 -55
  265. package/src/screens/tools/tool-access-ipc.ts +0 -27
  266. package/src/screens/tools/tool-catalog-ipc.test.ts +0 -52
  267. package/src/screens/tools/tool-catalog-ipc.ts +0 -51
  268. package/src/screens/tools/tool-details.tsx +0 -52
  269. package/src/screens/tools/tool-info.tsx +0 -45
  270. package/src/screens/tools/tool-list.tsx +0 -106
  271. package/src/screens/tools/tool-schema.tsx +0 -44
  272. package/src/screens/tools/tool-test-details.tsx +0 -89
  273. package/src/screens/tools/tool-test-list.tsx +0 -73
  274. package/src/screens/variables/variable-details.tsx +0 -289
  275. package/src/screens/variables/variable-list.tsx +0 -110
  276. package/src/screens/welcome-modal.tsx +0 -294
  277. package/src/screens/workflows/workflow-details.tsx +0 -115
  278. package/src/screens/workflows/workflow-info.tsx +0 -22
  279. package/src/screens/workflows/workflow-instructions.tsx +0 -134
  280. package/src/screens/workflows/workflow-list.tsx +0 -117
  281. package/src/setupTests.ts +0 -38
  282. package/src/system-apps/assistants.app.ts +0 -7
  283. package/src/system-apps/console-logs.app.ts +0 -7
  284. package/src/system-apps/data-explorer.app.ts +0 -8
  285. package/src/system-apps/identity.app.ts +0 -9
  286. package/src/system-apps/index.ts +0 -77
  287. package/src/system-apps/mobile-settings.app.ts +0 -7
  288. package/src/system-apps/network-viewer.app.ts +0 -8
  289. package/src/system-apps/packages.app.ts +0 -7
  290. package/src/system-apps/search.app.ts +0 -8
  291. package/src/system-apps/settings.app.ts +0 -7
  292. package/src/system-apps/threads.app.ts +0 -7
  293. package/src/system-apps/tools.app.ts +0 -7
  294. package/src/system-apps/types.app.ts +0 -7
  295. package/src/system-apps/variables.app.ts +0 -7
  296. package/src/system-apps/workflows.app.ts +0 -7
  297. package/src/tabs-layout/network-status-indicator.test.tsx +0 -799
  298. package/src/tabs-layout/network-status-indicator.tsx +0 -756
  299. package/src/tabs-layout/tabs-layout.tsx +0 -857
  300. package/src/tabs-layout/tabs-state.test.ts +0 -152
  301. package/src/tabs-layout/tabs-state.ts +0 -587
  302. package/src/tabs-layout/use-network-status.test.tsx +0 -58
  303. package/src/tabs-layout/use-network-status.ts +0 -68
  304. package/src/ui-defaults/index.ts +0 -2
  305. package/src/ui-defaults/list-screen.tsx +0 -94
  306. package/src/ui-defaults/markdown-field.tsx +0 -87
  307. package/src/ui-router/routes-loader.ts +0 -193
  308. package/src/ui-router/ui-loader.test.tsx +0 -108
  309. package/src/ui-router/ui-loader.tsx +0 -606
  310. package/src/utils.ts +0 -95
  311. 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
- }