@rapidmx/web-client 0.4.0 → 0.6.0

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 (320) hide show
  1. package/README.md +88 -0
  2. package/apps/admin/audit-log/index.tsx +35 -8
  3. package/apps/admin/data-requests/index.tsx +480 -485
  4. package/apps/admin/distribution-lists/[uid].tsx +19 -1
  5. package/apps/admin/escrow-scopes/[uid].tsx +348 -237
  6. package/apps/admin/escrow-scopes/new/index.tsx +8 -2
  7. package/apps/admin/index.tsx +19 -1
  8. package/apps/admin/ingest-queue/index.tsx +44 -5
  9. package/apps/admin/mailboxes/[uid].tsx +211 -184
  10. package/apps/admin/quarantine/index.tsx +46 -14
  11. package/apps/admin/transport-rules/[uid].tsx +30 -1
  12. package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
  13. package/apps/admin/transport-rules/index.tsx +24 -6
  14. package/apps/admin/transport-rules/new/index.tsx +30 -1
  15. package/apps/escrow/_layout.tsx +2 -3
  16. package/apps/escrow/audit-log/index.tsx +196 -168
  17. package/apps/escrow/matters/[uid].tsx +621 -521
  18. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
  19. package/apps/shared/components/admin/layout/AdminShell.tsx +28 -9
  20. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
  21. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  22. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
  23. package/apps/shared/components/admin/settings/BrandingForm.tsx +15 -4
  24. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +18 -7
  25. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +10 -2
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +6 -0
  27. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +45 -12
  28. package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -595
  29. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -98
  30. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +49 -13
  31. package/apps/shared/components/admin/setup/SetupWizard.tsx +150 -34
  32. package/apps/shared/components/admin/signOut.ts +79 -0
  33. package/apps/shared/components/admin/usePagedList.tsx +129 -0
  34. package/apps/shared/components/calendar/EventModal.tsx +717 -549
  35. package/apps/shared/components/calendar/MonthView.tsx +173 -139
  36. package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
  37. package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
  38. package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
  39. package/apps/shared/components/calendar/allDay.ts +124 -0
  40. package/apps/shared/components/calendar/layout/CalendarShell.tsx +194 -192
  41. package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
  42. package/apps/shared/components/contacts/ContactForm.tsx +377 -348
  43. package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
  44. package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
  45. package/apps/shared/components/contacts/contactKeys.ts +78 -0
  46. package/apps/shared/components/contacts/layout/ContactsShell.tsx +199 -197
  47. package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
  48. package/apps/shared/components/layout/AppShell.tsx +123 -33
  49. package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
  50. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +389 -298
  51. package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
  52. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
  53. package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -128
  54. package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
  55. package/apps/shared/components/mail/MessageDetailPane.tsx +1318 -630
  56. package/apps/shared/components/mail/compose/ComposeContext.tsx +20 -15
  57. package/apps/shared/components/mail/compose/ComposeWindow.tsx +854 -132
  58. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
  59. package/apps/shared/components/mail/layout/MailShell.tsx +420 -408
  60. package/apps/shared/components/mail/pinnedSigners.ts +124 -0
  61. package/apps/shared/components/mail/verificationSeals.ts +125 -0
  62. package/apps/shared/components/mail/writableMailboxes.ts +101 -0
  63. package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
  64. package/apps/shared/components/settings/layout/SettingsShell.tsx +23 -9
  65. package/apps/shared/components/tasks/layout/TasksShell.tsx +201 -199
  66. package/apps/shared/mail/listAllPages.ts +39 -0
  67. package/apps/shared/plugins/pluginNav.ts +73 -0
  68. package/apps/shared/search/LocalIndexLifecycle.tsx +64 -55
  69. package/apps/shared/search/localIndexBuilder.ts +481 -179
  70. package/apps/shared/search/localIndexRpcClient.ts +231 -29
  71. package/apps/shared/search/localIndexSchema.ts +15 -6
  72. package/apps/shared/search/localIndexStorage.ts +98 -0
  73. package/apps/shared/search/localIndexVFS.ts +32 -1
  74. package/apps/shared/search/localIndexWorker.ts +648 -186
  75. package/apps/www/calendar/index.tsx +438 -417
  76. package/apps/www/contacts/[uid].tsx +21 -2
  77. package/apps/www/contacts/index.tsx +116 -12
  78. package/apps/www/index.tsx +1314 -1041
  79. package/apps/www/messages/[uid].tsx +18 -6
  80. package/apps/www/settings/auto-reply/index.tsx +134 -129
  81. package/apps/www/settings/encryption/index.tsx +739 -216
  82. package/apps/www/settings/filters/[uid].tsx +171 -155
  83. package/apps/www/settings/filters/new/index.tsx +8 -1
  84. package/apps/www/settings/focused-inbox/index.tsx +150 -148
  85. package/apps/www/settings/privacy/index.tsx +494 -436
  86. package/apps/www/settings/read-receipts/index.tsx +148 -125
  87. package/apps/www/settings/sharing/index.tsx +17 -14
  88. package/apps/www/tasks/index.tsx +638 -579
  89. package/dist/apps/admin/_404.d.ts +2 -0
  90. package/dist/apps/admin/_500.d.ts +2 -0
  91. package/dist/apps/admin/_layout.d.ts +8 -0
  92. package/dist/apps/admin/audit-log/index.d.ts +11 -0
  93. package/dist/apps/admin/audit-log/index.js +38 -8
  94. package/dist/apps/admin/branding/index.d.ts +3 -0
  95. package/dist/apps/admin/data-requests/index.d.ts +3 -0
  96. package/dist/apps/admin/data-requests/index.js +52 -72
  97. package/dist/apps/admin/distribution-lists/[uid].d.ts +7 -0
  98. package/dist/apps/admin/distribution-lists/[uid].js +1 -1
  99. package/dist/apps/admin/distribution-lists/index.d.ts +3 -0
  100. package/dist/apps/admin/distribution-lists/new/index.d.ts +3 -0
  101. package/dist/apps/admin/domains/[uid].d.ts +7 -0
  102. package/dist/apps/admin/domains/index.d.ts +3 -0
  103. package/dist/apps/admin/domains/new/index.d.ts +3 -0
  104. package/dist/apps/admin/encryption-policy/index.d.ts +3 -0
  105. package/dist/apps/admin/escrow-scopes/[uid].d.ts +21 -0
  106. package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
  107. package/dist/apps/admin/escrow-scopes/index.d.ts +3 -0
  108. package/dist/apps/admin/escrow-scopes/new/index.d.ts +3 -0
  109. package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
  110. package/dist/apps/admin/index.d.ts +3 -0
  111. package/dist/apps/admin/index.js +4 -1
  112. package/dist/apps/admin/ingest-queue/index.d.ts +4 -0
  113. package/dist/apps/admin/ingest-queue/index.js +24 -6
  114. package/dist/apps/admin/mailbox-policy/index.d.ts +3 -0
  115. package/dist/apps/admin/mailboxes/[uid].d.ts +7 -0
  116. package/dist/apps/admin/mailboxes/[uid].js +20 -4
  117. package/dist/apps/admin/mailboxes/new/index.d.ts +3 -0
  118. package/dist/apps/admin/plugins/index.d.ts +3 -0
  119. package/dist/apps/admin/quarantine/index.d.ts +6 -0
  120. package/dist/apps/admin/quarantine/index.js +20 -11
  121. package/dist/apps/admin/retention-policy/index.d.ts +3 -0
  122. package/dist/apps/admin/setup/index.d.ts +3 -0
  123. package/dist/apps/admin/transport-rules/[uid].d.ts +7 -0
  124. package/dist/apps/admin/transport-rules/[uid].js +18 -2
  125. package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +21 -0
  126. package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
  127. package/dist/apps/admin/transport-rules/index.d.ts +3 -0
  128. package/dist/apps/admin/transport-rules/index.js +22 -6
  129. package/dist/apps/admin/transport-rules/new/index.d.ts +3 -0
  130. package/dist/apps/admin/transport-rules/new/index.js +18 -2
  131. package/dist/apps/escrow/_layout.d.ts +10 -0
  132. package/dist/apps/escrow/_layout.js +3 -3
  133. package/dist/apps/escrow/audit-log/index.d.ts +9 -0
  134. package/dist/apps/escrow/audit-log/index.js +26 -1
  135. package/dist/apps/escrow/index.d.ts +3 -0
  136. package/dist/apps/escrow/matters/[uid].d.ts +7 -0
  137. package/dist/apps/escrow/matters/[uid].js +77 -42
  138. package/dist/apps/escrow/matters/new/index.d.ts +3 -0
  139. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.d.ts +17 -0
  140. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +32 -0
  141. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
  142. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +32 -0
  143. package/dist/apps/shared/components/admin/layout/AdminShell.js +15 -6
  144. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +12 -0
  145. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
  146. package/dist/apps/shared/components/admin/mailboxes/MailboxTable.d.ts +6 -0
  147. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.d.ts +19 -0
  148. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  149. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +12 -0
  150. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
  151. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +9 -0
  152. package/dist/apps/shared/components/admin/settings/BrandingForm.js +15 -4
  153. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +12 -0
  154. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +13 -7
  155. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +12 -0
  156. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +7 -2
  157. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.d.ts +9 -0
  158. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.d.ts +19 -0
  159. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +6 -0
  160. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +10 -0
  161. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +34 -12
  162. package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +4 -0
  163. package/dist/apps/shared/components/admin/settings/PluginsManager.js +390 -78
  164. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +9 -0
  165. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +40 -11
  166. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.d.ts +14 -0
  167. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +29 -13
  168. package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +36 -0
  169. package/dist/apps/shared/components/admin/setup/SetupWizard.js +87 -21
  170. package/dist/apps/shared/components/admin/signOut.d.ts +23 -0
  171. package/dist/apps/shared/components/admin/signOut.js +75 -0
  172. package/dist/apps/shared/components/admin/usePagedList.d.ts +39 -0
  173. package/dist/apps/shared/components/admin/usePagedList.js +99 -0
  174. package/dist/apps/shared/components/calendar/CalendarListSidebar.d.ts +26 -0
  175. package/dist/apps/shared/components/calendar/EventModal.d.ts +46 -0
  176. package/dist/apps/shared/components/calendar/EventModal.js +142 -21
  177. package/dist/apps/shared/components/calendar/MonthView.d.ts +15 -0
  178. package/dist/apps/shared/components/calendar/MonthView.js +12 -5
  179. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +20 -0
  180. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
  181. package/dist/apps/shared/components/calendar/ResourcePicker.d.ts +19 -0
  182. package/dist/apps/shared/components/calendar/SplitDayView.d.ts +28 -0
  183. package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
  184. package/dist/apps/shared/components/calendar/TimeGridView.d.ts +17 -0
  185. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
  186. package/dist/apps/shared/components/calendar/allDay.d.ts +51 -0
  187. package/dist/apps/shared/components/calendar/allDay.js +111 -0
  188. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +40 -0
  189. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +3 -3
  190. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +23 -0
  191. package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
  192. package/dist/apps/shared/components/contacts/ContactForm.d.ts +20 -0
  193. package/dist/apps/shared/components/contacts/ContactForm.js +32 -10
  194. package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +40 -0
  195. package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
  196. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +25 -0
  197. package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +40 -0
  198. package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
  199. package/dist/apps/shared/components/contacts/contactKeys.d.ts +29 -0
  200. package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
  201. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +21 -0
  202. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +2 -2
  203. package/dist/apps/shared/components/escrow/layout/EscrowShell.d.ts +25 -0
  204. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
  205. package/dist/apps/shared/components/forms/StringListField.d.ts +22 -0
  206. package/dist/apps/shared/components/layout/AppShell.d.ts +49 -0
  207. package/dist/apps/shared/components/layout/AppShell.js +97 -26
  208. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +18 -0
  209. package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
  210. package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +51 -0
  211. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +49 -12
  212. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +13 -0
  213. package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
  214. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +66 -0
  215. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
  216. package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +45 -0
  217. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +73 -14
  218. package/dist/apps/shared/components/layout/UserMenu.d.ts +23 -0
  219. package/dist/apps/shared/components/mail/ConversationList.d.ts +11 -0
  220. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +23 -0
  221. package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
  222. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +93 -0
  223. package/dist/apps/shared/components/mail/MessageDetailPane.js +441 -36
  224. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +58 -0
  225. package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
  226. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +15 -0
  227. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +37 -0
  228. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +699 -128
  229. package/dist/apps/shared/components/mail/compose/EmojiPicker.d.ts +14 -0
  230. package/dist/apps/shared/components/mail/compose/GifPicker.d.ts +13 -0
  231. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +34 -0
  232. package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.d.ts +16 -0
  233. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +27 -0
  234. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
  235. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +57 -0
  236. package/dist/apps/shared/components/mail/layout/MailShell.js +13 -4
  237. package/dist/apps/shared/components/mail/pinnedSigners.d.ts +33 -0
  238. package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
  239. package/dist/apps/shared/components/mail/verificationSeals.d.ts +22 -0
  240. package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
  241. package/dist/apps/shared/components/mail/writableMailboxes.d.ts +15 -0
  242. package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
  243. package/dist/apps/shared/components/rules/RuleBuilder.d.ts +70 -0
  244. package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
  245. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +41 -0
  246. package/dist/apps/shared/components/settings/layout/SettingsShell.js +19 -9
  247. package/dist/apps/shared/components/tasks/TasksSidebar.d.ts +41 -0
  248. package/dist/apps/shared/components/tasks/TasksToolbar.d.ts +14 -0
  249. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +24 -0
  250. package/dist/apps/shared/components/tasks/layout/TasksShell.js +2 -2
  251. package/dist/apps/shared/mail/findWellKnownFolderUid.d.ts +5 -0
  252. package/dist/apps/shared/mail/listAllPages.d.ts +17 -0
  253. package/dist/apps/shared/mail/listAllPages.js +25 -0
  254. package/dist/apps/shared/plugins/pluginNav.d.ts +39 -0
  255. package/dist/apps/shared/plugins/pluginNav.js +33 -0
  256. package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +13 -0
  257. package/dist/apps/shared/search/LocalIndexLifecycle.js +58 -54
  258. package/dist/apps/shared/search/localIndexBlockCipher.d.ts +36 -0
  259. package/dist/apps/shared/search/localIndexBuilder.d.ts +85 -0
  260. package/dist/apps/shared/search/localIndexBuilder.js +292 -37
  261. package/dist/apps/shared/search/localIndexKey.d.ts +4 -0
  262. package/dist/apps/shared/search/localIndexRpcClient.d.ts +71 -0
  263. package/dist/apps/shared/search/localIndexRpcClient.js +191 -26
  264. package/dist/apps/shared/search/localIndexSchema.d.ts +111 -0
  265. package/dist/apps/shared/search/localIndexSchema.js +12 -6
  266. package/dist/apps/shared/search/localIndexSizePreference.d.ts +27 -0
  267. package/dist/apps/shared/search/localIndexStorage.d.ts +28 -0
  268. package/dist/apps/shared/search/localIndexStorage.js +86 -0
  269. package/dist/apps/shared/search/localIndexVFS.d.ts +101 -0
  270. package/dist/apps/shared/search/localIndexVFS.js +68 -40
  271. package/dist/apps/shared/search/localIndexWorker.d.ts +145 -0
  272. package/dist/apps/shared/search/localIndexWorker.js +519 -173
  273. package/dist/apps/shared/search/searchTier2.d.ts +30 -0
  274. package/dist/apps/www/_404.d.ts +2 -0
  275. package/dist/apps/www/_500.d.ts +2 -0
  276. package/dist/apps/www/_layout.d.ts +14 -0
  277. package/dist/apps/www/calendar/index.d.ts +3 -0
  278. package/dist/apps/www/calendar/index.js +28 -10
  279. package/dist/apps/www/contacts/[uid].d.ts +13 -0
  280. package/dist/apps/www/contacts/[uid].js +13 -2
  281. package/dist/apps/www/contacts/index.d.ts +3 -0
  282. package/dist/apps/www/contacts/index.js +80 -10
  283. package/dist/apps/www/index.d.ts +3 -0
  284. package/dist/apps/www/index.js +339 -108
  285. package/dist/apps/www/messages/[uid].d.ts +11 -0
  286. package/dist/apps/www/messages/[uid].js +18 -6
  287. package/dist/apps/www/settings/auto-reply/index.d.ts +4 -0
  288. package/dist/apps/www/settings/auto-reply/index.js +9 -4
  289. package/dist/apps/www/settings/encryption/index.d.ts +7 -0
  290. package/dist/apps/www/settings/encryption/index.js +514 -97
  291. package/dist/apps/www/settings/filters/[uid].d.ts +8 -0
  292. package/dist/apps/www/settings/filters/[uid].js +22 -7
  293. package/dist/apps/www/settings/filters/_mailFilterRuleConfig.d.ts +11 -0
  294. package/dist/apps/www/settings/filters/index.d.ts +4 -0
  295. package/dist/apps/www/settings/filters/new/index.d.ts +4 -0
  296. package/dist/apps/www/settings/filters/new/index.js +8 -2
  297. package/dist/apps/www/settings/focused-inbox/index.d.ts +4 -0
  298. package/dist/apps/www/settings/focused-inbox/index.js +3 -1
  299. package/dist/apps/www/settings/labels/index.d.ts +4 -0
  300. package/dist/apps/www/settings/privacy/index.d.ts +4 -0
  301. package/dist/apps/www/settings/privacy/index.js +57 -22
  302. package/dist/apps/www/settings/read-receipts/index.d.ts +4 -0
  303. package/dist/apps/www/settings/read-receipts/index.js +10 -3
  304. package/dist/apps/www/settings/sharing/index.d.ts +4 -0
  305. package/dist/apps/www/settings/sharing/index.js +7 -11
  306. package/dist/apps/www/settings/signatures/[uid].d.ts +8 -0
  307. package/dist/apps/www/settings/signatures/index.d.ts +4 -0
  308. package/dist/apps/www/settings/signatures/new/index.d.ts +4 -0
  309. package/dist/apps/www/settings/signatures/signatureDefaults.d.ts +10 -0
  310. package/dist/apps/www/tasks/index.d.ts +3 -0
  311. package/dist/apps/www/tasks/index.js +50 -10
  312. package/package.json +3 -3
  313. package/apps/shared/components/booking/AvailabilityEditor.tsx +0 -118
  314. package/apps/www/settings/booking-types/[uid].tsx +0 -300
  315. package/apps/www/settings/booking-types/index.tsx +0 -98
  316. package/apps/www/settings/booking-types/new/index.tsx +0 -212
  317. package/dist/apps/shared/components/booking/AvailabilityEditor.js +0 -45
  318. package/dist/apps/www/settings/booking-types/[uid].js +0 -129
  319. package/dist/apps/www/settings/booking-types/index.js +0 -29
  320. package/dist/apps/www/settings/booking-types/new/index.js +0 -86
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ import { Contact } from "@rapidmx/react-shared/contacts/contactsApi.js";
3
+ import { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
4
+ export interface ContactFormProps {
5
+ contact?: Contact;
6
+ /** Create mode: the default mailbox (and its Contacts folder) to create in. */
7
+ mailboxUid?: string;
8
+ folderUid?: string;
9
+ /** Create mode: every mailbox the contact could be created in - with more than one, a Mailbox selector
10
+ * is shown. Choosing a mailbox other than `mailboxUid` looks up that mailbox's own Contacts folder. */
11
+ mailboxes?: Mailbox[];
12
+ onSaved: (contact: Contact) => void;
13
+ onCancel: () => void;
14
+ }
15
+ /**
16
+ * A contact's create/edit form. Shared by the desktop inline pane (`apps/www/contacts/index.tsx`) and the
17
+ * mobile detail route's own edit mode (`apps/www/contacts/[uid].tsx`) — creation itself stays
18
+ * desktop-and-mobile-inline (see that route's own doc comment on why "new" never gets a dedicated route).
19
+ */
20
+ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes, onSaved, onCancel }: ContactFormProps): React.JSX.Element;
@@ -10,6 +10,7 @@ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
10
10
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
11
11
  import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
