@rapidmx/web-client 0.30.0 → 0.31.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 (400) hide show
  1. package/apps/admin/audit-log/index.tsx +3 -3
  2. package/apps/admin/branding/index.tsx +3 -3
  3. package/apps/admin/data-requests/index.tsx +18 -7
  4. package/apps/admin/distribution-lists/[uid].tsx +3 -3
  5. package/apps/admin/distribution-lists/index.tsx +3 -3
  6. package/apps/admin/distribution-lists/new/index.tsx +3 -3
  7. package/apps/admin/domains/[uid].tsx +6 -5
  8. package/apps/admin/domains/index.tsx +3 -3
  9. package/apps/admin/domains/new/index.tsx +3 -3
  10. package/apps/admin/escrow-scopes/[uid].tsx +11 -5
  11. package/apps/admin/escrow-scopes/index.tsx +3 -3
  12. package/apps/admin/escrow-scopes/new/index.tsx +9 -3
  13. package/apps/admin/index.tsx +4 -4
  14. package/apps/admin/ingest-queue/index.tsx +3 -3
  15. package/apps/admin/mailboxes/[uid].tsx +15 -7
  16. package/apps/admin/quarantine/index.tsx +4 -4
  17. package/apps/admin/retention-policy/index.tsx +3 -3
  18. package/apps/admin/signing-certificates/index.tsx +3 -3
  19. package/apps/admin/transport-rules/[uid].tsx +4 -4
  20. package/apps/admin/transport-rules/index.tsx +3 -3
  21. package/apps/admin/transport-rules/new/index.tsx +3 -3
  22. package/apps/escrow/matters/[uid].tsx +26 -2
  23. package/apps/escrow/matters/new/index.tsx +4 -0
  24. package/apps/shared/calendar/calendarLiveUpdates.ts +11 -10
  25. package/apps/shared/calendar/useCalendarReminders.ts +2 -0
  26. package/apps/shared/components/admin/ActionAlert.tsx +32 -0
  27. package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +1 -1
  28. package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +1 -1
  29. package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +1 -1
  30. package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +1 -1
  31. package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +1 -1
  32. package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
  33. package/apps/shared/components/admin/elevation.ts +19 -0
  34. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
  35. package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
  36. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
  37. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
  38. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
  39. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  40. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
  41. package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
  42. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
  43. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
  44. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
  45. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
  46. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
  47. package/apps/shared/components/admin/settings/PluginsManager.tsx +39 -17
  48. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
  49. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
  50. package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
  51. package/apps/shared/components/admin/signOut.ts +3 -0
  52. package/apps/shared/components/admin/usePagedList.tsx +2 -2
  53. package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
  54. package/apps/shared/components/calendar/EventDetails.tsx +34 -8
  55. package/apps/shared/components/calendar/EventEditor.tsx +90 -40
  56. package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
  57. package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
  58. package/apps/shared/components/calendar/EventModal.tsx +25 -1
  59. package/apps/shared/components/calendar/FindATime.tsx +10 -2
  60. package/apps/shared/components/calendar/ListView.tsx +99 -25
  61. package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
  62. package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
  63. package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
  64. package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
  65. package/apps/shared/components/calendar/gridLayout.ts +94 -0
  66. package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
  67. package/apps/shared/components/calendar/timePicker.ts +5 -2
  68. package/apps/shared/components/contacts/ContactCard.tsx +15 -4
  69. package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
  70. package/apps/shared/components/contacts/ContactForm.tsx +61 -17
  71. package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
  72. package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
  73. package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
  74. package/apps/shared/components/contacts/participantDetails.ts +18 -1
  75. package/apps/shared/components/layout/AppShell.tsx +9 -0
  76. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
  77. package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
  78. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
  79. package/apps/shared/components/mail/ConversationList.tsx +49 -6
  80. package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
  81. package/apps/shared/components/mail/MenuButton.tsx +3 -400
  82. package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
  83. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
  84. package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
  85. package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
  86. package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
  87. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
  88. package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
  89. package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
  90. package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
  91. package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
  92. package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
  93. package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
  94. package/apps/shared/components/mail/writableMailboxes.ts +2 -0
  95. package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
  96. package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
  97. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
  98. package/apps/shared/components/tasks/dueDate.ts +29 -0
  99. package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
  100. package/apps/shared/mail/decryptedMessages.ts +51 -2
  101. package/apps/shared/mail/listSnapshots.ts +2 -0
  102. package/apps/shared/mail/outbox/sendDecision.ts +14 -3
  103. package/apps/shared/mail/outbox/sendJob.ts +5 -0
  104. package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
  105. package/apps/shared/mail/usePermanentDelete.tsx +3 -1
  106. package/apps/shared/notifications/apiErrors.ts +2 -0
  107. package/apps/shared/notifications/preferences.ts +1 -1
  108. package/apps/shared/notifications/store.ts +3 -1
  109. package/apps/shared/plugins/pluginNav.ts +4 -2
  110. package/apps/www/calendar/index.tsx +24 -12
  111. package/apps/www/contacts/[uid].tsx +6 -1
  112. package/apps/www/contacts/index.tsx +66 -12
  113. package/apps/www/index.tsx +91 -28
  114. package/apps/www/messages/[uid].tsx +8 -3
  115. package/apps/www/settings/auto-reply/index.tsx +13 -0
  116. package/apps/www/settings/blocked-senders/index.tsx +4 -0
  117. package/apps/www/settings/encryption/index.tsx +66 -4
  118. package/apps/www/settings/privacy/index.tsx +4 -1
  119. package/apps/www/settings/profile/index.tsx +12 -1
  120. package/apps/www/settings/sharing/index.tsx +11 -1
  121. package/apps/www/settings/signatures/[uid].tsx +6 -4
  122. package/apps/www/tasks/index.tsx +97 -29
  123. package/dist/apps/admin/audit-log/index.js +3 -3
  124. package/dist/apps/admin/branding/index.js +3 -3
  125. package/dist/apps/admin/data-requests/index.js +17 -7
  126. package/dist/apps/admin/distribution-lists/[uid].js +3 -3
  127. package/dist/apps/admin/distribution-lists/index.js +3 -3
  128. package/dist/apps/admin/distribution-lists/new/index.js +3 -3
  129. package/dist/apps/admin/domains/[uid].js +6 -5
  130. package/dist/apps/admin/domains/index.js +3 -3
  131. package/dist/apps/admin/domains/new/index.js +3 -3
  132. package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
  133. package/dist/apps/admin/escrow-scopes/index.js +3 -3
  134. package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
  135. package/dist/apps/admin/index.js +4 -4
  136. package/dist/apps/admin/ingest-queue/index.js +3 -3
  137. package/dist/apps/admin/mailboxes/[uid].js +15 -7
  138. package/dist/apps/admin/quarantine/index.js +4 -4
  139. package/dist/apps/admin/retention-policy/index.js +3 -3
  140. package/dist/apps/admin/signing-certificates/index.js +3 -3
  141. package/dist/apps/admin/transport-rules/[uid].js +4 -4
  142. package/dist/apps/admin/transport-rules/index.js +3 -3
  143. package/dist/apps/admin/transport-rules/new/index.js +3 -3
  144. package/dist/apps/escrow/matters/[uid].js +8 -3
  145. package/dist/apps/escrow/matters/new/index.js +4 -0
  146. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
  147. package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
  148. package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
  149. package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
  150. package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
  151. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +1 -1
  152. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
  153. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
  154. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
  155. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
  156. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
  157. package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
  158. package/dist/apps/shared/components/admin/elevation.js +16 -0
  159. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
  160. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
  161. package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
  162. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
  163. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
  164. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
  165. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  166. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
  167. package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
  168. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
  169. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
  170. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
  171. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
  172. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
  173. package/dist/apps/shared/components/admin/settings/PluginsManager.js +26 -12
  174. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
  175. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
  176. package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
  177. package/dist/apps/shared/components/admin/signOut.js +3 -0
  178. package/dist/apps/shared/components/admin/usePagedList.js +2 -2
  179. package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
  180. package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
  181. package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
  182. package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
  183. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
  184. package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
  185. package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
  186. package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
  187. package/dist/apps/shared/components/calendar/EventModal.js +10 -2
  188. package/dist/apps/shared/components/calendar/FindATime.js +4 -3
  189. package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
  190. package/dist/apps/shared/components/calendar/ListView.js +92 -27
  191. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
  192. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
  193. package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
  194. package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
  195. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
  196. package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
  197. package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
  198. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
  199. package/dist/apps/shared/components/calendar/timePicker.js +5 -2
  200. package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
  201. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
  202. package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
  203. package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
  204. package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
  205. package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
  206. package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
  207. package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
  208. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
  209. package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
  210. package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
  211. package/dist/apps/shared/components/layout/AppShell.js +9 -0
  212. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
  213. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
  214. package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
  215. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
  216. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
  217. package/dist/apps/shared/components/mail/ConversationList.js +49 -6
  218. package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
  219. package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
  220. package/dist/apps/shared/components/mail/MenuButton.js +2 -211
  221. package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
  222. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
  223. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
  224. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
  225. package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
  226. package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
  227. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
  228. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
  229. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
  230. package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
  231. package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
  232. package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
  233. package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
  234. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
  235. package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
  236. package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
  237. package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
  238. package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
  239. package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
  240. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
  241. package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
  242. package/dist/apps/shared/components/tasks/dueDate.js +26 -0
  243. package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
  244. package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
  245. package/dist/apps/shared/mail/decryptedMessages.js +46 -2
  246. package/dist/apps/shared/mail/listSnapshots.js +2 -0
  247. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
  248. package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
  249. package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
  250. package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
  251. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
  252. package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
  253. package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
  254. package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
  255. package/dist/apps/shared/notifications/apiErrors.js +2 -0
  256. package/dist/apps/shared/notifications/preferences.d.ts +1 -1
  257. package/dist/apps/shared/notifications/preferences.js +1 -1
  258. package/dist/apps/shared/notifications/store.js +3 -1
  259. package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
  260. package/dist/apps/shared/plugins/pluginNav.js +4 -2
  261. package/dist/apps/www/calendar/index.js +24 -12
  262. package/dist/apps/www/contacts/[uid].js +6 -2
  263. package/dist/apps/www/contacts/index.js +62 -11
  264. package/dist/apps/www/index.js +85 -30
  265. package/dist/apps/www/messages/[uid].js +8 -3
  266. package/dist/apps/www/settings/auto-reply/index.js +13 -1
  267. package/dist/apps/www/settings/blocked-senders/index.js +4 -0
  268. package/dist/apps/www/settings/encryption/index.js +29 -5
  269. package/dist/apps/www/settings/privacy/index.js +3 -2
  270. package/dist/apps/www/settings/profile/index.js +4 -1
  271. package/dist/apps/www/settings/sharing/index.js +10 -1
  272. package/dist/apps/www/settings/signatures/[uid].js +6 -4
  273. package/dist/apps/www/tasks/index.js +94 -32
  274. package/dist/lib/admin/auditLogApi.js +2 -2
  275. package/dist/lib/admin/distributionListsApi.js +2 -2
  276. package/dist/lib/admin/domainsApi.js +2 -2
  277. package/dist/lib/admin/escrowAuditLogApi.js +2 -2
  278. package/dist/lib/admin/escrowScopesApi.js +2 -2
  279. package/dist/lib/admin/pluginsApi.d.ts +13 -2
  280. package/dist/lib/admin/pluginsApi.js +8 -0
  281. package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
  282. package/dist/lib/admin/retentionPolicyApi.js +3 -0
  283. package/dist/lib/admin/transportRulesApi.js +2 -2
  284. package/dist/lib/appearance/preferencesApi.d.ts +1 -1
  285. package/dist/lib/appearance/preferencesApi.js +3 -14
  286. package/dist/lib/auth/session.d.ts +51 -12
  287. package/dist/lib/auth/session.js +214 -38
  288. package/dist/lib/branding/brandingApi.d.ts +3 -3
  289. package/dist/lib/branding/brandingApi.js +9 -21
  290. package/dist/lib/calendar/calendarApi.js +3 -1
  291. package/dist/lib/calendar/eventSearch.js +43 -6
  292. package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
  293. package/dist/lib/calendar/freeBusyApi.js +13 -5
  294. package/dist/lib/calendar/recurrence.js +51 -3
  295. package/dist/lib/calendar/useDayKey.d.ts +7 -0
  296. package/dist/lib/calendar/useDayKey.js +43 -0
  297. package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
  298. package/dist/lib/components/avatar/ContactAvatar.js +37 -8
  299. package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
  300. package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
  301. package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
  302. package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
  303. package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
  304. package/dist/lib/components/forms/ImageEditBadge.js +62 -0
  305. package/dist/lib/components/menus/MenuButton.d.ts +78 -0
  306. package/dist/lib/components/menus/MenuButton.js +219 -0
  307. package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
  308. package/dist/lib/components/navigation/BottomTabBar.js +22 -3
  309. package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
  310. package/dist/lib/components/overlays/PopoverPortal.js +3 -2
  311. package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
  312. package/dist/lib/contacts/contactsApi.d.ts +5 -2
  313. package/dist/lib/contacts/contactsApi.js +16 -19
  314. package/dist/lib/contacts/gravatar.d.ts +14 -3
  315. package/dist/lib/contacts/gravatar.js +42 -5
  316. package/dist/lib/contacts/preparePhoto.d.ts +24 -0
  317. package/dist/lib/contacts/preparePhoto.js +250 -0
  318. package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
  319. package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
  320. package/dist/lib/contacts/vcard.d.ts +3 -0
  321. package/dist/lib/contacts/vcard.js +59 -7
  322. package/dist/lib/crypto/escrowKeys.d.ts +8 -0
  323. package/dist/lib/crypto/escrowKeys.js +20 -1
  324. package/dist/lib/crypto/messageSecurity.d.ts +7 -4
  325. package/dist/lib/crypto/messageSecurity.js +23 -5
  326. package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
  327. package/dist/lib/crypto/passwordUnlock.js +13 -2
  328. package/dist/lib/crypto/smime.js +12 -2
  329. package/dist/lib/mail/compose/composeQuoting.js +3 -2
  330. package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
  331. package/dist/lib/mail/compose/quotedImages.js +35 -9
  332. package/dist/lib/mail/labelsApi.js +2 -2
  333. package/dist/lib/mail/mailApi.d.ts +8 -5
  334. package/dist/lib/mail/mailApi.js +73 -39
  335. package/dist/lib/mail/mailDetailHooks.js +11 -3
  336. package/dist/lib/mail/mailFilterRulesApi.js +2 -2
  337. package/dist/lib/mail/mailSignaturesApi.js +2 -2
  338. package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
  339. package/dist/lib/mail/mailboxImportApi.js +3 -15
  340. package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
  341. package/dist/lib/mail/messageBodySanitizer.js +44 -1
  342. package/dist/lib/search/queryGrammar.js +12 -4
  343. package/dist/lib/search/searchTier3.d.ts +6 -1
  344. package/dist/lib/search/searchTier3.js +54 -25
  345. package/dist/lib/tasks/tasksApi.js +3 -3
  346. package/dist/lib/util/api.d.ts +26 -4
  347. package/dist/lib/util/api.js +73 -16
  348. package/dist/lib/util/apiQuery.d.ts +11 -0
  349. package/dist/lib/util/apiQuery.js +11 -0
  350. package/dist/lib/util/settleWithLimit.d.ts +5 -0
  351. package/dist/lib/util/settleWithLimit.js +25 -0
  352. package/lib/admin/auditLogApi.ts +2 -2
  353. package/lib/admin/distributionListsApi.ts +2 -2
  354. package/lib/admin/domainsApi.ts +2 -2
  355. package/lib/admin/escrowAuditLogApi.ts +2 -2
  356. package/lib/admin/escrowScopesApi.ts +2 -2
  357. package/lib/admin/pluginsApi.ts +21 -2
  358. package/lib/admin/retentionPolicyApi.ts +4 -0
  359. package/lib/admin/transportRulesApi.ts +2 -2
  360. package/lib/appearance/preferencesApi.ts +3 -14
  361. package/lib/auth/session.ts +239 -39
  362. package/lib/branding/brandingApi.ts +9 -21
  363. package/lib/calendar/calendarApi.ts +3 -1
  364. package/lib/calendar/eventSearch.ts +46 -6
  365. package/lib/calendar/freeBusyApi.ts +14 -6
  366. package/lib/calendar/recurrence.ts +59 -8
  367. package/lib/calendar/useDayKey.ts +45 -0
  368. package/lib/components/avatar/ContactAvatar.tsx +46 -8
  369. package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
  370. package/lib/components/forms/CameraCaptureModal.tsx +133 -0
  371. package/lib/components/forms/ImageEditBadge.tsx +121 -0
  372. package/lib/components/menus/MenuButton.tsx +408 -0
  373. package/lib/components/navigation/BottomTabBar.tsx +30 -8
  374. package/lib/components/overlays/PopoverPortal.tsx +3 -2
  375. package/lib/components/pickers/TimeZonePicker.tsx +17 -1
  376. package/lib/contacts/contactsApi.ts +18 -19
  377. package/lib/contacts/gravatar.ts +47 -5
  378. package/lib/contacts/preparePhoto.ts +273 -0
  379. package/lib/contacts/useContactPhotoSrc.ts +177 -0
  380. package/lib/contacts/vcard.ts +60 -7
  381. package/lib/crypto/escrowKeys.ts +21 -1
  382. package/lib/crypto/messageSecurity.ts +33 -9
  383. package/lib/crypto/passwordUnlock.ts +14 -2
  384. package/lib/crypto/smime.ts +13 -2
  385. package/lib/mail/compose/composeQuoting.ts +3 -2
  386. package/lib/mail/compose/quotedImages.ts +39 -9
  387. package/lib/mail/labelsApi.ts +2 -2
  388. package/lib/mail/mailApi.ts +96 -39
  389. package/lib/mail/mailDetailHooks.ts +11 -3
  390. package/lib/mail/mailFilterRulesApi.ts +2 -2
  391. package/lib/mail/mailSignaturesApi.ts +2 -2
  392. package/lib/mail/mailboxImportApi.ts +9 -15
  393. package/lib/mail/messageBodySanitizer.ts +47 -1
  394. package/lib/search/queryGrammar.ts +13 -4
  395. package/lib/search/searchTier3.ts +65 -32
  396. package/lib/tasks/tasksApi.ts +3 -3
  397. package/lib/util/api.ts +99 -16
  398. package/lib/util/apiQuery.ts +13 -0
  399. package/lib/util/settleWithLimit.ts +25 -0
  400. package/package.json +1 -1