12
12
  import { findWellKnownFolderUid } from "../../mail/findWellKnownFolderUid.js";
13
+ import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
13
14
  const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
14
15
  const SELECT_CLASS = "text-sm py-2 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
15
16
  const ADDRESS_KINDS = ["home", "work", "other"];
@@ -30,7 +31,9 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
30
31
  const [categoriesText, setCategoriesText] = useState((contact?.categories ?? []).join(", "));
31
32
  const [emails, setEmails] = useState(contact?.emails ?? []);
32
33
  const [phones, setPhones] = useState(contact?.phones ?? []);
33
- const [address, setAddress] = useState(contact?.addresses[0] ?? null);
34
+ // Every stored address is kept and editable - editing a contact must never silently drop addresses
35
+ // beyond the first.
36
+ const [addresses, setAddresses] = useState(contact?.addresses ?? []);
34
37
  const [error, setError] = useState(null);
35
38
  const [saving, setSaving] = useState(false);
36
39
  function updateEmail(index, patch) {
@@ -45,6 +48,12 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
45
48
  function removePhone(index) {
46
49
  setPhones((prev) => prev.filter((_, i) => i !== index));
47
50
  }
51
+ function updateAddress(index, patch) {
52
+ setAddresses((prev) => prev.map((a, i) => (i === index ? { ...a, ...patch } : a)));
53
+ }
54
+ function removeAddress(index) {
55
+ setAddresses((prev) => prev.filter((_, i) => i !== index));
56
+ }
48
57
  async function handleSubmit(e) {
49
58
  e.preventDefault();
50
59
  setError(null);
@@ -58,22 +67,29 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
58
67
  .split(",")
59
68
  .map((c) => c.trim())
60
69
  .filter(Boolean);
70
+ // An update merges only the fields in its body, so a field the user cleared is sent as an
71
+ // explicit `null` (an omitted/undefined one would keep its stored value); a create just omits it.
72
+ const cleared = contact ? null : undefined;
61
73
  const input = {
62
74
  displayName: displayName.trim(),
63
- givenName: givenName.trim() || undefined,
64
- surname: surname.trim() || undefined,
65
- company: company.trim() || undefined,
66
- jobTitle: jobTitle.trim() || undefined,
67
- notes: notes.trim() || undefined,
75
+ givenName: givenName.trim() || cleared,
76
+ surname: surname.trim() || cleared,
77
+ company: company.trim() || cleared,
78
+ jobTitle: jobTitle.trim() || cleared,
79
+ notes: notes.trim() || cleared,
68
80
  favorite,
69
81
  categories,
70
82
  emails,
71
83
  phones,
72
- addresses: address ? [address] : [],
84
+ addresses,
73
85
  };
74
86
  let saved;
75
87
  if (contact) {
76
- saved = await updateContact({ uid: contact.uid, version: contact.version, mailboxUid: contact.mailboxUid, folderUid: contact.folderUid, ...input });
88
+ saved = await updateContact({
89
+ uid: contact.uid,
90
+ version: contact.version,
91
+ ...input,
92
+ });
77
93
  }
78
94
  else {
79
95
  const targetFolderUid = targetMailboxUid === mailboxUid ? folderUid : await findWellKnownFolderUid(targetMailboxUid, "contacts");
@@ -81,8 +97,14 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
81
97
  setError("That mailbox has no Contacts folder.");
82
98
  return;
83
99
  }
84
- saved = await createContact({ mailboxUid: targetMailboxUid, folderUid: targetFolderUid, ...input });
100
+ saved = await createContact({
101
+ mailboxUid: targetMailboxUid,
102
+ folderUid: targetFolderUid,
103
+ ...input,
104
+ });
85
105
  }
106
+ // A message opened later in this page load must see the changed contact's signing keys.
107
+ clearPinnedSignerCache();
86
108
  onSaved(saved);
87
109
  }
88
110
  catch (err) {
@@ -92,5 +114,5 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
92
114
  setSaving(false);
93
115
  }
94
116
  }
95
- return (_jsxs("form", { onSubmit: handleSubmit, className: "max-w-xl flex flex-col gap-1", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-3", children: contact ? "Edit contact" : "New contact" }), error && _jsx(Alert, { children: error }), !contact && mailboxes && mailboxes.length > 1 && (_jsx(FormField, { label: "Mailbox", htmlFor: "contact-mailbox", children: _jsx("select", { id: "contact-mailbox", className: `${SELECT_CLASS} w-full`, value: targetMailboxUid, onChange: (e) => setTargetMailboxUid(e.target.value), children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) }) })), _jsx(FormField, { label: "Display name", htmlFor: "contact-displayName", children: _jsx("input", { id: "contact-displayName", type: "text", className: INPUT_CLASS, value: displayName, onChange: (e) => setDisplayName(e.target.value) }) }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-3", children: [_jsx(FormField, { label: "First name", htmlFor: "contact-givenName", children: _jsx("input", { id: "contact-givenName", type: "text", className: INPUT_CLASS, value: givenName, onChange: (e) => setGivenName(e.target.value) }) }), _jsx(FormField, { label: "Last name", htmlFor: "contact-surname", children: _jsx("input", { id: "contact-surname", type: "text", className: INPUT_CLASS, value: surname, onChange: (e) => setSurname(e.target.value) }) })] }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-3", children: [_jsx(FormField, { label: "Company", htmlFor: "contact-company", children: _jsx("input", { id: "contact-company", type: "text", className: INPUT_CLASS, value: company, onChange: (e) => setCompany(e.target.value) }) }), _jsx(FormField, { label: "Job title", htmlFor: "contact-jobTitle", children: _jsx("input", { id: "contact-jobTitle", type: "text", className: INPUT_CLASS, value: jobTitle, onChange: (e) => setJobTitle(e.target.value) }) })] }), _jsxs("label", { className: "flex items-center gap-2 text-sm mb-4", children: [_jsx("input", { type: "checkbox", checked: favorite, onChange: (e) => setFavorite(e.target.checked) }), "Favorite"] }), _jsx(FormField, { label: "Categories (comma-separated)", htmlFor: "contact-categories", children: _jsx("input", { id: "contact-categories", type: "text", className: INPUT_CLASS, value: categoriesText, onChange: (e) => setCategoriesText(e.target.value), placeholder: "VIP, Work" }) }), _jsx(FormField, { label: "Email", htmlFor: "contact-emails", children: _jsxs("div", { id: "contact-emails", className: "flex flex-col gap-2", children: [emails.map((email, i) => (_jsxs("div", { className: "flex gap-2", children: [_jsx("input", { type: "email", className: `${INPUT_CLASS} flex-1`, value: email.address, onChange: (e) => updateEmail(i, { address: e.target.value }), "aria-label": `Email address ${i + 1}` }), _jsx("select", { className: SELECT_CLASS, value: email.type, onChange: (e) => updateEmail(i, { type: e.target.value }), "aria-label": `Email type ${i + 1}`, children: ADDRESS_KINDS.map((kind) => (_jsx("option", { value: kind, children: kind }, kind))) }), _jsx("button", { type: "button", onClick: () => removeEmail(i), className: "text-sm text-danger px-2", "aria-label": `Remove email ${i + 1}`, children: "\u00D7" })] }, i))), _jsx("button", { type: "button", onClick: () => setEmails((prev) => [...prev, { address: "", type: "work" }]), className: "self-start text-xs font-medium text-primary-dark hover:underline", children: "+ Add email" })] }) }), _jsx(FormField, { label: "Phone", htmlFor: "contact-phones", children: _jsxs("div", { id: "contact-phones", className: "flex flex-col gap-2", children: [phones.map((phone, i) => (_jsxs("div", { className: "flex gap-2", children: [_jsx("input", { type: "tel", className: `${INPUT_CLASS} flex-1`, value: phone.phoneNumber, onChange: (e) => updatePhone(i, { phoneNumber: e.target.value }), "aria-label": `Phone number ${i + 1}` }), _jsx("select", { className: SELECT_CLASS, value: phone.type, onChange: (e) => updatePhone(i, { type: e.target.value }), "aria-label": `Phone type ${i + 1}`, children: ADDRESS_KINDS.map((kind) => (_jsx("option", { value: kind, children: kind }, kind))) }), _jsx("button", { type: "button", onClick: () => removePhone(i), className: "text-sm text-danger px-2", "aria-label": `Remove phone ${i + 1}`, children: "\u00D7" })] }, i))), _jsx("button", { type: "button", onClick: () => setPhones((prev) => [...prev, { phoneNumber: "", type: "work" }]), className: "self-start text-xs font-medium text-primary-dark hover:underline", children: "+ Add phone" })] }) }), _jsx(FormField, { label: "Address", htmlFor: "contact-address", children: address ? (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-2", children: [_jsx("input", { type: "text", placeholder: "Street", className: INPUT_CLASS, value: address.street ?? "", onChange: (e) => setAddress({ ...address, street: e.target.value }) }), _jsx("input", { type: "text", placeholder: "City", className: INPUT_CLASS, value: address.city ?? "", onChange: (e) => setAddress({ ...address, city: e.target.value }) }), _jsx("input", { type: "text", placeholder: "State/Province", className: INPUT_CLASS, value: address.state ?? "", onChange: (e) => setAddress({ ...address, state: e.target.value }) }), _jsx("input", { type: "text", placeholder: "Postal code", className: INPUT_CLASS, value: address.postalCode ?? "", onChange: (e) => setAddress({ ...address, postalCode: e.target.value }) }), _jsx("input", { type: "text", placeholder: "Country", className: INPUT_CLASS, value: address.country ?? "", onChange: (e) => setAddress({ ...address, country: e.target.value }) }), _jsx("select", { className: SELECT_CLASS, value: address.type, onChange: (e) => setAddress({ ...address, type: e.target.value }), "aria-label": "Address type", children: ADDRESS_KINDS.map((kind) => (_jsx("option", { value: kind, children: kind }, kind))) })] }), _jsx("button", { type: "button", onClick: () => setAddress(null), className: "self-start text-xs font-medium text-danger hover:underline", children: "Remove address" })] })) : (_jsx("button", { type: "button", onClick: () => setAddress({ type: "home" }), className: "self-start text-xs font-medium text-primary-dark hover:underline", children: "+ Add address" })) }), _jsx(FormField, { label: "Notes", htmlFor: "contact-notes", children: _jsx("textarea", { id: "contact-notes", rows: 3, className: INPUT_CLASS, value: notes, onChange: (e) => setNotes(e.target.value) }) }), _jsxs("div", { className: "flex gap-3 mt-2", children: [_jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onCancel, children: "Cancel" })] })] }));
117
+ return (_jsxs("form", { onSubmit: handleSubmit, className: "max-w-xl flex flex-col gap-1", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-3", children: contact ? "Edit contact" : "New contact" }), error && _jsx(Alert, { children: error }), !contact && mailboxes && mailboxes.length > 1 && (_jsx(FormField, { label: "Mailbox", htmlFor: "contact-mailbox", children: _jsx("select", { id: "contact-mailbox", className: `${SELECT_CLASS} w-full`, value: targetMailboxUid, onChange: (e) => setTargetMailboxUid(e.target.value), children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) }) })), _jsx(FormField, { label: "Display name", htmlFor: "contact-displayName", children: _jsx("input", { id: "contact-displayName", type: "text", className: INPUT_CLASS, value: displayName, onChange: (e) => setDisplayName(e.target.value) }) }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-3", children: [_jsx(FormField, { label: "First name", htmlFor: "contact-givenName", children: _jsx("input", { id: "contact-givenName", type: "text", className: INPUT_CLASS, value: givenName, onChange: (e) => setGivenName(e.target.value) }) }), _jsx(FormField, { label: "Last name", htmlFor: "contact-surname", children: _jsx("input", { id: "contact-surname", type: "text", className: INPUT_CLASS, value: surname, onChange: (e) => setSurname(e.target.value) }) })] }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-3", children: [_jsx(FormField, { label: "Company", htmlFor: "contact-company", children: _jsx("input", { id: "contact-company", type: "text", className: INPUT_CLASS, value: company, onChange: (e) => setCompany(e.target.value) }) }), _jsx(FormField, { label: "Job title", htmlFor: "contact-jobTitle", children: _jsx("input", { id: "contact-jobTitle", type: "text", className: INPUT_CLASS, value: jobTitle, onChange: (e) => setJobTitle(e.target.value) }) })] }), _jsxs("label", { className: "flex items-center gap-2 text-sm mb-4", children: [_jsx("input", { type: "checkbox", checked: favorite, onChange: (e) => setFavorite(e.target.checked) }), "Favorite"] }), _jsx(FormField, { label: "Categories (comma-separated)", htmlFor: "contact-categories", children: _jsx("input", { id: "contact-categories", type: "text", className: INPUT_CLASS, value: categoriesText, onChange: (e) => setCategoriesText(e.target.value), placeholder: "VIP, Work" }) }), _jsx(FormField, { label: "Email", htmlFor: "contact-emails", children: _jsxs("div", { id: "contact-emails", className: "flex flex-col gap-2", children: [emails.map((email, i) => (_jsxs("div", { className: "flex gap-2", children: [_jsx("input", { type: "email", className: `${INPUT_CLASS} flex-1`, value: email.address, onChange: (e) => updateEmail(i, { address: e.target.value }), "aria-label": `Email address ${i + 1}` }), _jsx("select", { className: SELECT_CLASS, value: email.type, onChange: (e) => updateEmail(i, { type: e.target.value }), "aria-label": `Email type ${i + 1}`, children: ADDRESS_KINDS.map((kind) => (_jsx("option", { value: kind, children: kind }, kind))) }), _jsx("button", { type: "button", onClick: () => removeEmail(i), className: "text-sm text-danger px-2", "aria-label": `Remove email ${i + 1}`, children: "\u00D7" })] }, i))), _jsx("button", { type: "button", onClick: () => setEmails((prev) => [...prev, { address: "", type: "work" }]), className: "self-start text-xs font-medium text-primary-dark hover:underline", children: "+ Add email" })] }) }), _jsx(FormField, { label: "Phone", htmlFor: "contact-phones", children: _jsxs("div", { id: "contact-phones", className: "flex flex-col gap-2", children: [phones.map((phone, i) => (_jsxs("div", { className: "flex gap-2", children: [_jsx("input", { type: "tel", className: `${INPUT_CLASS} flex-1`, value: phone.phoneNumber, onChange: (e) => updatePhone(i, { phoneNumber: e.target.value }), "aria-label": `Phone number ${i + 1}` }), _jsx("select", { className: SELECT_CLASS, value: phone.type, onChange: (e) => updatePhone(i, { type: e.target.value }), "aria-label": `Phone type ${i + 1}`, children: ADDRESS_KINDS.map((kind) => (_jsx("option", { value: kind, children: kind }, kind))) }), _jsx("button", { type: "button", onClick: () => removePhone(i), className: "text-sm text-danger px-2", "aria-label": `Remove phone ${i + 1}`, children: "\u00D7" })] }, i))), _jsx("button", { type: "button", onClick: () => setPhones((prev) => [...prev, { phoneNumber: "", type: "work" }]), className: "self-start text-xs font-medium text-primary-dark hover:underline", children: "+ Add phone" })] }) }), _jsx(FormField, { label: "Address", htmlFor: "contact-address", children: _jsxs("div", { className: "flex flex-col gap-3", children: [addresses.map((address, i) => (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-2", children: [_jsx("input", { type: "text", placeholder: "Street", className: INPUT_CLASS, value: address.street ?? "", onChange: (e) => updateAddress(i, { street: e.target.value }) }), _jsx("input", { type: "text", placeholder: "City", className: INPUT_CLASS, value: address.city ?? "", onChange: (e) => updateAddress(i, { city: e.target.value }) }), _jsx("input", { type: "text", placeholder: "State/Province", className: INPUT_CLASS, value: address.state ?? "", onChange: (e) => updateAddress(i, { state: e.target.value }) }), _jsx("input", { type: "text", placeholder: "Postal code", className: INPUT_CLASS, value: address.postalCode ?? "", onChange: (e) => updateAddress(i, { postalCode: e.target.value }) }), _jsx("input", { type: "text", placeholder: "Country", className: INPUT_CLASS, value: address.country ?? "", onChange: (e) => updateAddress(i, { country: e.target.value }) }), _jsx("select", { className: SELECT_CLASS, value: address.type, onChange: (e) => updateAddress(i, { type: e.target.value }), "aria-label": i === 0 ? "Address type" : `Address type ${i + 1}`, children: ADDRESS_KINDS.map((kind) => (_jsx("option", { value: kind, children: kind }, kind))) })] }), _jsx("button", { type: "button", onClick: () => removeAddress(i), className: "self-start text-xs font-medium text-danger hover:underline", "aria-label": i === 0 ? "Remove address" : `Remove address ${i + 1}`, children: "Remove address" })] }, i))), _jsx("button", { type: "button", onClick: () => setAddresses((prev) => [...prev, { type: "home" }]), className: "self-start text-xs font-medium text-primary-dark hover:underline", children: "+ Add address" })] }) }), _jsx(FormField, { label: "Notes", htmlFor: "contact-notes", children: _jsx("textarea", { id: "contact-notes", rows: 3, className: INPUT_CLASS, value: notes, onChange: (e) => setNotes(e.target.value) }) }), _jsxs("div", { className: "flex gap-3 mt-2", children: [_jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onCancel, children: "Cancel" })] })] }));
96
118
  }