@@ -0,0 +1,219 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useRef, useState } from "react";
7
+ import { HiCheck, HiChevronDown, HiChevronLeft, HiChevronRight, HiMinus } from "react-icons/hi2";
8
+ import PopoverPortal from "../overlays/PopoverPortal.js";
9
+ // `PopoverPortal` positions a fixed-size box (it has no auto-height mode), so the menu's height is
10
+ // computed from its own contents rather than measured - from whichever level is *shown*, so a submenu
11
+ // isn't left standing in the parent menu's taller box. These are the exact heights the classes below
12
+ // render at, so the box is never short enough to clip its last row: an item is `h-9`, a group label
13
+ // `h-6`, a note as many `leading-4` lines as it wraps to, a separator a 1px rule inside `my-1`, and the
14
+ // list itself `py-1`.
15
+ const ITEM_HEIGHT = 36;
16
+ const ITEM_DESCRIPTION_HEIGHT = 16;
17
+ const GROUP_LABEL_HEIGHT = 24;
18
+ const NOTE_LINE_HEIGHT = 16;
19
+ const NOTE_PADDING = 4;
20
+ /** A note wraps, so its height depends on how much of it fits a line: `text-xs` averages a little over 6px
21
+ * a character, and the note sits inside the list's `px-3`. Rounded so the estimate is never *under* the
22
+ * lines the browser actually draws - a box a few pixels too tall shows blank space, one too short clips. */
23
+ const NOTE_CHAR_WIDTH = 6.4;
24
+ const NOTE_PADDING_X = 24;
25
+ const SEPARATOR_HEIGHT = 9;
26
+ const LIST_PADDING = 8;
27
+ const MENU_MAX_HEIGHT = 460;
28
+ const DEFAULT_MENU_WIDTH = 248;
29
+ /** How tall `note` renders at `width`, wrapped. */
30
+ function noteHeight(note, width) {
31
+ const charsPerLine = Math.max(1, Math.floor((width - NOTE_PADDING_X) / NOTE_CHAR_WIDTH));
32
+ return Math.ceil(note.length / charsPerLine) * NOTE_LINE_HEIGHT + NOTE_PADDING;
33
+ }
34
+ /** The height `PopoverPortal` is asked for - exact for a short menu, capped (the list scrolls) for a long one. */
35
+ export function menuHeight(sections, width = DEFAULT_MENU_WIDTH) {
36
+ let height = LIST_PADDING;
37
+ sections.forEach((section, index) => {
38
+ if (index > 0) {
39
+ height += SEPARATOR_HEIGHT;
40
+ }
41
+ if (section.label) {
42
+ height += GROUP_LABEL_HEIGHT;
43
+ }
44
+ for (const item of section.items) {
45
+ height += ITEM_HEIGHT + (item.description ? ITEM_DESCRIPTION_HEIGHT : 0);
46
+ }
47
+ if (section.note) {
48
+ height += noteHeight(section.note, width);
49
+ }
50
+ });
51
+ return Math.min(height, MENU_MAX_HEIGHT);
52
+ }
53
+ /**
54
+ * A toolbar button that opens an ARIA menu: `role="menu"` rows with checkmarks for the current choice,
55
+ * full keyboard support (Enter/Space or Arrow Down to open, arrows/Home/End to move with wrap, Enter/Space
56
+ * to choose, Escape or Tab to close with focus returning to the trigger) and a click outside to dismiss.
57
+ *
58
+ * Rendered through `PopoverPortal` rather than as an absolutely-positioned child: the mail list toolbar
59
+ * lives inside the list's own `overflow-y-auto` scroll container, which clips an absolute popup however
60
+ * high its `z-index` - the same reason Compose's own pickers portal out (see `PopoverPortal`'s own doc
61
+ * comment). The portal supplies the positioning, the outside-click dismissal and the box; the menu
62
+ * semantics, roving focus and keyboard handling are this component's own.
63
+ */
64
+ export default function MenuButton({ label, iconOnly = false, icon, disabled, title, sections, width = DEFAULT_MENU_WIDTH, className = "", "aria-label": ariaLabel, onOpenChange, }) {
65
+ const [open, setOpen] = useState(false);
66
+ const [activeIndex, setActiveIndex] = useState(0);
67
+ // The key of the item whose submenu is showing, or `null` at the top level. Held as a key, not as the
68
+ // item itself, because `sections` is rebuilt on every render.
69
+ const [submenuKey, setSubmenuKey] = useState(null);
70
+ const triggerRef = useRef(null);
71
+ const itemRefs = useRef([]);
72
+ // State, not a ref: `PopoverPortal` renders nothing at all until its own positioning effect has run, so
73
+ // the rows don't exist yet when this component's effects first fire. Keying the focus effect on the
74
+ // menu node itself makes it run again once they do.
75
+ const [menuNode, setMenuNode] = useState(null);
76
+ const openSubmenu = sections
77
+ .flatMap((section) => section.items)
78
+ .find((item) => item.key === submenuKey && !!item.submenu);
79
+ /** The row that leaves a submenu again - synthesized rather than asked of the caller, so every submenu
80
+ * has the same way back however it was built. */
81
+ const backItem = {
82
+ key: "__back",
83
+ label: `Back to ${ariaLabel}`,
84
+ keepOpen: true,
85
+ onSelect: () => leaveSubmenu(),
86
+ };
87
+ const shownSections = openSubmenu
88
+ ? [{ key: "__back", items: [backItem] }, ...openSubmenu.submenu]
89
+ : sections;
90
+ const items = shownSections.flatMap((section) => section.items);
91
+ const enabledIndexes = items.map((item, index) => (item.disabled ? -1 : index)).filter((index) => index !== -1);
92
+ /** The row focus starts on: the current choice where there is one, else the first row that can take focus. */
93
+ function initialActiveIndex(within) {
94
+ const checked = within.findIndex((item) => item.checked === true && !item.disabled);
95
+ const enabled = within.map((item, index) => (item.disabled ? -1 : index)).filter((index) => index !== -1);
96
+ return checked === -1 ? (enabled[0] ?? 0) : checked;
97
+ }
98
+ function openMenu() {
99
+ setSubmenuKey(null);
100
+ setActiveIndex(initialActiveIndex(sections.flatMap((section) => section.items)));
101
+ setOpen(true);
102
+ onOpenChange?.(true);
103
+ }
104
+ function closeMenu(returnFocus) {
105
+ setOpen(false);
106
+ setSubmenuKey(null);
107
+ onOpenChange?.(false);
108
+ if (returnFocus) {
109
+ triggerRef.current?.focus();
110
+ }
111
+ }
112
+ function enterSubmenu(item) {
113
+ setSubmenuKey(item.key);
114
+ const rows = item.submenu.flatMap((section) => section.items);
115
+ // Past the Back row, onto the first row of the submenu itself - or, when none of them can take focus (every row is unavailable right now), onto
116
+ // the Back row, so the keyboard still has a place to be.
117
+ setActiveIndex(rows.some((row) => !row.disabled) ? 1 + initialActiveIndex(rows) : 0);
118
+ }
119
+ function leaveSubmenu() {
120
+ // Back on the row the submenu was opened from.
121
+ const parentIndex = sections.flatMap((section) => section.items).findIndex((item) => item.key === submenuKey);
122
+ setSubmenuKey(null);
123
+ setActiveIndex(parentIndex);
124
+ }
125
+ /** What a row does when it is chosen: open its submenu, or run it and close unless it asked to stay. */
126
+ function selectItem(item) {
127
+ if (item.submenu) {
128
+ enterSubmenu(item);
129
+ return;
130
+ }
131
+ item.onSelect();
132
+ if (!item.keepOpen) {
133
+ closeMenu(true);
134
+ }
135
+ }
136
+ // Roving focus: only the active row is tabbable, and it takes DOM focus whenever it changes, so the
137
+ // arrow keys move the screen reader's cursor too rather than only a visual highlight.
138
+ useEffect(() => {
139
+ if (open && menuNode) {
140
+ // A disabled row can't take focus, so a menu with nothing enabled focuses its own container -
141
+ // otherwise the keys it handles would go to whatever had focus before it opened.
142
+ if (enabledIndexes.length === 0) {
143
+ menuNode.focus();
144
+ }
145
+ else {
146
+ itemRefs.current[activeIndex]?.focus();
147
+ }
148
+ }
149
+ // `submenuKey` too: drilling in or out can land on the same index in the other level's list, and
150
+ // the row that index *means* is a different button, which the effect must move focus to.
151
+ }, [open, activeIndex, submenuKey, menuNode]);
152
+ function moveActive(delta) {
153
+ if (enabledIndexes.length === 0) {
154
+ return;
155
+ }
156
+ const position = enabledIndexes.indexOf(activeIndex);
157
+ setActiveIndex(enabledIndexes[(position + delta + enabledIndexes.length) % enabledIndexes.length]);
158
+ }
159
+ function handleMenuKeyDown(e) {
160
+ if (e.key === "Escape") {
161
+ // Not on to a `Modal` the menu was opened in: Escape closes the menu (or its submenu) only.
162
+ e.stopPropagation();
163
+ }
164
+ if (e.key === "ArrowDown") {
165
+ e.preventDefault();
166
+ moveActive(1);
167
+ }
168
+ else if (e.key === "ArrowUp") {
169
+ e.preventDefault();
170
+ moveActive(-1);
171
+ }
172
+ else if (e.key === "Home") {
173
+ e.preventDefault();
174
+ setActiveIndex(enabledIndexes[0] ?? 0);
175
+ }
176
+ else if (e.key === "End") {
177
+ e.preventDefault();
178
+ setActiveIndex(enabledIndexes[enabledIndexes.length - 1] ?? 0);
179
+ }
180
+ else if (e.key === "ArrowRight" && items[activeIndex]?.submenu) {
181
+ e.preventDefault();
182
+ enterSubmenu(items[activeIndex]);
183
+ }
184
+ else if ((e.key === "ArrowLeft" || e.key === "Escape") && openSubmenu) {
185
+ // ARIA's own submenu behaviour: Escape leaves the submenu for its parent menu rather than
186
+ // dismissing the whole thing.
187
+ e.preventDefault();
188
+ leaveSubmenu();
189
+ }
190
+ else if (e.key === "Escape" || e.key === "Tab") {
191
+ e.preventDefault();
192
+ closeMenu(true);
193
+ }
194
+ }
195
+ function handleTriggerKeyDown(e) {
196
+ if (!open && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
197
+ e.preventDefault();
198
+ openMenu();
199
+ }
200
+ }
201
+ let itemIndex = -1;
202
+ return (_jsxs(_Fragment, { children: [_jsxs("button", { ref: triggerRef, type: "button", "aria-haspopup": "menu", "aria-expanded": open, "aria-label": ariaLabel, title: title, disabled: disabled, onClick: () => (open ? closeMenu(false) : openMenu()), onKeyDown: handleTriggerKeyDown, className: iconOnly
203
+ ? className
204
+ : [
205
+ // `min-w-0` so a long label ("Filter: Has attachments") truncates instead of pushing
206
+ // whatever sits beside it in the toolbar off the edge of a 384px list column.
207
+ "inline-flex min-w-0 items-center gap-1 px-2 py-1 rounded-md text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent",
208
+ className,
209
+ ].join(" "), children: [icon, !iconOnly && _jsx("span", { className: "truncate", children: label }), !iconOnly && _jsx(HiChevronDown, { size: 14, "aria-hidden": "true", className: "shrink-0 text-text-muted" })] }), open && (_jsx(PopoverPortal, { anchorRef: triggerRef, onClose: () => closeMenu(false), width: width, height: menuHeight(shownSections, width), "aria-label": ariaLabel, children: _jsx("div", { ref: setMenuNode, role: "menu", tabIndex: -1, "aria-label": ariaLabel, onKeyDown: handleMenuKeyDown, className: "flex-1 overflow-y-auto py-1", children: shownSections.map((section, sectionIndex) => (_jsxs("div", { role: "group", "aria-label": section.label, className: sectionIndex > 0 ? "border-t border-border mt-1 pt-1" : "", children: [section.label && (_jsx("div", { "aria-hidden": "true", className: "h-6 flex items-center px-3 text-xs font-semibold uppercase tracking-wide text-text-muted", children: section.label })), section.items.map((item) => {
210
+ itemIndex += 1;
211
+ const index = itemIndex;
212
+ const role = item.role ?? "menuitem";
213
+ return (_jsxs("button", { ref: (node) => {
214
+ itemRefs.current[index] = node;
215
+ }, type: "button", role: role, disabled: item.disabled, title: item.title, tabIndex: index === activeIndex ? 0 : -1, ...(role === "menuitem"
216
+ ? {}
217
+ : { "aria-checked": item.checked === "mixed" ? "mixed" : !!item.checked }), ...(item.submenu ? { "aria-haspopup": "menu", "aria-expanded": false } : {}), onClick: () => selectItem(item), className: "w-full flex items-start gap-2 px-3 py-2 text-left text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent", children: [_jsxs("span", { className: "w-4 shrink-0 flex justify-center pt-0.5", children: [item.key === "__back" && (_jsx(HiChevronLeft, { size: 14, "aria-hidden": "true", className: "text-text-muted" })), item.checked === "mixed" ? (_jsx(HiMinus, { size: 14, "aria-hidden": "true", className: "text-primary-dark" })) : (item.checked && _jsx(HiCheck, { size: 14, "aria-hidden": "true", className: "text-primary-dark" }))] }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "flex h-5 items-center gap-1.5", children: [item.icon && (_jsx("span", { "aria-hidden": "true", className: "shrink-0 text-text-muted", children: item.icon })), item.swatchColor && (_jsx("span", { "aria-hidden": "true", className: "w-2.5 h-2.5 rounded-full shrink-0", style: { backgroundColor: item.swatchColor } })), _jsx("span", { className: "truncate", children: item.label })] }), item.description && (_jsx("span", { className: "block h-4 text-xs leading-4 text-text-muted truncate font-normal", children: item.description }))] }), item.submenu && (_jsx(HiChevronRight, { size: 14, "aria-hidden": "true", className: "shrink-0 mt-0.5 text-text-muted" }))] }, item.key));
218
+ }), section.note && _jsx("p", { className: "px-3 py-1 text-xs leading-4 text-text-muted", children: section.note })] }, section.key))) }) }))] }));
219
+ }
@@ -34,6 +34,8 @@ export declare function splitTabs(apps: NavItem[], active: string): {
34
34
  * `hidden md:flex` there) — never both hidden or both visible at once.