@@ -0,0 +1,40 @@
1
+ import React from "react";
2
+ import { Contact } from "@rapidmx/react-shared/contacts/contactsApi.js";
3
+ export type ContactsView = {
4
+ type: "all";
5
+ } | {
6
+ type: "favorites";
7
+ } | {
8
+ type: "list";
9
+ uid: string;
10
+ name: string;
11
+ } | {
12
+ type: "deleted";
13
+ } | {
14
+ type: "category";
15
+ name: string;
16
+ };
17
+ export declare function contactsViewKey(view: ContactsView): string;
18
+ export interface ContactsSidebarProps {
19
+ mailboxUid?: string;
20
+ /** The caller's own (non-deleted) contacts — used to compute counts and the distinct category list.
21
+ * Deliberately not fetched again here; the parent already has this loaded for the main table. */
22
+ contacts: Contact[];
23
+ active: ContactsView;
24
+ onSelect: (view: ContactsView) => void;
25
+ /** Whether to offer the "Deleted" view - restapi only honors `deleted=true` for a caller with delete and
26
+ * update rights on the mailbox (anyone else silently gets the live contacts), so the parent hides it
27
+ * otherwise. Defaults to `true`. */
28
+ showDeleted?: boolean;
29
+ /** Bumped by the parent whenever a contact-list membership changes, so a newly-created/renamed list
30
+ * shows up without a full page reload. */
31
+ refreshToken?: number;
32
+ }
33
+ /**
34
+ * Contacts' Outlook-style left sidebar: "Your contacts" (all, with count), "Favorites", "Your contact
35
+ * lists" (real `ContactList` records, fetched here), "Deleted", and "Categories" (derived from the
36
+ * distinct `categories` values across the loaded contacts — no separate category registry exists
37
+ * server-side). Owns its own contact-list data-fetching, per this app's convention of keeping
38
+ * `ContactsShellContext` itself minimal (mailbox/folder resolution only).
39
+ */
40
+ export default function ContactsSidebar({ mailboxUid, contacts, active, onSelect, refreshToken, showDeleted }: ContactsSidebarProps): React.JSX.Element;
@@ -43,7 +43,7 @@ function NavItem({ label, count, active, onClick, swatch, }) {
43
43
  * server-side). Owns its own contact-list data-fetching, per this app's convention of keeping
44
44
  * `ContactsShellContext` itself minimal (mailbox/folder resolution only).
45
45
  */
46
- export default function ContactsSidebar({ mailboxUid, contacts, active, onSelect, refreshToken }) {
46
+ export default function ContactsSidebar({ mailboxUid, contacts, active, onSelect, refreshToken, showDeleted = true }) {
47
47
  const [lists, setLists] = useState([]);
48
48
  const [listsError, setListsError] = useState(null);
49
49
  const [addingList, setAddingList] = useState(false);
@@ -100,6 +100,6 @@ export default function ContactsSidebar({ mailboxUid, contacts, active, onSelect
100
100
  // A function, not a plain JSX constant — rendered both in the desktop `<nav>` and the mobile
101
101
  // `Drawer` (see MailShell/ContactsShell for the identical pattern and the duplicate-id reasoning
102
102
  // behind the `idPrefix`).
103
- const navContent = (idPrefix) => (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx(NavItem, { label: "Your contacts", count: contacts.length, active: active.type === "all", onClick: () => handleSelect({ type: "all" }) }), _jsx(NavItem, { label: "Favorites", count: favoriteCount, active: active.type === "favorites", onClick: () => handleSelect({ type: "favorites" }) }), _jsx(NavItem, { label: "Deleted", active: active.type === "deleted", onClick: () => handleSelect({ type: "deleted" }) })] }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between px-2.5 mb-1", children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted", children: "Your contact lists" }), _jsx("button", { type: "button", onClick: () => setAddingList(true), "aria-label": "New contact list", className: "text-xs font-bold text-primary-dark hover:underline", children: "+" })] }), listsError && _jsx("p", { className: "px-2.5 text-xs text-danger", children: listsError }), _jsx("div", { className: "flex flex-col gap-0.5", children: lists.map((list) => (_jsx(NavItem, { label: list.name, count: listCounts.get(list.uid) ?? 0, active: active.type === "list" && active.uid === list.uid, onClick: () => handleSelect({ type: "list", uid: list.uid, name: list.name }) }, list.uid))) }), addingList && (_jsxs("form", { onSubmit: handleAddList, className: "flex items-center gap-1 px-2.5 mt-1", children: [_jsx("input", { type: "text", autoFocus: true, "aria-label": "New list name", id: `${idPrefix}-new-list-name`, value: newListName, onChange: (e) => setNewListName(e.target.value), className: "flex-1 text-xs py-1 px-1.5 border border-border rounded-sm bg-surface" }), _jsx("button", { type: "submit", className: "text-xs font-semibold text-primary-dark", children: "Add" })] }))] }), categories.length > 0 && (_jsxs("div", { children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted px-2.5 mb-1", children: "Categories" }), _jsx("div", { className: "flex flex-col gap-0.5", children: categories.map((name) => (_jsx(NavItem, { label: name, swatch: categoryColor(name), active: active.type === "category" && active.name === name, onClick: () => handleSelect({ type: "category", name }) }, name))) })] }))] }));
103
+ const navContent = (idPrefix) => (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx(NavItem, { label: "Your contacts", count: contacts.length, active: active.type === "all", onClick: () => handleSelect({ type: "all" }) }), _jsx(NavItem, { label: "Favorites", count: favoriteCount, active: active.type === "favorites", onClick: () => handleSelect({ type: "favorites" }) }), showDeleted && _jsx(NavItem, { label: "Deleted", active: active.type === "deleted", onClick: () => handleSelect({ type: "deleted" }) })] }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between px-2.5 mb-1", children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted", children: "Your contact lists" }), _jsx("button", { type: "button", onClick: () => setAddingList(true), "aria-label": "New contact list", className: "text-xs font-bold text-primary-dark hover:underline", children: "+" })] }), listsError && _jsx("p", { className: "px-2.5 text-xs text-danger", children: listsError }), _jsx("div", { className: "flex flex-col gap-0.5", children: lists.map((list) => (_jsx(NavItem, { label: list.name, count: listCounts.get(list.uid) ?? 0, active: active.type === "list" && active.uid === list.uid, onClick: () => handleSelect({ type: "list", uid: list.uid, name: list.name }) }, list.uid))) }), addingList && (_jsxs("form", { onSubmit: handleAddList, className: "flex items-center gap-1 px-2.5 mt-1", children: [_jsx("input", { type: "text", autoFocus: true, "aria-label": "New list name", id: `${idPrefix}-new-list-name`, value: newListName, onChange: (e) => setNewListName(e.target.value), className: "flex-1 text-xs py-1 px-1.5 border border-border rounded-sm bg-surface" }), _jsx("button", { type: "submit", className: "text-xs font-semibold text-primary-dark", children: "Add" })] }))] }), categories.length > 0 && (_jsxs("div", { children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted px-2.5 mb-1", children: "Categories" }), _jsx("div", { className: "flex flex-col gap-0.5", children: categories.map((name) => (_jsx(NavItem, { label: name, swatch: categoryColor(name), active: active.type === "category" && active.name === name, onClick: () => handleSelect({ type: "category", name }) }, name))) })] }))] }));
104
104
  return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "md:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open contacts menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("nav", { "aria-label": "Contacts", className: "hidden md:flex w-56 shrink-0 border-r border-border flex-col gap-4 p-3 overflow-y-auto", children: navContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Contacts", children: _jsx("div", { className: "flex flex-col gap-4", children: navContent("mobile") }) })] }));