35
35
  *
36
36
  * The entries share the width equally and never run off the screen: with more than `MAX_TABS`, the last slot is a "More" (…) button that opens a menu
37
- * of the others (see `splitTabs()`). The menu closes on Escape, on a click elsewhere and on choosing an entry.
37
+ * of the others (see `splitTabs()`). That is a plain disclosure - a button (`aria-expanded`, `aria-controls`) and a list of links, all reached by Tab - not an ARIA menu,
38
+ * which would owe its arrow-key navigation. It closes on Escape, on a click elsewhere, when focus tabs out of it and on choosing an entry; opened from the keyboard,
39
+ * it moves focus to its first link.
38
40
  */
39
41
  export default function BottomTabBar({ apps, active }: BottomTabBarProps): React.JSX.Element;
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
- import { useEffect, useRef, useState } from "react";
6
+ import { useEffect, useId, useRef, useState } from "react";
7
7
  import { HiOutlineEllipsisHorizontal } from "react-icons/hi2";
8
8
  import NavBadge, { navItemLabel } from "./NavBadge.js";
9
9
  /** The most entries the bar shows at once: any more and the last slot becomes "More", holding the rest. */
@@ -32,13 +32,23 @@ const TAB_CLASS = "flex-1 min-w-0 overflow-hidden px-1 flex flex-col items-cente
32
32
  * `hidden md:flex` there) — never both hidden or both visible at once.