105
105
  }
@@ -0,0 +1,25 @@
1
+ import React from "react";
2
+ export interface ContactsToolbarProps {
3
+ selectedCount: number;
4
+ /** `true` only when every currently-selected contact is already favorited — flips the "favorite" button
5
+ * to act (and read) as a removal instead of an addition. */
6
+ allSelectedFavorited: boolean;
7
+ onNewContact: () => void;
8
+ onEdit: () => void;
9
+ onDelete: () => void;
10
+ onEmail: () => void;
11
+ onToggleFavorite: () => void;
12
+ onAddCategory: () => void;
13
+ onExportVCard: () => void;
14
+ /** A `.vcf` file the user picked via the toolbar's own hidden file input — parsing/import is the
15
+ * page's responsibility (it has the mailbox/folder context and the actual `createContact` calls). */
16
+ onImportFile: (file: File) => void;
17
+ }
18
+ /**
19
+ * Contacts' Outlook-style ribbon toolbar. Deliberately presentational — every action is a callback the
20
+ * page (`apps/www/contacts/index.tsx`) implements, since it owns the actual contact data, the current
21
+ * selection, and the mailbox/folder context every real action (create/delete/email/favorite/category)
22
+ * needs. "New contact list" isn't duplicated here — `ContactsSidebar` already has its own inline
23
+ * "+ New list" affordance next to the "Your contact lists" heading.
24
+ */
25
+ export default function ContactsToolbar({ selectedCount, allSelectedFavorited, onNewContact, onEdit, onDelete, onEmail, onToggleFavorite, onAddCategory, onExportVCard, onImportFile, }: ContactsToolbarProps): React.JSX.Element;
@@ -0,0 +1,40 @@
1
+ import React from "react";
2
+ import { type KeyConflict } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
3
+ export interface KeyChangeReviewProps {
4
+ /** The mailbox whose contact holds the pinned key. */
5
+ mailboxUid: string;
6
+ /** The contact's address `resolveKeyConflict()` looks the contact up by; absent hides the actions. */
7
+ address?: string;
8
+ useType: "sign" | "encrypt";
9
+ /** Who the key belongs to, for the advice ("confirm with <name>"). */
10
+ ownerName: string;
11
+ /** The currently pinned key and when it was first trusted; absent (couldn't be loaded) hides the actions. */
12
+ current?: {
13
+ fingerprint: string;
14
+ since: number;
15
+ };
16
+ /** The new key: its fingerprint, the addresses its certificate names, and when and how it was recorded (absent when
17
+ * it's only known from the message being read). */
18
+ proposed: {
19
+ fingerprint: string;
20
+ emails?: string[];
21
+ observedAt?: number;
22
+ source?: KeyConflict["source"];
23
+ };
24
+ /** For accepting a key seen in a message: its certificate. Omitted, accepting pins the recorded conflict's key. */
25
+ certificate?: string;
26
+ /** Whether "Keep current key" applies - only when a conflict for this exact key is recorded. */
27
+ canReject: boolean;
28
+ /** `false` when the reader is known to lack the rights to change the contact's keys. */
29
+ canResolve?: boolean;
30
+ /** After a successful accept or reject - the caller refreshes what it shows. */
31
+ onResolved: (action: "accept" | "reject") => void;
32
+ /** After a 409: the pinned key changed meanwhile. The caller reloads and says so (`KEY_CHANGE_STALE_MESSAGE`). */
33
+ onPinnedKeyChanged: () => void;
34
+ }
35
+ /**
36
+ * The old-versus-new comparison for a changed contact key, with Accept new key (confirmed in a dialog) and Keep current
37
+ * key, calling `resolveKeyConflict()` against the pinned fingerprint shown. Shared by a message's "signing key changed"
38
+ * notice and a contact's recorded key conflicts.
39
+ */
40
+ export default function KeyChangeReview({ mailboxUid, address, useType, ownerName, current, proposed, certificate, canReject, canResolve, onResolved, onPinnedKeyChanged, }: KeyChangeReviewProps): React.JSX.Element;
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useState } from "react";
7
+ import { PinnedKeyChangedError, resolveKeyConflict } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
8
+ import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
9
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
10
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
11
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
12
+ import { conflictSourceLabel, formatDate, groupFingerprint, keyChangeErrorMessage } from "./contactKeys.js";
13
+ /**
14
+ * The old-versus-new comparison for a changed contact key, with Accept new key (confirmed in a dialog) and Keep current
15
+ * key, calling `resolveKeyConflict()` against the pinned fingerprint shown. Shared by a message's "signing key changed"
16
+ * notice and a contact's recorded key conflicts.
17
+ */
18
+ export default function KeyChangeReview({ mailboxUid, address, useType, ownerName, current, proposed, certificate, canReject, canResolve, onResolved, onPinnedKeyChanged, }) {
19
+ const [confirmOpen, setConfirmOpen] = useState(false);
20
+ const [busy, setBusy] = useState(null);
21
+ const [error, setError] = useState(null);
22
+ const [forbidden, setForbidden] = useState(false);
23
+ const kind = useType === "sign" ? "signing" : "encryption";
24
+ const actionable = !!address && !!current && canResolve !== false && !forbidden;
25
+ async function resolve(action) {
26
+ setBusy(action);
27
+ setError(null);
28
+ try {
29
+ await resolveKeyConflict(mailboxUid, {
30
+ address: address,
31
+ useType,
32
+ action,
33
+ expectedPinnedFingerprint: current.fingerprint,
34
+ ...(action === "accept" && certificate !== undefined ? { certificate } : {}),
35
+ });
36
+ setConfirmOpen(false);
37
+ onResolved(action);
38
+ }
39
+ catch (err) {
40
+ setConfirmOpen(false);
41
+ if (err instanceof PinnedKeyChangedError) {
42
+ onPinnedKeyChanged();
43
+ return;
44
+ }
45
+ if (err instanceof ApiRequestError && err.status === 403) {
46
+ setForbidden(true);
47
+ }
48
+ setError(keyChangeErrorMessage(err));
49
+ }
50
+ finally {
51
+ setBusy(null);
52
+ }
53
+ }
54
+ const proposedSeen = proposed.observedAt !== undefined
55
+ ? `First seen ${formatDate(proposed.observedAt)}${proposed.source ? `, ${conflictSourceLabel(proposed.source)}` : ""}`
56
+ : "Only seen in this message";
57
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-2", children: [_jsxs("div", { className: "rounded-sm bg-surface p-2", children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Current key" }), current ? (_jsxs("dd", { children: [_jsx("span", { className: "font-mono text-xs break-all", children: groupFingerprint(current.fingerprint) }), _jsxs("span", { className: "block text-xs text-text-muted", children: ["First seen ", formatDate(current.since)] })] })) : (_jsx("dd", { className: "text-xs text-text-muted", children: "The key you trust couldn\u2019t be loaded." }))] }), _jsxs("div", { className: "rounded-sm bg-surface p-2", children: [_jsx("dt", { className: "text-xs text-text-muted", children: "New key" }), _jsxs("dd", { children: [_jsx("span", { className: "font-mono text-xs break-all", children: groupFingerprint(proposed.fingerprint) }), proposed.emails && (_jsxs("span", { className: "block text-xs text-text-muted", children: ["Certificate for ", proposed.emails.length > 0 ? proposed.emails.join(", ") : "no email address"] })), _jsx("span", { className: "block text-xs text-text-muted", children: proposedSeen })] })] })] }), _jsxs("p", { children: ["This is routine when ", ownerName, " renews a certificate, but it can also mean someone is impersonating them. Confirm the new fingerprint with ", ownerName, " another way, such as a phone call, before accepting it."] }), error && _jsx(Alert, { children: error }), actionable && (_jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { type: "button", className: "!w-auto", disabled: busy !== null, onClick: () => {
58
+ setError(null);
59
+ setConfirmOpen(true);
60
+ }, children: "Accept new key" }), canReject && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: busy === "reject", disabled: busy !== null, onClick: () => resolve("reject"), children: "Keep current key" }))] })), _jsx(Modal, { open: confirmOpen, onClose: () => busy === null && setConfirmOpen(false), title: "Accept the new key?", children: _jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsxs("p", { children: [useType === "sign"
61
+ ? `Mail from ${address} signed with this key will show as verified.`
62
+ : `Mail you send to ${address} will be encrypted to this key.`, " ", "The current ", kind, " key moves to this contact\u2019s key history."] }), _jsxs("dl", { className: "flex flex-col gap-1", children: [_jsx("dt", { className: "text-xs text-text-muted", children: "New key fingerprint" }), _jsx("dd", { className: "font-mono text-xs break-all", children: groupFingerprint(proposed.fingerprint) })] }), _jsxs("p", { className: "text-text-muted", children: ["Only accept it once you\u2019ve confirmed this fingerprint with ", ownerName, "."] }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "button", className: "!w-auto", loading: busy === "accept", disabled: busy !== null, onClick: () => resolve("accept"), children: "Accept new key" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: busy !== null, onClick: () => setConfirmOpen(false), children: "Cancel" })] })] }) })] }));
63
+ }
@@ -0,0 +1,29 @@
1
+ import type { Contact } from "@rapidmx/react-shared/contacts/contactsApi.js";
2
+ import { type KeyConflict, type PublicKey } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
3
+ export declare const KEY_CHANGE_STALE_MESSAGE = "This contact's keys changed while you were looking; reload to see the latest.";
4
+ export declare const KEY_CHANGE_INVALID_MESSAGE = "The new key isn't a valid, current certificate for this address, so it can't be accepted. Nothing was changed.";
5
+ export declare const KEY_CHANGE_FORBIDDEN_MESSAGE = "You don't have permission to change this contact's keys.";
6
+ export declare const KEY_CHANGE_NOT_FOUND_MESSAGE = "There's no key change to resolve for this address any more. It may already have been resolved, or the contact was removed.";
7
+ export declare const KEY_CHANGE_GENERIC_MESSAGE = "Couldn't update this contact's keys. Try again.";
8
+ /** The error text for a failed `resolveKeyConflict()` other than `PinnedKeyChangedError` (which the caller reloads
9
+ * on and reports with `KEY_CHANGE_STALE_MESSAGE`). */
10
+ export declare function keyChangeErrorMessage(err: unknown): string;
11
+ /** Groups a fingerprint into 4-character blocks (`ab12 cd34 ...`), ignoring any separators it came with, so it can be
12
+ * read out and compared over another channel. */
13
+ export declare function groupFingerprint(fingerprint: string): string;
14
+ /** Whether two fingerprints name the same key (case and separators ignored). */
15
+ export declare function sameFingerprint(a: string | undefined, b: string | undefined): boolean;
16
+ /** A revoked key's label: `"superseded"` for a routine replacement, `"revoked"` for a compromised key or one revoked
17
+ * with no reason; `undefined` when the key isn't revoked. */
18
+ export declare function revocationLabel(key: PublicKey): "superseded" | "revoked" | undefined;
19
+ /** Where a key conflict was observed, for display. */
20
+ export declare function conflictSourceLabel(source: KeyConflict["source"]): string;
21
+ /** restapi records `keysFirstSeen` on a contact but react-shared's `Contact` type doesn't carry it. */
22
+ type ContactWithFirstSeen = Pick<Contact, "previousKeys"> & {
23
+ keysFirstSeen?: number;
24
+ };
25
+ /** When `key` (one of `contact`'s pinned keys) started being trusted: when it replaced the newest previous key of its
26
+ * use, else the contact's `keysFirstSeen`, else the key's own `notBefore`. */
27
+ export declare function keyPinnedSince(contact: ContactWithFirstSeen, key: PublicKey): number;
28
+ export declare function formatDate(ms: number): string;
29
+ export {};
@@ -0,0 +1,64 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /** Display and error helpers for a contact's pinned keys and key changes (`KeyChangeReview`, `ContactDetailPane`,
6
+ * `MessageDetailPane`). */
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import { PinnedKeyChangedError } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
9
+ export const KEY_CHANGE_STALE_MESSAGE = "This contact's keys changed while you were looking; reload to see the latest.";
10
+ export const KEY_CHANGE_INVALID_MESSAGE = "The new key isn't a valid, current certificate for this address, so it can't be accepted. Nothing was changed.";
11
+ export const KEY_CHANGE_FORBIDDEN_MESSAGE = "You don't have permission to change this contact's keys.";
12
+ export const KEY_CHANGE_NOT_FOUND_MESSAGE = "There's no key change to resolve for this address any more. It may already have been resolved, or the contact was removed.";
13
+ export const KEY_CHANGE_GENERIC_MESSAGE = "Couldn't update this contact's keys. Try again.";
14
+ /** The error text for a failed `resolveKeyConflict()` other than `PinnedKeyChangedError` (which the caller reloads
15
+ * on and reports with `KEY_CHANGE_STALE_MESSAGE`). */
16
+ export function keyChangeErrorMessage(err) {
17
+ if (err instanceof PinnedKeyChangedError) {
18
+ return KEY_CHANGE_STALE_MESSAGE;
19
+ }
20
+ if (err instanceof ApiRequestError && err.status === 400) {
21
+ return KEY_CHANGE_INVALID_MESSAGE;
22
+ }
23
+ if (err instanceof ApiRequestError && err.status === 403) {
24
+ return KEY_CHANGE_FORBIDDEN_MESSAGE;
25
+ }
26
+ if (err instanceof ApiRequestError && err.status === 404) {
27
+ return KEY_CHANGE_NOT_FOUND_MESSAGE;
28
+ }
29
+ return KEY_CHANGE_GENERIC_MESSAGE;
30
+ }
31
+ /** Groups a fingerprint into 4-character blocks (`ab12 cd34 ...`), ignoring any separators it came with, so it can be
32
+ * read out and compared over another channel. */
33
+ export function groupFingerprint(fingerprint) {
34
+ const compact = fingerprint.replace(/[\s:]/g, "").toLowerCase();
35
+ return compact.match(/.{1,4}/g)?.join(" ") ?? compact;
36
+ }
37
+ /** Whether two fingerprints name the same key (case and separators ignored). */
38
+ export function sameFingerprint(a, b) {
39
+ return a !== undefined && b !== undefined && a.replace(/[\s:]/g, "").toLowerCase() === b.replace(/[\s:]/g, "").toLowerCase();
40
+ }
41
+ /** A revoked key's label: `"superseded"` for a routine replacement, `"revoked"` for a compromised key or one revoked
42
+ * with no reason; `undefined` when the key isn't revoked. */
43
+ export function revocationLabel(key) {
44
+ if (!key.revokedAt) {
45
+ return undefined;
46
+ }
47
+ return key.revocationReason === "superseded" ? "superseded" : "revoked";
48
+ }
49
+ /** Where a key conflict was observed, for display. */
50
+ export function conflictSourceLabel(source) {
51
+ return source === "header" ? "from an incoming message" : "by key discovery";
52
+ }
53
+ /** When `key` (one of `contact`'s pinned keys) started being trusted: when it replaced the newest previous key of its
54
+ * use, else the contact's `keysFirstSeen`, else the key's own `notBefore`. */
55
+ export function keyPinnedSince(contact, key) {
56
+ const replaced = (contact.previousKeys ?? []).filter((previous) => previous.useType === key.useType).map((previous) => previous.replacedAt);
57
+ if (replaced.length > 0) {
58
+ return Math.max(...replaced);
59
+ }
60
+ return contact.keysFirstSeen ?? key.notBefore;
61
+ }
62
+ export function formatDate(ms) {
63
+ return new Date(ms).toLocaleDateString();
64
+ }
@@ -0,0 +1,21 @@
1
+ import React, { PropsWithChildren } from "react";
2
+ import { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
3
+ import { AppShellProps } from "../../layout/AppShell.js";
4
+ export type ContactsShellProps = Omit<AppShellProps, "active">;
5
+ export interface ContactsShellContextValue {
6
+ /** The mailbox currently selected (`?mailboxUid=`, or the caller's first accessible mailbox). */
7
+ mailboxUid?: string;
8
+ /** The selected mailbox's single `contacts`-type folder. */
9
+ folderUid?: string;
10
+ mailboxes: Mailbox[];
11
+ }
12
+ /** Reads the mailbox/folder Contacts is currently showing, as resolved by the enclosing `ContactsShell`. */
13
+ export declare function useContactsShell(): ContactsShellContextValue;
14
+ /**
15
+ * The Contacts app's shell: unlike `MailShell` (a folder tree, since a mailbox has many mail folders),
16
+ * a mailbox has exactly one well-known `contacts` folder — see `BaseMailboxRoute.create()`'s eager
17
+ * provisioning in `@rapidmx/restapi`, which guarantees it exists for every mailbox. This shell's own
18
+ * sidebar is therefore just a mailbox switcher (when the caller has more than one); the actual contact
19
+ * list/detail UI is `apps/www/contacts/index.tsx`'s own content, rendered as `children` here.
20
+ */
21
+ export default function ContactsShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }: PropsWithChildren<ContactsShellProps>): React.JSX.Element;
@@ -24,7 +24,7 @@ export function useContactsShell() {
24
24
  * sidebar is therefore just a mailbox switcher (when the caller has more than one); the actual contact
25
25
  * list/detail UI is `apps/www/contacts/index.tsx`'s own content, rendered as `children` here.
26
26
  */
27
- export default function ContactsShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, children, }) {
27
+ export default function ContactsShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }) {
28
28
  const [status, setStatus] = useState("checking");
29
29
  const [error, setError] = useState(null);
30
30
  const [mailboxes, setMailboxes] = useState([]);
@@ -89,5 +89,5 @@ export default function ContactsShell({ userUid, authServerUrl, impersonating, i
89
89
  }, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), folderError && _jsx(Alert, { children: folderError })] }));