33
33
  *
34
34
  * The entries share the width equally and never run off the screen: with more than `MAX_TABS`, the last slot is a "More" (…) button that opens a menu
35
- * of the others (see `splitTabs()`). The menu closes on Escape, on a click elsewhere and on choosing an entry.
35
+ * of the others (see `splitTabs()`). That is a plain disclosure - a button (`aria-expanded`, `aria-controls`) and a list of links, all reached by Tab - not an ARIA menu,
36
+ * which would owe its arrow-key navigation. It closes on Escape, on a click elsewhere, when focus tabs out of it and on choosing an entry; opened from the keyboard,
37
+ * it moves focus to its first link.
36
38
  */
37
39
  export default function BottomTabBar({ apps, active }) {
38
40
  const { shown, more } = splitTabs(apps, active);
39
41
  const [open, setOpen] = useState(false);
40
42
  const rootRef = useRef(null);
41
43
  const buttonRef = useRef(null);
44
+ const popupId = useId();
45
+ // Whether the press that opened it was the keyboard's (a click with no pointer has a `detail` of 0).
46
+ const openedByKeyboard = useRef(false);
47
+ useEffect(() => {
48
+ if (open && openedByKeyboard.current) {
49
+ rootRef.current.querySelector("a").focus();
50
+ }
51
+ }, [open]);
42
52
  useEffect(() => {
43
53
  if (!open) {
44
54
  return;
@@ -65,5 +75,14 @@ export default function BottomTabBar({ apps, active }) {
65
75
  }, [open]);
66
76
  // The unread count of an entry that is behind "More" is still worth a dot on it.
67
77
  const hiddenBadge = more.some((app) => (app.badge ?? 0) > 0);
68
- return (_jsxs("nav", { "aria-label": "Mobile navigation", className: "md:hidden fixed bottom-0 inset-x-0 z-30 h-14 bg-surface border-t border-border flex items-stretch", children: [shown.map(({ id, href, label, icon: Icon, badge }) => (_jsxs("a", { href: href, "aria-label": badge ? navItemLabel(label, badge) : undefined, "aria-current": id === active ? "page" : undefined, className: [TAB_CLASS, id === active ? "text-primary-dark" : "text-text-muted"].join(" "), children: [_jsxs("span", { className: "relative inline-flex", children: [_jsx(Icon, { size: 20, "aria-hidden": "true" }), _jsx(NavBadge, { count: badge })] }), label] }, id))), more.length > 0 && (_jsxs("div", { ref: rootRef, className: "flex-1 min-w-0 flex items-stretch relative", children: [_jsxs("button", { ref: buttonRef, type: "button", "aria-label": hiddenBadge ? "More, with unread items" : "More", "aria-haspopup": "menu", "aria-expanded": open, onClick: () => setOpen((value) => !value), className: [TAB_CLASS, "w-full", open ? "text-primary-dark" : "text-text-muted"].join(" "), children: [_jsxs("span", { className: "relative inline-flex", children: [_jsx(HiOutlineEllipsisHorizontal, { size: 20, "aria-hidden": "true" }), hiddenBadge && _jsx("span", { "aria-hidden": "true", "data-testid": "more-dot", className: "absolute -top-0.5 -right-1 size-2 rounded-full bg-danger" })] }), _jsx("span", { children: "More" })] }), open && (_jsx("ul", { role: "menu", "aria-label": "More", className: "absolute bottom-full right-1 mb-2 min-w-48 max-w-[calc(100vw-1rem)] rounded-md border border-border bg-surface py-1 shadow-modal", children: more.map(({ id, href, label, icon: Icon, badge }) => (_jsx("li", { role: "none", children: _jsxs("a", { role: "menuitem", href: href, "aria-label": badge ? navItemLabel(label, badge) : undefined, onClick: () => setOpen(false), className: "flex items-center gap-3 px-4 py-3 text-sm text-text hover:bg-surface-alt focus-visible:outline-2 focus-visible:outline-primary", children: [_jsxs("span", { className: "relative inline-flex", children: [_jsx(Icon, { size: 20, "aria-hidden": "true" }), _jsx(NavBadge, { count: badge })] }), label] }) }, id))) }))] }))] }));
78
+ return (_jsxs("nav", { "aria-label": "Mobile navigation", className: "md:hidden fixed bottom-0 inset-x-0 z-30 h-14 bg-surface border-t border-border flex items-stretch", children: [shown.map(({ id, href, label, icon: Icon, badge }) => (_jsxs("a", { href: href, "aria-label": badge ? navItemLabel(label, badge) : undefined, "aria-current": id === active ? "page" : undefined, className: [TAB_CLASS, id === active ? "text-primary-dark" : "text-text-muted"].join(" "), children: [_jsxs("span", { className: "relative inline-flex", children: [_jsx(Icon, { size: 20, "aria-hidden": "true" }), _jsx(NavBadge, { count: badge })] }), label] }, id))), more.length > 0 && (_jsxs("div", { ref: rootRef, className: "flex-1 min-w-0 flex items-stretch relative",
79
+ // Focus moved on to something outside (not merely lost, as a tap on a link does in a browser that does not focus links).
80
+ onBlur: (event) => {
81
+ if (event.relatedTarget instanceof Node && !event.currentTarget.contains(event.relatedTarget)) {
82
+ setOpen(false);
83
+ }
84
+ }, children: [_jsxs("button", { ref: buttonRef, type: "button", "aria-label": hiddenBadge ? "More, with unread items" : "More", "aria-expanded": open, "aria-controls": open ? popupId : undefined, onClick: (event) => {
85
+ openedByKeyboard.current = event.detail === 0;
86
+ setOpen((value) => !value);
87
+ }, className: [TAB_CLASS, "w-full", open ? "text-primary-dark" : "text-text-muted"].join(" "), children: [_jsxs("span", { className: "relative inline-flex", children: [_jsx(HiOutlineEllipsisHorizontal, { size: 20, "aria-hidden": "true" }), hiddenBadge && _jsx("span", { "aria-hidden": "true", "data-testid": "more-dot", className: "absolute -top-0.5 -right-1 size-2 rounded-full bg-danger" })] }), _jsx("span", { children: "More" })] }), open && (_jsx("ul", { id: popupId, "aria-label": "More", className: "absolute bottom-full right-1 mb-2 min-w-48 max-w-[calc(100vw-1rem)] rounded-md border border-border bg-surface py-1 shadow-modal", children: more.map(({ id, href, label, icon: Icon, badge }) => (_jsx("li", { children: _jsxs("a", { href: href, "aria-label": badge ? navItemLabel(label, badge) : undefined, onClick: () => setOpen(false), className: "flex items-center gap-3 px-4 py-3 text-sm text-text hover:bg-surface-alt focus-visible:outline-2 focus-visible:outline-primary", children: [_jsxs("span", { className: "relative inline-flex", children: [_jsx(Icon, { size: 20, "aria-hidden": "true" }), _jsx(NavBadge, { count: badge })] }), label] }) }, id))) }))] }))] }));
69
88
  }
@@ -18,6 +18,7 @@ export interface PopoverPortalProps {
18
18
  * isn't a stacking-order problem `z-index` can override). Prefers opening *below* the anchor (there's
19
19
  * more room in the editor area below the toolbar than above it, where the To/Cc/Subject rows are —
20
20
  * confirmed directly: an earlier `absolute`+`overflow-hidden` version of this rendered the popup
21
- * behind those rows), flipping to *above* only if there isn't enough room below.
21
+ * behind those rows), flipping to *above* only if there isn't enough room below. Its `z-index` is above a `Modal`/`Drawer` backdrop (`z-[1000]`), so a popover opened from inside
22
+ * a dialog (the contact card's photo menu) is not hidden behind it.
22
23
  */
23
24
  export default function PopoverPortal({ anchorRef, onClose, width, height, children, ...rest }: PropsWithChildren<PopoverPortalProps>): React.ReactPortal | null;
@@ -13,7 +13,8 @@ import { createPortal } from "react-dom";
13
13
  * isn't a stacking-order problem `z-index` can override). Prefers opening *below* the anchor (there's
14
14
  * more room in the editor area below the toolbar than above it, where the To/Cc/Subject rows are —
15
15
  * confirmed directly: an earlier `absolute`+`overflow-hidden` version of this rendered the popup
16
- * behind those rows), flipping to *above* only if there isn't enough room below.
16
+ * behind those rows), flipping to *above* only if there isn't enough room below. Its `z-index` is above a `Modal`/`Drawer` backdrop (`z-[1000]`), so a popover opened from inside
17
+ * a dialog (the contact card's photo menu) is not hidden behind it.
17
18
  */
18
19
  export default function PopoverPortal({ anchorRef, onClose, width, height, children, ...rest }) {
19
20
  const containerRef = useRef(null);
@@ -49,5 +50,5 @@ export default function PopoverPortal({ anchorRef, onClose, width, height, child
49
50
  if (!style) {
50
51
  return null;
51
52
  }
52
- return createPortal(_jsx("div", { ref: containerRef, role: "dialog", style: style, className: "z-50 flex flex-col bg-surface border border-border rounded-md shadow-modal overflow-hidden", ...rest, children: children }), document.body);
53
+ return createPortal(_jsx("div", { ref: containerRef, role: "dialog", style: style, className: "z-[1010] flex flex-col bg-surface border border-border rounded-md shadow-modal overflow-hidden", ...rest, children: children }), document.body);
53
54
  }
@@ -50,6 +50,12 @@ export default function TimeZonePicker({ id, "aria-label": label, "aria-describe
50
50
  document.addEventListener("pointerdown", outside);
51
51
  return () => document.removeEventListener("pointerdown", outside);
52
52
  }, [open]);
53
+ // The arrow keys move `active` (and `aria-activedescendant`) only: the list scrolls, so the zone they reach has to be brought into view.
54
+ useEffect(() => {
55
+ if (open && active >= 0) {
56
+ document.getElementById(`${listId}-${active}`)?.scrollIntoView?.({ block: "nearest" });
57
+ }
58
+ }, [open, active]);
53
59
  function close(refocus) {
54
60
  setOpen(false);
55
61
  setQuery("");
@@ -88,7 +94,13 @@ export default function TimeZonePicker({ id, "aria-label": label, "aria-describe
88
94
  close(true);
89
95
  }
90
96
  }
91
- return (_jsxs("div", { ref: root, className: "relative inline-block max-w-full", children: [_jsxs("button", { ref: trigger, id: id, type: "button", role: "combobox", "aria-label": label, "aria-describedby": describedBy, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listId : undefined, className: ["flex items-center justify-between gap-2 text-left", className].filter(Boolean).join(" "), onClick: () => setOpen((wasOpen) => !wasOpen), onKeyDown: (event) => {
97
+ return (_jsxs("div", { ref: root, className: "relative inline-block max-w-full",
98
+ // Tabbing away leaves the list open otherwise (only a press outside closed it). Focus that goes nowhere in particular (the window losing it) is not leaving.
99
+ onBlur: (event) => {
100
+ if (open && event.relatedTarget instanceof Node && !root.current?.contains(event.relatedTarget)) {
101
+ close(false);
102
+ }
103
+ }, children: [_jsxs("button", { ref: trigger, id: id, type: "button", role: "combobox", "aria-label": label, "aria-describedby": describedBy, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listId : undefined, className: ["flex items-center justify-between gap-2 text-left", className].filter(Boolean).join(" "), onClick: () => setOpen((wasOpen) => !wasOpen), onKeyDown: (event) => {
92
104
  if (event.key === "ArrowDown" && !open) {
93
105
  event.preventDefault();
94
106
  setOpen(true);
@@ -156,14 +156,17 @@ export declare function updateContact(patch: ContactPatch, client?: ApiClient):
156
156
  export declare function setContactFavorite(contact: Contact, favorite: boolean, client?: ApiClient): Promise<Contact>;
157
157
  /**
158
158
  * The URL of a contact's own picture, or `undefined` when they have none. The version is part of it so a replaced picture is fetched again rather than
159
- * served from the browser's cache.
159
+ * served from the browser's cache. Only an `<img src>` of the cookie-authenticated web app can load it: under an explicit `ApiClient` use
160
+ * `useContactPhotoSrc()` instead.
160
161
  */
161
162
  export declare function contactPhotoUrl(contact: Pick<Contact, "uid" | "version" | "photoBlobKey">): string | undefined;
162
163
  /** The picture types the server accepts for a contact, and the most bytes it takes. */
163
164
  export declare const CONTACT_PHOTO_TYPES: string[];
164
165
  export declare const CONTACT_PHOTO_MAX_BYTES: number;
165
166
  /** Sets a contact's own picture from an image file (the file's own bytes go up, not JSON) and returns the updated contact. */
166
- export declare function uploadContactPhoto(uid: string, version: number, file: Blob): Promise<Contact>;
167
+ export declare function uploadContactPhoto(uid: string, version: number, file: Blob, client?: ApiClient): Promise<Contact>;
168
+ /** The bytes of a contact's own picture through an explicit `client` - for one that authenticates with a token, whose `<img>` cannot (see `contactPhotoUrl()`). */
169
+ export declare function fetchContactPhoto(uid: string, version: number, client: ApiClient): Promise<Blob>;
167
170
  /** Removes a contact's own picture and returns the updated contact. */
168
171
  export declare function deleteContactPhoto(uid: string, version: number, client?: ApiClient): Promise<Contact>;
169
172
  export declare function deleteContact(uid: string, version: number, client?: ApiClient): Promise<void>;
@@ -4,13 +4,16 @@
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  /** Typed wrappers over `@rapidmx/restapi`'s `/mail/contacts` REST surface — see `mailApi.ts`'s own header
6
6
  * comment for the shared ACL/authorization model every wrapper file here follows. */
7
- import { ApiRequestError, apiUrl, withClient, withCsrfHeader } from "../util/api.js";
8
- import { buildQuery } from "../util/apiQuery.js";
7
+ import { apiUrl, withClient, withClientRaw } from "../util/api.js";
8
+ import { SETTINGS_LIST_LIMIT, buildQuery } from "../util/apiQuery.js";
9
9
  import { signingKeyFingerprints, } from "../crypto/keyvaultApi.js";
10
+ /** The order a contacts list is read in: alphabetical, with the `uid` as the tie-breaker so contacts sharing a name keep one fixed order and
11
+ * a page boundary between them can neither skip nor repeat one. */
12
+ const CONTACT_SORT = JSON.stringify({ displayName: "ASC", uid: "ASC" });
10
13
  /** Lists a folder's contacts, alphabetically by display name. Never includes soft-deleted contacts — see
11
14
  * `listDeletedContacts()` for those. */
12
15
  export function listContacts(folderUid, params = {}, client) {
13
- return withClient(client, `/mail/contacts?${buildQuery(params, { folderUid, sort: JSON.stringify({ displayName: "ASC" }) })}`);
16
+ return withClient(client, `/mail/contacts?${buildQuery(params, { folderUid, sort: CONTACT_SORT })}`);
14
17
  }
15
18
  function contactsMatching(contacts, address) {
16
19
  const wanted = address.trim().toLowerCase();
@@ -95,7 +98,7 @@ export async function fetchSignerKeyState(folderUids, address, client) {
95
98
  * is browse-only until that lands.
96
99
  */
97
100
  export function listDeletedContacts(folderUid, params = {}, client) {
98
- return withClient(client, `/mail/contacts?${buildQuery(params, { folderUid, deleted: "true", sort: JSON.stringify({ displayName: "ASC" }) })}`);
101
+ return withClient(client, `/mail/contacts?${buildQuery(params, { folderUid, deleted: "true", sort: CONTACT_SORT })}`);
99
102
  }
100
103
  export function getContact(uid, client) {
101
104
  return withClient(client, `/mail/contacts/${encodeURIComponent(uid)}`);
@@ -120,7 +123,8 @@ export function setContactFavorite(contact, favorite, client) {
120
123
  }
121
124
  /**
122
125
  * The URL of a contact's own picture, or `undefined` when they have none. The version is part of it so a replaced picture is fetched again rather than
123
- * served from the browser's cache.
126
+ * served from the browser's cache. Only an `<img src>` of the cookie-authenticated web app can load it: under an explicit `ApiClient` use
127
+ * `useContactPhotoSrc()` instead.
124
128
  */
125
129
  export function contactPhotoUrl(contact) {
126
130
  return contact.photoBlobKey ? apiUrl(`/mail/contacts/${encodeURIComponent(contact.uid)}/photo?v=${contact.version}`) : undefined;
@@ -129,19 +133,12 @@ export function contactPhotoUrl(contact) {
129
133
  export const CONTACT_PHOTO_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
130
134
  export const CONTACT_PHOTO_MAX_BYTES = 1024 * 1024;
131
135
  /** Sets a contact's own picture from an image file (the file's own bytes go up, not JSON) and returns the updated contact. */
132
- export async function uploadContactPhoto(uid, version, file) {
133
- const res = await fetch(apiUrl(`/mail/contacts/${encodeURIComponent(uid)}/photo?version=${version}`), {
134
- method: "PUT",
135
- credentials: "include",
136
- headers: withCsrfHeader({ "Content-Type": file.type }, "PUT"),
137
- body: file,
138
- });
139
- const contentType = res.headers.get("content-type") ?? "";
140
- const body = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
141
- if (!res.ok) {
142
- throw new ApiRequestError((body && (body.message || body.error)) || res.statusText || "Upload failed.", res.status, body?.code);
143
- }
144
- return body;
136
+ export function uploadContactPhoto(uid, version, file, client) {
137
+ return withClientRaw(client, `/mail/contacts/${encodeURIComponent(uid)}/photo?version=${version}`, "PUT", file);
138
+ }
139
+ /** The bytes of a contact's own picture through an explicit `client` - for one that authenticates with a token, whose `<img>` cannot (see `contactPhotoUrl()`). */
140
+ export function fetchContactPhoto(uid, version, client) {
141
+ return client.fetchBlob(`/mail/contacts/${encodeURIComponent(uid)}/photo?v=${version}`);
145
142
  }
146
143
  /** Removes a contact's own picture and returns the updated contact. */
147
144
  export function deleteContactPhoto(uid, version, client) {
@@ -152,7 +149,7 @@ export function deleteContact(uid, version, client) {
152
149
  }
153
150
  /** Lists a mailbox's contact lists (Outlook-style "Your contact lists"), alphabetically by name. */
154
151
  export function listContactLists(mailboxUid, params = {}, client) {
155
- return withClient(client, `/mail/contact-lists?${buildQuery(params, { mailboxUid, sort: JSON.stringify({ name: "ASC" }) })}`);
152
+ return withClient(client, `/mail/contact-lists?${buildQuery({ limit: SETTINGS_LIST_LIMIT, ...params }, { mailboxUid, sort: JSON.stringify({ name: "ASC", uid: "ASC" }) })}`);
156
153
  }
157
154
  export function createContactList(input, client) {
158
155
  return withClient(client, "/mail/contact-lists", { method: "POST", body: JSON.stringify(input) });
@@ -1,10 +1,21 @@
1
- /** The `localStorage` key of the privacy preference: `"off"` stops contact avatars from asking Gravatar for a picture. */
1
+ /** The `localStorage` key of the privacy preference: `"on"` lets contact avatars ask Gravatar for a picture; anything else (including unset or an old `"off"`) does not. */
2
2
  export declare const GRAVATAR_PREFERENCE_KEY = "rapidmx:gravatar";
3
+ /** Fired on `window` when this tab changes the preference (the `storage` event covers other tabs). */
4
+ export declare const GRAVATAR_PREFERENCE_EVENT = "rapidmx:gravatar-changed";
3
5
  /**
4
- * Whether avatars may look a person up at Gravatar by the SHA-256 hash of their address (never the address itself). On unless the reader turned
5
- * it off (`localStorage["rapidmx:gravatar"] = "off"`); a browser without storage keeps the default.
6
+ * Whether avatars may look a person up at Gravatar by the SHA-256 hash of their address (never the address itself). Off unless the reader turned it on
7
+ * in Settings > Privacy (`localStorage["rapidmx:gravatar"] = "on"`): it tells gravatar.com the hash and the reader's IP address. A browser without
8
+ * storage stays off.
6
9
  */
7
10
  export declare function gravatarEnabled(): boolean;
11
+ /** Turns the Gravatar lookup on or off and tells every subscriber (`subscribeGravatarPreference()`) in this tab. */
12
+ export declare function setGravatarEnabled(enabled: boolean): void;
13
+ /** Calls `listener` whenever the preference changes in this tab or another; returns the unsubscribe function. Shaped for `useSyncExternalStore`. */
14
+ export declare function subscribeGravatarPreference(listener: () => void): () => void;
15
+ /** Remembers that Gravatar has no picture at `url`. */
16
+ export declare function markGravatarMissing(url: string): void;
17
+ /** Whether Gravatar already answered with no picture at `url`. */
18
+ export declare function isGravatarMissing(url: string): boolean;
8
19
  /**
9
20
  * The Gravatar picture URL for `email`, `size` pixels square. It answers `404` (`d=404`) for a person with no Gravatar, so an `<img>` fails to load
10
21
  * and the caller falls back to initials rather than showing Gravatar's placeholder. `undefined` for an empty address or without Web Crypto.
@@ -2,20 +2,57 @@
2
2
  // Copyright (C) 2026 Jean-Philippe Steinmetz
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
- /** The `localStorage` key of the privacy preference: `"off"` stops contact avatars from asking Gravatar for a picture. */
5
+ /** The `localStorage` key of the privacy preference: `"on"` lets contact avatars ask Gravatar for a picture; anything else (including unset or an old `"off"`) does not. */
6
6
  export const GRAVATAR_PREFERENCE_KEY = "rapidmx:gravatar";
7
+ /** Fired on `window` when this tab changes the preference (the `storage` event covers other tabs). */
8
+ export const GRAVATAR_PREFERENCE_EVENT = "rapidmx:gravatar-changed";
7
9
  /**
8
- * Whether avatars may look a person up at Gravatar by the SHA-256 hash of their address (never the address itself). On unless the reader turned
9
- * it off (`localStorage["rapidmx:gravatar"] = "off"`); a browser without storage keeps the default.
10
+ * Whether avatars may look a person up at Gravatar by the SHA-256 hash of their address (never the address itself). Off unless the reader turned it on
11
+ * in Settings > Privacy (`localStorage["rapidmx:gravatar"] = "on"`): it tells gravatar.com the hash and the reader's IP address. A browser without
12
+ * storage stays off.
10
13
  */
11
14
  export function gravatarEnabled() {
12
15
  try {
13
- return globalThis.localStorage?.getItem(GRAVATAR_PREFERENCE_KEY) !== "off";
16
+ return globalThis.localStorage.getItem(GRAVATAR_PREFERENCE_KEY) === "on";
14
17
  }
15
18
  catch {
16
- return true;
19
+ return false;
17
20
  }
18
21
  }
22
+ /** Turns the Gravatar lookup on or off and tells every subscriber (`subscribeGravatarPreference()`) in this tab. */
23
+ export function setGravatarEnabled(enabled) {
24
+ try {
25
+ globalThis.localStorage.setItem(GRAVATAR_PREFERENCE_KEY, enabled ? "on" : "off");
26
+ }
27
+ catch {
28
+ // Storage is unavailable: the preference cannot be kept, so it stays off.
29
+ }
30
+ globalThis.dispatchEvent(new Event(GRAVATAR_PREFERENCE_EVENT));
31
+ }
32
+ /** Calls `listener` whenever the preference changes in this tab or another; returns the unsubscribe function. Shaped for `useSyncExternalStore`. */
33
+ export function subscribeGravatarPreference(listener) {
34
+ const onStorage = (event) => {
35
+ if (event.key === null || event.key === GRAVATAR_PREFERENCE_KEY) {
36
+ listener();
37
+ }
38
+ };
39
+ globalThis.addEventListener(GRAVATAR_PREFERENCE_EVENT, listener);
40
+ globalThis.addEventListener("storage", onStorage);
41
+ return () => {
42
+ globalThis.removeEventListener(GRAVATAR_PREFERENCE_EVENT, listener);
43
+ globalThis.removeEventListener("storage", onStorage);
44
+ };
45
+ }
46
+ /** Gravatar URLs that answered with nothing, so a list that shows the same person again does not ask again for the page's lifetime. */
47
+ const missing = new Set();
48
+ /** Remembers that Gravatar has no picture at `url`. */
49
+ export function markGravatarMissing(url) {
50
+ missing.add(url);
51
+ }
52
+ /** Whether Gravatar already answered with no picture at `url`. */
53
+ export function isGravatarMissing(url) {
54
+ return missing.has(url);
55
+ }
19
56
  const hashes = new Map();
20
57
  /** The lowercase-hex SHA-256 of a trimmed, lowercased `email`, or `undefined` where the browser has no Web Crypto (an insecure page). */
21
58
  function hashOf(email) {
@@ -0,0 +1,24 @@
1
+ /** The longest side (in pixels) an accepted picture may have and still be sent as it is. */
2
+ export declare const CONTACT_PHOTO_MAX_SIDE = 1024;
3
+ /** The side (in pixels) of the square picture made from one that has to be changed - an avatar is shown at 72 pixels at most, 512 covers a high-density screen. */
4
+ export declare const CONTACT_PHOTO_OUTPUT_SIDE = 512;
5
+ /** JPEG qualities tried in turn until the picture fits `CONTACT_PHOTO_MAX_BYTES`. */
6
+ export declare const CONTACT_PHOTO_QUALITIES: number[];
7
+ /** The most pixels (width x height) a picture may have to be decoded: a few kilobytes of PNG can say 30000 x 30000, which takes gigabytes of memory to decode. */
8
+ export declare const CONTACT_PHOTO_MAX_PIXELS = 16000000;
9
+ export declare const CONTACT_PHOTO_UNSUPPORTED_MESSAGE = "This picture's format isn't supported by your browser \u2014 choose a JPEG or PNG instead.";
10
+ export declare const CONTACT_PHOTO_TOO_LARGE_MESSAGE = "This picture could not be made small enough \u2014 choose another one.";
11
+ /** Why a chosen picture cannot be used (a format the browser cannot decode, such as HEIC, or one that stays too large); its `message` is fit to show the user. */
12
+ export declare class ContactPhotoError extends Error {
13
+ constructor(message?: string);
14
+ }
15
+ /**
16
+ * The picture to upload for `file`. A picture the server already takes (an accepted type within the byte limit and at most `CONTACT_PHOTO_MAX_SIDE` pixels a
17
+ * side) is returned as it is - an animated GIF stays animated - unless it is a JPEG, which is always drawn again to drop its EXIF. Anything else (a phone's 6 MB photo, a HEIC, a huge PNG) is decoded with its EXIF orientation applied,
18
+ * cropped to the centered square - avatars are circular, so the corners are never seen - scaled down to at most `CONTACT_PHOTO_OUTPUT_SIDE` pixels and encoded as a
19
+ * JPEG (transparent parts on white, quality lowered step by step until it fits), or kept as PNG/WebP when it has transparency and still fits.
20
+ *
21
+ * @throws ContactPhotoError when the browser cannot decode it (HEIC, or not a picture at all), it says it has more than `CONTACT_PHOTO_MAX_PIXELS` pixels
22
+ * (it is not decoded at all then) or it cannot be made small enough.
23
+ */
24
+ export declare function prepareContactPhoto(file: File): Promise<File>;