90
90
  inner = (_jsxs(_Fragment, { children: [_jsx("aside", { className: "hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Mailbox", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [(mailboxes.length > 1 || folderError) && (_jsx("button", { type: "button", className: "md:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open mailbox switcher", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) })), _jsx(ContactsShellContext.Provider, { value: contextValue, children: children })] })] }));
91
91
  }
92
- return (_jsx(AppShell, { active: "contacts", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, children: inner }));
92
+ return (_jsx(AppShell, { active: "contacts", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, pluginNav: pluginNav, children: inner }));
93
93
  }
@@ -0,0 +1,25 @@
1
+ import "../../../styles/app.css";
2
+ import React, { PropsWithChildren } from "react";
3
+ export type EscrowSection = "matters" | "auditLog";
4
+ export interface EscrowShellProps {
5
+ active: EscrowSection;
6
+ userUid?: string;
7
+ authServerUrl?: string;
8
+ }
9
+ /**
10
+ * Gates every `apps/escrow` page behind "is this an authenticated caller at all" — deliberately **not**
11
+ * the same "does a canary endpoint 403 me" pattern `AdminShell` uses for the trusted `admin` role.
12
+ * `specs/end-to-end_encryption.md`'s "Separation of duties" makes holder-ness a per-`EscrowScope` grant
13
+ * (`EscrowScope.holderUserUids`), checked directly against `holderUserUids` in every restapi route that
14
+ * needs it (`requireEscrowHolder()`) — never a role, and never global. There is no canary endpoint a
15
+ * non-holder gets a clean 403 from: `GET /escrow/matters` (`BaseMatterRoute.find()`) returns `200 []` for
16
+ * *any* authenticated caller who holds no scope at all, the same as it would for a holder of zero matters
17
+ * — so unlike `AdminShell`, this shell cannot distinguish "holds nothing" from "holds no scope at all" up
18
+ * front, and does not try to. It only confirms the caller is signed in and the API is reachable; every
19
+ * actual holder-gated action (create a Matter, approve/deny a request, read material) is enforced
20
+ * server-side and surfaces its own 403 `ApiRequestError` on the specific page that attempted it, same as
21
+ * any other gated action elsewhere in this codebase. A non-holder who reaches `/escrow` simply sees an
22
+ * empty Matters list and gets a real error if they try to act on something they don't hold — this is a
23
+ * deliberate, documented scope trim rather than an oversight (see this session's own report on it).
24
+ */
25
+ export default function EscrowShell({ active, userUid, authServerUrl, children }: PropsWithChildren<EscrowShellProps>): React.JSX.Element;
@@ -9,11 +9,12 @@ import { HiOutlineClipboardDocumentList, HiOutlineFolderOpen } from "react-icons
9
9
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
10
10
  import { listMatters } from "@rapidmx/react-shared/admin/mattersApi.js";
11
11
  import { useRedirectIfUnauthenticated } from "@rapidmx/react-shared/auth/session.js";
12
- import useBranding from "@rapidmx/react-shared/branding/useBranding.js";
12
+ import { getBranding } from "@rapidmx/react-shared/branding/brandingApi.js";
13
13
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
14
14
  import BottomTabBar from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
15
- import { BrandingFooter, BrandingHeader } from "../../layout/BrandingChrome.js";
16
15
  import UserMenu from "../../layout/UserMenu.js";
16
+ import { signOutOfConsole } from "../../admin/signOut.js";
17
+ const DEFAULT_ICON_SRC = "/images/logo.svg";
17
18
  const NAV_ITEMS = [
18
19
  { id: "matters", href: "/escrow", label: "Matters", icon: HiOutlineFolderOpen },
19
20
  { id: "auditLog", href: "/escrow/audit-log", label: "Audit Log", icon: HiOutlineClipboardDocumentList },
@@ -37,8 +38,25 @@ const NAV_ITEMS = [
37
38
  export default function EscrowShell({ active, userUid, authServerUrl, children }) {
38
39
  const [status, setStatus] = useState("checking");
39
40
  const [error, setError] = useState(null);
40
- const { branding, iconSrc } = useBranding();
41
+ const [iconSrc, setIconSrc] = useState(DEFAULT_ICON_SRC);
41
42
  useRedirectIfUnauthenticated(userUid, authServerUrl);
43
+ // Only the icon is taken from branding here. The escrow console deliberately skips `useBranding()`, which
44
+ // would inject the custom stylesheet, and never renders the admin-configured header/footer HTML - this
45
+ // console handles escrow key material, so no admin-supplied markup or CSS runs on it.
46
+ useEffect(() => {
47
+ let cancelled = false;
48
+ getBranding()
49
+ .then((branding) => {
50
+ if (!cancelled)
51
+ setIconSrc(branding.iconUrl || branding.logoUrl || DEFAULT_ICON_SRC);
52
+ })
53
+ .catch(() => {
54
+ // Decorative only - keep the default icon.
55
+ });
56
+ return () => {
57
+ cancelled = true;
58
+ };
59
+ }, []);
42
60
  useEffect(() => {
43
61
  if (!userUid) {
44
62
  return;
@@ -53,7 +71,8 @@ export default function EscrowShell({ active, userUid, authServerUrl, children }
53
71
  });
54
72
  }, [userUid]);
55
73
  function handleSignOut() {
56
- window.location.href = authServerUrl ?? "/";
74
+ // Ends the auth-server session and tells other tabs, not just navigates - see `signOutOfConsole()`.
75
+ void signOutOfConsole(authServerUrl);
57
76
  }
58
77
  let content;
59
78
  if (!userUid || status === "checking") {
@@ -71,5 +90,5 @@ export default function EscrowShell({ active, userUid, authServerUrl, children }
71
90
  : "text-text-muted hover:bg-surface-alt hover:text-text",
72
91
  ].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] }), _jsx(BottomTabBar, { apps: NAV_ITEMS, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeItem?.label }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut })] }), _jsx("div", { className: "flex-1 pb-14 md:pb-0", children: _jsx("main", { className: "max-w-6xl mx-auto px-6 py-8", children: children }) })] })] }) }));
73
92
  }
74
- return (_jsxs(_Fragment, { children: [_jsx(BrandingHeader, { branding: branding }), content, _jsx(BrandingFooter, { branding: branding })] }));
93
+ return _jsx(_Fragment, { children: content });
75
94
  }
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ export interface StringListFieldProps {
3
+ label: string;
4
+ /** Associates the label with the add-input via `htmlFor`/`id`, same as `FormField` — required so the
5
+ * field is reachable via `getByLabelText()` in tests and by assistive tech, not just visually. */
6
+ id: string;
7
+ values: string[];
8
+ onChange: (values: string[]) => void;
9
+ placeholder?: string;
10
+ emptyMessage?: string;
11
+ disabled?: boolean;
12
+ }
13
+ /**
14
+ * A plain, controlled add/remove list-of-strings editor — the same visual add-row/per-item-remove shape
15
+ * `MemberListCard` already established for a `DistributionList`'s `memberAddresses`, but operating on a
16
+ * local `value`/`onChange` pair rather than persisting itself via its own API call. `MemberListCard`
17
+ * isn't reusable here as-is: `EscrowScope.holderUserUids` (at least one required) and
18
+ * `Matter.custodianMailboxUids` (also non-empty) are both required *at creation time*, before there's any
19
+ * uid yet to `PUT` against — this needs to compose into an ordinary create/edit form's own local state
20
+ * instead, same as every other field on those forms.
21
+ */
22
+ export default function StringListField({ label, id, values, onChange, placeholder, emptyMessage, disabled, }: StringListFieldProps): React.JSX.Element;