@planningcenter/chat-react-native 4.4.0-rc.7 → 4.4.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 (339) hide show
  1. package/build/components/conversation/jump_to_bottom_button.d.ts.map +1 -1
  2. package/build/components/conversation/jump_to_bottom_button.js +42 -40
  3. package/build/components/conversation/jump_to_bottom_button.js.map +1 -1
  4. package/build/components/conversation/message.d.ts +2 -1
  5. package/build/components/conversation/message.d.ts.map +1 -1
  6. package/build/components/conversation/message.js +3 -1
  7. package/build/components/conversation/message.js.map +1 -1
  8. package/build/components/conversation/message_form.d.ts.map +1 -1
  9. package/build/components/conversation/message_form.js +3 -1
  10. package/build/components/conversation/message_form.js.map +1 -1
  11. package/build/components/conversation/message_list.d.ts.map +1 -1
  12. package/build/components/conversation/message_list.js +35 -31
  13. package/build/components/conversation/message_list.js.map +1 -1
  14. package/build/components/conversation/message_reaction.d.ts +2 -1
  15. package/build/components/conversation/message_reaction.d.ts.map +1 -1
  16. package/build/components/conversation/message_reaction.js +67 -60
  17. package/build/components/conversation/message_reaction.js.map +1 -1
  18. package/build/components/conversation/messages_disabled_banners.d.ts +7 -5
  19. package/build/components/conversation/messages_disabled_banners.d.ts.map +1 -1
  20. package/build/components/conversation/messages_disabled_banners.js +15 -14
  21. package/build/components/conversation/messages_disabled_banners.js.map +1 -1
  22. package/build/components/conversation/system_message.d.ts +2 -1
  23. package/build/components/conversation/system_message.d.ts.map +1 -1
  24. package/build/components/conversation/system_message.js +133 -100
  25. package/build/components/conversation/system_message.js.map +1 -1
  26. package/build/components/conversations/archived_results_footer.d.ts +6 -0
  27. package/build/components/conversations/archived_results_footer.d.ts.map +1 -0
  28. package/build/components/conversations/archived_results_footer.js +51 -0
  29. package/build/components/conversations/archived_results_footer.js.map +1 -0
  30. package/build/components/conversations/conversation_actions.d.ts.map +1 -1
  31. package/build/components/conversations/conversation_actions.js +241 -206
  32. package/build/components/conversations/conversation_actions.js.map +1 -1
  33. package/build/components/conversations/conversation_preview.d.ts.map +1 -1
  34. package/build/components/conversations/conversation_preview.js +102 -93
  35. package/build/components/conversations/conversation_preview.js.map +1 -1
  36. package/build/components/conversations/conversations.d.ts.map +1 -1
  37. package/build/components/conversations/conversations.js +66 -41
  38. package/build/components/conversations/conversations.js.map +1 -1
  39. package/build/components/conversations/conversations_blank_state.d.ts.map +1 -1
  40. package/build/components/conversations/conversations_blank_state.js +55 -12
  41. package/build/components/conversations/conversations_blank_state.js.map +1 -1
  42. package/build/components/conversations/filtered_results_footer.d.ts +2 -0
  43. package/build/components/conversations/filtered_results_footer.d.ts.map +1 -0
  44. package/build/components/conversations/filtered_results_footer.js +77 -0
  45. package/build/components/conversations/filtered_results_footer.js.map +1 -0
  46. package/build/components/display/action_button.d.ts.map +1 -1
  47. package/build/components/display/action_button.js +18 -16
  48. package/build/components/display/action_button.js.map +1 -1
  49. package/build/components/display/filter_dropdown.d.ts +14 -0
  50. package/build/components/display/filter_dropdown.d.ts.map +1 -0
  51. package/build/components/display/filter_dropdown.js +193 -0
  52. package/build/components/display/filter_dropdown.js.map +1 -0
  53. package/build/components/display/icon_tile.d.ts +13 -0
  54. package/build/components/display/icon_tile.d.ts.map +1 -0
  55. package/build/components/display/icon_tile.js +149 -0
  56. package/build/components/display/icon_tile.js.map +1 -0
  57. package/build/components/display/index.d.ts +2 -0
  58. package/build/components/display/index.d.ts.map +1 -1
  59. package/build/components/display/index.js +2 -0
  60. package/build/components/display/index.js.map +1 -1
  61. package/build/contexts/api_provider.d.ts.map +1 -1
  62. package/build/contexts/api_provider.js +21 -9
  63. package/build/contexts/api_provider.js.map +1 -1
  64. package/build/contexts/conversations_context.d.ts.map +1 -1
  65. package/build/contexts/conversations_context.js +21 -18
  66. package/build/contexts/conversations_context.js.map +1 -1
  67. package/build/hooks/groups/use_group_members_for_new_conversation.d.ts +196 -16
  68. package/build/hooks/groups/use_group_members_for_new_conversation.d.ts.map +1 -1
  69. package/build/hooks/groups/use_group_members_for_new_conversation.js +72 -58
  70. package/build/hooks/groups/use_group_members_for_new_conversation.js.map +1 -1
  71. package/build/hooks/index.d.ts +3 -0
  72. package/build/hooks/index.d.ts.map +1 -1
  73. package/build/hooks/index.js +3 -0
  74. package/build/hooks/index.js.map +1 -1
  75. package/build/hooks/use_animated_message_background_color.d.ts.map +1 -1
  76. package/build/hooks/use_animated_message_background_color.js +5 -3
  77. package/build/hooks/use_animated_message_background_color.js.map +1 -1
  78. package/build/hooks/use_api.d.ts +3 -2
  79. package/build/hooks/use_api.d.ts.map +1 -1
  80. package/build/hooks/use_api.js.map +1 -1
  81. package/build/hooks/use_can_browse_archived_conversations.d.ts +2 -0
  82. package/build/hooks/use_can_browse_archived_conversations.d.ts.map +1 -0
  83. package/build/hooks/use_can_browse_archived_conversations.js +29 -0
  84. package/build/hooks/use_can_browse_archived_conversations.js.map +1 -0
  85. package/build/hooks/use_clear_conversation_filters.d.ts +2 -0
  86. package/build/hooks/use_clear_conversation_filters.d.ts.map +1 -0
  87. package/build/hooks/use_clear_conversation_filters.js +25 -0
  88. package/build/hooks/use_clear_conversation_filters.js.map +1 -0
  89. package/build/hooks/use_conversation.d.ts +73 -1
  90. package/build/hooks/use_conversation.d.ts.map +1 -1
  91. package/build/hooks/use_conversation.js +130 -3
  92. package/build/hooks/use_conversation.js.map +1 -1
  93. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  94. package/build/hooks/use_conversation_messages.js +29 -26
  95. package/build/hooks/use_conversation_messages.js.map +1 -1
  96. package/build/hooks/use_current_person.d.ts.map +1 -1
  97. package/build/hooks/use_current_person.js +1 -1
  98. package/build/hooks/use_current_person.js.map +1 -1
  99. package/build/hooks/use_jolt.d.ts +1 -0
  100. package/build/hooks/use_jolt.d.ts.map +1 -1
  101. package/build/hooks/use_jolt.js +59 -0
  102. package/build/hooks/use_jolt.js.map +1 -1
  103. package/build/hooks/use_manual_archiving_enabled.d.ts +2 -0
  104. package/build/hooks/use_manual_archiving_enabled.d.ts.map +1 -0
  105. package/build/hooks/use_manual_archiving_enabled.js +18 -0
  106. package/build/hooks/use_manual_archiving_enabled.js.map +1 -0
  107. package/build/hooks/use_mark_latest_message_read.d.ts.map +1 -1
  108. package/build/hooks/use_mark_latest_message_read.js +3 -3
  109. package/build/hooks/use_mark_latest_message_read.js.map +1 -1
  110. package/build/hooks/use_reduce_motion.d.ts +2 -0
  111. package/build/hooks/use_reduce_motion.d.ts.map +1 -0
  112. package/build/hooks/use_reduce_motion.js +28 -0
  113. package/build/hooks/use_reduce_motion.js.map +1 -0
  114. package/build/hooks/use_unarchive_conversation_action.d.ts +7 -0
  115. package/build/hooks/use_unarchive_conversation_action.d.ts.map +1 -0
  116. package/build/hooks/use_unarchive_conversation_action.js +52 -0
  117. package/build/hooks/use_unarchive_conversation_action.js.map +1 -0
  118. package/build/screens/attachment_actions/attachment_actions_screen.d.ts.map +1 -1
  119. package/build/screens/attachment_actions/attachment_actions_screen.js +84 -61
  120. package/build/screens/attachment_actions/attachment_actions_screen.js.map +1 -1
  121. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  122. package/build/screens/conversation_details_screen.js +490 -395
  123. package/build/screens/conversation_details_screen.js.map +1 -1
  124. package/build/screens/conversation_filters/components/conversation_filters.d.ts.map +1 -1
  125. package/build/screens/conversation_filters/components/conversation_filters.js +88 -18
  126. package/build/screens/conversation_filters/components/conversation_filters.js.map +1 -1
  127. package/build/screens/conversation_filters/components/rows.d.ts +9 -0
  128. package/build/screens/conversation_filters/components/rows.d.ts.map +1 -1
  129. package/build/screens/conversation_filters/components/rows.js +93 -1
  130. package/build/screens/conversation_filters/components/rows.js.map +1 -1
  131. package/build/screens/conversation_filters/context/conversation_filter_context.d.ts.map +1 -1
  132. package/build/screens/conversation_filters/context/conversation_filter_context.js +8 -4
  133. package/build/screens/conversation_filters/context/conversation_filter_context.js.map +1 -1
  134. package/build/screens/conversation_filters/filter_types.d.ts +1 -0
  135. package/build/screens/conversation_filters/filter_types.d.ts.map +1 -1
  136. package/build/screens/conversation_filters/filter_types.js +1 -0
  137. package/build/screens/conversation_filters/filter_types.js.map +1 -1
  138. package/build/screens/conversation_filters/screen_props.d.ts +1 -0
  139. package/build/screens/conversation_filters/screen_props.d.ts.map +1 -1
  140. package/build/screens/conversation_filters/screen_props.js.map +1 -1
  141. package/build/screens/conversation_filters_screen.d.ts +1 -0
  142. package/build/screens/conversation_filters_screen.d.ts.map +1 -1
  143. package/build/screens/conversation_filters_screen.js +79 -48
  144. package/build/screens/conversation_filters_screen.js.map +1 -1
  145. package/build/screens/conversation_new/components/groups_form.d.ts.map +1 -1
  146. package/build/screens/conversation_new/components/groups_form.js +37 -34
  147. package/build/screens/conversation_new/components/groups_form.js.map +1 -1
  148. package/build/screens/conversation_screen.d.ts.map +1 -1
  149. package/build/screens/conversation_screen.js +13 -10
  150. package/build/screens/conversation_screen.js.map +1 -1
  151. package/build/screens/conversations/components/filter_dropdown_list_header_component.d.ts +2 -0
  152. package/build/screens/conversations/components/filter_dropdown_list_header_component.d.ts.map +1 -0
  153. package/build/screens/conversations/components/filter_dropdown_list_header_component.js +580 -0
  154. package/build/screens/conversations/components/filter_dropdown_list_header_component.js.map +1 -0
  155. package/build/screens/conversations/components/list_header_component.d.ts.map +1 -1
  156. package/build/screens/conversations/components/list_header_component.js +95 -90
  157. package/build/screens/conversations/components/list_header_component.js.map +1 -1
  158. package/build/screens/conversations/components/search_list_header_component.d.ts.map +1 -1
  159. package/build/screens/conversations/components/search_list_header_component.js +100 -95
  160. package/build/screens/conversations/components/search_list_header_component.js.map +1 -1
  161. package/build/screens/conversations/conversations_screen.d.ts +1 -0
  162. package/build/screens/conversations/conversations_screen.d.ts.map +1 -1
  163. package/build/screens/conversations/conversations_screen.js +15 -2
  164. package/build/screens/conversations/conversations_screen.js.map +1 -1
  165. package/build/screens/design_system_screen.d.ts.map +1 -1
  166. package/build/screens/design_system_screen.js +275 -215
  167. package/build/screens/design_system_screen.js.map +1 -1
  168. package/build/screens/message_actions_screen.d.ts.map +1 -1
  169. package/build/screens/message_actions_screen.js +112 -92
  170. package/build/screens/message_actions_screen.js.map +1 -1
  171. package/build/screens/search/search_screen.d.ts.map +1 -1
  172. package/build/screens/search/search_screen.js +10 -2
  173. package/build/screens/search/search_screen.js.map +1 -1
  174. package/build/screens/send_giphy_screen.d.ts +1 -0
  175. package/build/screens/send_giphy_screen.d.ts.map +1 -1
  176. package/build/screens/send_giphy_screen.js +108 -106
  177. package/build/screens/send_giphy_screen.js.map +1 -1
  178. package/build/screens/team_conversation_screen.js +1 -1
  179. package/build/screens/team_conversation_screen.js.map +1 -1
  180. package/build/types/jolt_events/message_events.d.ts +2 -0
  181. package/build/types/jolt_events/message_events.d.ts.map +1 -1
  182. package/build/types/jolt_events/message_events.js.map +1 -1
  183. package/build/types/resources/conversation.d.ts +2 -2
  184. package/build/types/resources/conversation.d.ts.map +1 -1
  185. package/build/types/resources/conversation.js.map +1 -1
  186. package/build/types/resources/member_ability.d.ts +2 -0
  187. package/build/types/resources/member_ability.d.ts.map +1 -1
  188. package/build/types/resources/member_ability.js.map +1 -1
  189. package/build/types/resources/message.d.ts +2 -0
  190. package/build/types/resources/message.d.ts.map +1 -1
  191. package/build/types/resources/message.js.map +1 -1
  192. package/build/types/resources/person.d.ts +1 -0
  193. package/build/types/resources/person.d.ts.map +1 -1
  194. package/build/types/resources/person.js.map +1 -1
  195. package/build/utils/archived_conversation.d.ts +5 -0
  196. package/build/utils/archived_conversation.d.ts.map +1 -0
  197. package/build/utils/archived_conversation.js +5 -0
  198. package/build/utils/archived_conversation.js.map +1 -0
  199. package/build/utils/date.d.ts +1 -0
  200. package/build/utils/date.d.ts.map +1 -1
  201. package/build/utils/date.js +9 -0
  202. package/build/utils/date.js.map +1 -1
  203. package/build/utils/index.d.ts +1 -0
  204. package/build/utils/index.d.ts.map +1 -1
  205. package/build/utils/index.js +1 -0
  206. package/build/utils/index.js.map +1 -1
  207. package/build/utils/jolt/transform_message_event_data_to_message_resource.d.ts.map +1 -1
  208. package/build/utils/jolt/transform_message_event_data_to_message_resource.js +2 -1
  209. package/build/utils/jolt/transform_message_event_data_to_message_resource.js.map +1 -1
  210. package/build/utils/native_adapters/configuration.d.ts +3 -0
  211. package/build/utils/native_adapters/configuration.d.ts.map +1 -1
  212. package/build/utils/native_adapters/configuration.js +3 -0
  213. package/build/utils/native_adapters/configuration.js.map +1 -1
  214. package/build/utils/native_adapters/index.d.ts +1 -0
  215. package/build/utils/native_adapters/index.d.ts.map +1 -1
  216. package/build/utils/native_adapters/index.js +1 -0
  217. package/build/utils/native_adapters/index.js.map +1 -1
  218. package/build/utils/native_adapters/menu.d.ts +41 -0
  219. package/build/utils/native_adapters/menu.d.ts.map +1 -0
  220. package/build/utils/native_adapters/menu.js +56 -0
  221. package/build/utils/native_adapters/menu.js.map +1 -0
  222. package/build/utils/request/conversation.d.ts +2 -1
  223. package/build/utils/request/conversation.d.ts.map +1 -1
  224. package/build/utils/request/conversation.js +4 -1
  225. package/build/utils/request/conversation.js.map +1 -1
  226. package/build/utils/request/messages_data_options.d.ts.map +1 -1
  227. package/build/utils/request/messages_data_options.js +1 -1
  228. package/build/utils/request/messages_data_options.js.map +1 -1
  229. package/build/utils/system_messages.d.ts +7 -5
  230. package/build/utils/system_messages.d.ts.map +1 -1
  231. package/build/utils/system_messages.js +10 -2
  232. package/build/utils/system_messages.js.map +1 -1
  233. package/build/vendor/font_awesome_pro_solid_icons.d.ts +1 -0
  234. package/build/vendor/font_awesome_pro_solid_icons.d.ts.map +1 -1
  235. package/build/vendor/font_awesome_pro_solid_icons.js +5 -0
  236. package/build/vendor/font_awesome_pro_solid_icons.js.map +1 -1
  237. package/package.json +3 -3
  238. package/src/__tests__/hooks/use_can_browse_archived_conversations.test.tsx +98 -0
  239. package/src/__tests__/screens/conversation_filters/components/conversation_filters.test.tsx +132 -0
  240. package/src/__tests__/screens/conversation_filters_screen.test.tsx +76 -0
  241. package/src/__tests__/screens/conversation_new/components/services_form.test.tsx +1 -0
  242. package/src/__tests__/screens/message_actions_screen.test.tsx +65 -2
  243. package/src/__tests__/screens/send_giphy_screen.test.tsx +102 -0
  244. package/src/__tests__/utils/system_messages.ts +19 -32
  245. package/src/components/conversation/__tests__/message_list.test.tsx +68 -2
  246. package/src/components/conversation/__tests__/system_message.test.tsx +106 -0
  247. package/src/components/conversation/jump_to_bottom_button.tsx +4 -3
  248. package/src/components/conversation/message.tsx +3 -0
  249. package/src/components/conversation/message_form.tsx +3 -1
  250. package/src/components/conversation/message_list.tsx +9 -1
  251. package/src/components/conversation/message_reaction.tsx +7 -2
  252. package/src/components/conversation/messages_disabled_banners.tsx +17 -18
  253. package/src/components/conversation/system_message.tsx +44 -16
  254. package/src/components/conversations/__tests__/conversation_actions.test.tsx +67 -0
  255. package/src/components/conversations/__tests__/conversation_preview.test.tsx +105 -0
  256. package/src/components/conversations/__tests__/conversations.test.tsx +93 -0
  257. package/src/components/conversations/__tests__/conversations_blank_state.test.tsx +73 -0
  258. package/src/components/conversations/archived_results_footer.tsx +36 -0
  259. package/src/components/conversations/conversation_actions.tsx +27 -19
  260. package/src/components/conversations/conversation_preview.tsx +39 -10
  261. package/src/components/conversations/conversations.tsx +26 -3
  262. package/src/components/conversations/conversations_blank_state.tsx +21 -0
  263. package/src/components/conversations/filtered_results_footer.tsx +38 -0
  264. package/src/components/display/__tests__/filter_dropdown.test.tsx +114 -0
  265. package/src/components/display/__tests__/icon_tile.test.tsx +16 -0
  266. package/src/components/display/action_button.tsx +4 -3
  267. package/src/components/display/filter_dropdown.tsx +124 -0
  268. package/src/components/display/icon_tile.tsx +67 -0
  269. package/src/components/display/index.ts +2 -0
  270. package/src/contexts/api_provider.tsx +8 -0
  271. package/src/contexts/conversations_context.tsx +2 -1
  272. package/src/hooks/__tests__/use_conversation_archive.test.tsx +152 -0
  273. package/src/hooks/__tests__/use_conversation_jolt_events.test.tsx +74 -0
  274. package/src/hooks/__tests__/use_conversation_messages.test.tsx +10 -2
  275. package/src/hooks/__tests__/use_jolt.test.tsx +100 -1
  276. package/src/hooks/__tests__/use_mark_latest_message_read.test.tsx +21 -7
  277. package/src/hooks/__tests__/use_reduce_motion.test.tsx +61 -0
  278. package/src/hooks/__tests__/use_unarchive_conversation_action.test.tsx +67 -0
  279. package/src/hooks/groups/use_group_members_for_new_conversation.ts +21 -16
  280. package/src/hooks/index.ts +3 -0
  281. package/src/hooks/use_animated_message_background_color.ts +6 -3
  282. package/src/hooks/use_api.ts +9 -2
  283. package/src/hooks/use_can_browse_archived_conversations.ts +17 -0
  284. package/src/hooks/use_clear_conversation_filters.ts +16 -0
  285. package/src/hooks/use_conversation.ts +44 -3
  286. package/src/hooks/use_conversation_messages.ts +7 -5
  287. package/src/hooks/use_current_person.ts +1 -0
  288. package/src/hooks/use_jolt.ts +57 -1
  289. package/src/hooks/use_manual_archiving_enabled.ts +10 -0
  290. package/src/hooks/use_mark_latest_message_read.ts +4 -3
  291. package/src/hooks/use_reduce_motion.ts +19 -0
  292. package/src/hooks/use_unarchive_conversation_action.ts +24 -0
  293. package/src/screens/__tests__/conversation_details_screen.test.tsx +305 -0
  294. package/src/screens/attachment_actions/__tests__/attachment_actions_screen.test.tsx +72 -0
  295. package/src/screens/attachment_actions/attachment_actions_screen.tsx +3 -1
  296. package/src/screens/conversation_details_screen.tsx +80 -16
  297. package/src/screens/conversation_filters/__tests__/conversation_filters.test.tsx +101 -0
  298. package/src/screens/conversation_filters/components/conversation_filters.tsx +40 -7
  299. package/src/screens/conversation_filters/components/rows.tsx +29 -1
  300. package/src/screens/conversation_filters/context/conversation_filter_context.tsx +4 -0
  301. package/src/screens/conversation_filters/filter_types.ts +1 -0
  302. package/src/screens/conversation_filters/screen_props.ts +1 -0
  303. package/src/screens/conversation_filters_screen.tsx +25 -5
  304. package/src/screens/conversation_new/components/groups_form.tsx +1 -0
  305. package/src/screens/conversation_screen.tsx +4 -2
  306. package/src/screens/conversations/__tests__/conversations_screen.test.tsx +52 -0
  307. package/src/screens/conversations/components/__tests__/filter_dropdown_list_header_component.test.tsx +259 -0
  308. package/src/screens/conversations/components/__tests__/list_header_component.test.tsx +21 -4
  309. package/src/screens/conversations/components/filter_dropdown_list_header_component.tsx +286 -0
  310. package/src/screens/conversations/components/list_header_component.tsx +17 -12
  311. package/src/screens/conversations/components/search_list_header_component.tsx +17 -12
  312. package/src/screens/conversations/conversations_screen.tsx +15 -2
  313. package/src/screens/design_system_screen.tsx +26 -0
  314. package/src/screens/message_actions_screen.tsx +29 -20
  315. package/src/screens/search/__tests__/search_screen.test.tsx +13 -1
  316. package/src/screens/search/search_screen.tsx +8 -2
  317. package/src/screens/send_giphy_screen.tsx +6 -3
  318. package/src/screens/team_conversation_screen.tsx +1 -1
  319. package/src/types/jolt_events/message_events.ts +2 -0
  320. package/src/types/resources/conversation.ts +2 -2
  321. package/src/types/resources/member_ability.ts +2 -0
  322. package/src/types/resources/message.ts +3 -0
  323. package/src/types/resources/person.ts +1 -0
  324. package/src/utils/__tests__/conversation.test.ts +1 -0
  325. package/src/utils/__tests__/date.test.ts +23 -0
  326. package/src/utils/__tests__/group_messages.test.ts +1 -0
  327. package/src/utils/archived_conversation.ts +13 -0
  328. package/src/utils/date.ts +8 -0
  329. package/src/utils/index.ts +1 -0
  330. package/src/utils/jolt/__tests__/transform_message_event_data_to_message_resource.test.ts +35 -0
  331. package/src/utils/jolt/transform_message_event_data_to_message_resource.ts +1 -0
  332. package/src/utils/native_adapters/__tests__/menu.test.tsx +46 -0
  333. package/src/utils/native_adapters/configuration.ts +5 -0
  334. package/src/utils/native_adapters/index.ts +1 -0
  335. package/src/utils/native_adapters/menu.tsx +70 -0
  336. package/src/utils/request/conversation.ts +4 -0
  337. package/src/utils/request/messages_data_options.ts +1 -0
  338. package/src/utils/system_messages.ts +19 -10
  339. package/src/vendor/font_awesome_pro_solid_icons.ts +12 -0
@@ -0,0 +1,67 @@
1
+ import type { IconDefinition } from '@fortawesome/fontawesome-svg-core'
2
+ import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'
3
+ import React from 'react'
4
+ import { StyleSheet, View } from 'react-native'
5
+ import { useFontScale, useTheme } from '../../hooks'
6
+ import { MAX_FONT_SIZE_MULTIPLIER } from '../../utils'
7
+ import AvatarPrimitive, { type AvatarRootProps } from '../primitive/avatar_primitive'
8
+
9
+ const ICON_SIZE_PX = 16
10
+ const SQUARE_BORDER_RADIUS_PX = 4
11
+
12
+ interface IconTileProps {
13
+ icon: IconDefinition
14
+ shape: 'circle' | 'square'
15
+ size?: AvatarRootProps['size']
16
+ maxFontSizeMultiplier?: AvatarRootProps['maxFontSizeMultiplier']
17
+ style?: AvatarRootProps['style']
18
+ testID?: string
19
+ }
20
+
21
+ export function IconTile({
22
+ icon,
23
+ shape,
24
+ size = 'lg',
25
+ maxFontSizeMultiplier = MAX_FONT_SIZE_MULTIPLIER,
26
+ style,
27
+ testID,
28
+ }: IconTileProps) {
29
+ const fontScale = useFontScale({ maxFontSizeMultiplier })
30
+ const styles = useStyles({ fontScale })
31
+
32
+ const tile = (
33
+ <View
34
+ style={[styles.tile, shape === 'square' && styles.square]}
35
+ testID={testID}
36
+ accessibilityElementsHidden
37
+ >
38
+ <FontAwesomeIcon icon={icon} size={ICON_SIZE_PX * fontScale} color={styles.icon.color} />
39
+ </View>
40
+ )
41
+
42
+ return (
43
+ <AvatarPrimitive.Root size={size} maxFontSizeMultiplier={maxFontSizeMultiplier} style={style}>
44
+ {shape === 'circle' ? <AvatarPrimitive.Mask>{tile}</AvatarPrimitive.Mask> : tile}
45
+ </AvatarPrimitive.Root>
46
+ )
47
+ }
48
+
49
+ const useStyles = ({ fontScale }: { fontScale: number }) => {
50
+ const { colors } = useTheme()
51
+
52
+ return StyleSheet.create({
53
+ tile: {
54
+ flex: 1,
55
+ alignItems: 'center',
56
+ justifyContent: 'center',
57
+ backgroundColor: colors.fillColorNeutral070,
58
+ },
59
+ square: {
60
+ borderRadius: SQUARE_BORDER_RADIUS_PX * fontScale,
61
+ overflow: 'hidden',
62
+ },
63
+ icon: {
64
+ color: colors.iconColorDefaultPrimary,
65
+ },
66
+ })
67
+ }
@@ -4,6 +4,7 @@ export * from './badge'
4
4
  export * from './conversation_avatar'
5
5
  export * from './emoji_avatar'
6
6
  export * from './icon_avatar'
7
+ export * from './icon_tile'
7
8
  export * from './banner_collapsible'
8
9
  export * from './banner'
9
10
  export * from './button'
@@ -23,6 +24,7 @@ export * from './text_button'
23
24
  export * from './text_inline_button'
24
25
  export * from './text'
25
26
  export * from './toggle_button'
27
+ export * from './filter_dropdown'
26
28
  export * from './keyboard_scroll_view'
27
29
  export * from './keyboard_view'
28
30
  export * from './platform_modal_header_buttons'
@@ -11,6 +11,7 @@ import { ApiClient, useApiClient } from '../hooks/use_api_client'
11
11
  import { useAppState } from '../hooks/use_app_state'
12
12
  import { appGrantsQueryOptions } from '../hooks/use_chat_permissions'
13
13
  import { featuresQueryOptions } from '../hooks/use_features'
14
+ import { useJoltForegroundPing } from '../hooks/use_jolt'
14
15
  import { RequestQueryKey } from '../hooks/use_suspense_api'
15
16
  import { emitAuthRefresh } from '../utils/auth_events'
16
17
  import { ResponseError } from '../utils/response_error'
@@ -73,6 +74,7 @@ export function ApiProvider({ children }: ViewProps) {
73
74
  return (
74
75
  <QueryClientProvider client={chatQueryClient}>
75
76
  {session.isAuthenticated && <PrefetchQueries />}
77
+ <JoltForegroundPing />
76
78
  {children}
77
79
  </QueryClientProvider>
78
80
  )
@@ -90,6 +92,12 @@ const PrefetchQueries = () => {
90
92
  return null
91
93
  }
92
94
 
95
+ const JoltForegroundPing = () => {
96
+ useJoltForegroundPing()
97
+
98
+ return null
99
+ }
100
+
93
101
  export function useFocusManager() {
94
102
  const appState = useAppState()
95
103
 
@@ -45,13 +45,14 @@ export const ConversationsContextProvider = ({
45
45
  opts?: ApiGetQueryOptions
46
46
  }>) => {
47
47
  const [activeConversationId, setActiveConversationId] = useState<number | undefined>()
48
- const { chat_group_graph_id, group_source_app_name, show_direct_messages } = args
48
+ const { chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived } = args
49
49
 
50
50
  const query = useConversations(
51
51
  {
52
52
  chat_group_graph_id,
53
53
  group_source_app_name,
54
54
  show_direct_messages,
55
+ show_archived,
55
56
  },
56
57
  paginatorOptions
57
58
  )
@@ -0,0 +1,152 @@
1
+ import { QueryClientProvider, useQuery } from '@tanstack/react-query'
2
+ import { act, renderHook, waitFor } from '@testing-library/react-native'
3
+ import React from 'react'
4
+ import { Alert } from 'react-native'
5
+ import { buildTestQueryClient } from '../../__utils__/query_client'
6
+ import { getConversationsRequestArgs } from '../../utils/request/conversation'
7
+ import { getConversationRequestArgs, useConversationArchive } from '../use_conversation'
8
+ import { currentPersonQueryKey } from '../use_current_person'
9
+ import { getRequestQueryKey } from '../use_suspense_api'
10
+
11
+ const mockPost = jest.fn()
12
+ jest.mock('../use_api_client', () => ({
13
+ useApiClient: () => ({ chat: { post: mockPost } }),
14
+ }))
15
+
16
+ const conversation_id = 7
17
+ const conversationKey = getRequestQueryKey(getConversationRequestArgs({ conversation_id }))
18
+ const conversationsKey = getRequestQueryKey(getConversationsRequestArgs({ gids: 'all' }))
19
+
20
+ const buildQueryClient = () =>
21
+ buildTestQueryClient({ defaultOptions: { queries: { gcTime: Infinity } } })
22
+
23
+ let queryClient = buildQueryClient()
24
+
25
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
26
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
27
+ )
28
+
29
+ const isInvalidated = (queryKey: unknown[]) =>
30
+ queryClient.getQueryCache().find({ queryKey, exact: true })?.state.isInvalidated
31
+
32
+ describe('useConversationArchive', () => {
33
+ beforeEach(() => {
34
+ queryClient = buildQueryClient()
35
+ queryClient.setQueryData(conversationKey, { data: { id: conversation_id } })
36
+ queryClient.setQueryData(conversationsKey, { data: [] })
37
+ queryClient.setQueryData(currentPersonQueryKey, {
38
+ data: { id: 1, canBrowseArchivedConversations: false },
39
+ })
40
+ mockPost.mockReset()
41
+ jest.spyOn(Alert, 'alert').mockImplementation(() => {})
42
+ })
43
+
44
+ afterEach(() => {
45
+ jest.restoreAllMocks()
46
+ })
47
+
48
+ it.each([
49
+ [true, 'archive'],
50
+ [false, 'unarchive'],
51
+ ])(
52
+ 'posts archived=%s to the %s action, then invalidates the conversation and list',
53
+ async (archived, action) => {
54
+ mockPost.mockResolvedValue({ data: { id: conversation_id } })
55
+ const { result } = renderHook(() => useConversationArchive({ conversation_id }), { wrapper })
56
+
57
+ await act(() => result.current.mutateAsync(archived))
58
+
59
+ expect(mockPost).toHaveBeenCalledWith({
60
+ url: `/me/conversations/7/${action}`,
61
+ data: { data: { type: 'Conversation', attributes: {} } },
62
+ })
63
+ expect(isInvalidated(conversationKey)).toBe(true)
64
+ expect(isInvalidated(conversationsKey)).toBe(true)
65
+ expect(isInvalidated(currentPersonQueryKey)).toBe(false)
66
+ }
67
+ )
68
+
69
+ it.each([
70
+ [true, true],
71
+ [false, false],
72
+ ])(
73
+ 'sets archive access in the person cache to %s after archived=%s',
74
+ async (archived, canBrowse) => {
75
+ mockPost.mockResolvedValue({ data: { id: conversation_id } })
76
+ const { result } = renderHook(() => useConversationArchive({ conversation_id }), { wrapper })
77
+
78
+ await act(() => result.current.mutateAsync(archived))
79
+
80
+ expect(queryClient.getQueryData(currentPersonQueryKey)).toEqual({
81
+ data: { id: 1, canBrowseArchivedConversations: canBrowse },
82
+ })
83
+ }
84
+ )
85
+
86
+ it('stays pending until the conversation refetch finishes', async () => {
87
+ mockPost.mockResolvedValue({ data: { id: conversation_id } })
88
+ let finishRefetch = () => {}
89
+ const { result } = renderHook(
90
+ () => {
91
+ useQuery({
92
+ queryKey: conversationKey,
93
+ queryFn: () =>
94
+ new Promise(resolve => {
95
+ finishRefetch = () => resolve({ data: { id: conversation_id } })
96
+ }),
97
+ staleTime: Infinity,
98
+ })
99
+ return useConversationArchive({ conversation_id })
100
+ },
101
+ { wrapper }
102
+ )
103
+
104
+ let archiving = Promise.resolve()
105
+ await act(async () => {
106
+ archiving = result.current.mutateAsync(true).then(() => {})
107
+ })
108
+
109
+ await waitFor(() => expect(result.current.isPending).toBe(true))
110
+
111
+ await act(async () => {
112
+ finishRefetch()
113
+ await archiving
114
+ })
115
+
116
+ await waitFor(() => expect(result.current.isPending).toBe(false))
117
+ })
118
+
119
+ it("doesn't wait on the list refetch", async () => {
120
+ mockPost.mockResolvedValue({ data: { id: conversation_id } })
121
+ const { result } = renderHook(
122
+ () => {
123
+ useQuery({
124
+ queryKey: conversationsKey,
125
+ queryFn: () => new Promise(() => {}),
126
+ staleTime: Infinity,
127
+ })
128
+ return useConversationArchive({ conversation_id })
129
+ },
130
+ { wrapper }
131
+ )
132
+
133
+ await act(() => result.current.mutateAsync(true))
134
+
135
+ expect(result.current.isPending).toBe(false)
136
+ })
137
+
138
+ it.each([
139
+ [true, 'archive'],
140
+ [false, 'unarchive'],
141
+ ])('alerts when archived=%s fails', async (archived, action) => {
142
+ mockPost.mockRejectedValue(new Error('nope'))
143
+ const { result } = renderHook(() => useConversationArchive({ conversation_id }), { wrapper })
144
+
145
+ await act(() => result.current.mutateAsync(archived).catch(() => {}))
146
+
147
+ expect(Alert.alert).toHaveBeenCalledWith(
148
+ 'Oops',
149
+ `Couldn't ${action} this conversation. Please try again.`
150
+ )
151
+ })
152
+ })
@@ -0,0 +1,74 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
2
+ import { renderHook } from '@testing-library/react-native'
3
+ import React from 'react'
4
+ import { buildTestQueryClient } from '../../__utils__/query_client'
5
+ import type { ApiResource, ConversationResource } from '../../types'
6
+ import { getConversationRequestArgs } from '../use_conversation'
7
+ import { useConversationJoltEvents } from '../use_conversation_jolt_events'
8
+ import { getRequestQueryKey } from '../use_suspense_api'
9
+
10
+ const handlers: Record<string, (event: unknown) => void> = {}
11
+ jest.mock('../use_jolt', () => ({
12
+ useJoltChannel: () => 'channel',
13
+ useJoltEvent: (_channel: unknown, name: string, handler: (event: unknown) => void) => {
14
+ handlers[name] = handler
15
+ },
16
+ }))
17
+ jest.mock('../use_current_person', () => ({ useCurrentPerson: () => ({ id: 1 }) }))
18
+ jest.mock('../use_api_client', () => ({ useApiClient: () => ({ chat: {} }) }))
19
+
20
+ const conversationId = 7
21
+ const queryKey = getRequestQueryKey(getConversationRequestArgs({ conversation_id: conversationId }))
22
+ let queryClient: QueryClient
23
+
24
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
25
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
26
+ )
27
+
28
+ const archivedAttributes = {
29
+ disabled: true,
30
+ disabled_reason: 'archived',
31
+ disabled_message: 'This conversation is archived.',
32
+ }
33
+ const activeAttributes = { disabled: false, disabled_reason: null, disabled_message: null }
34
+
35
+ const seed = (conversation: Partial<ConversationResource>) =>
36
+ queryClient.setQueryData<ApiResource<ConversationResource>>(queryKey, {
37
+ data: { type: 'Conversation', id: conversationId, title: 'Youth', ...conversation },
38
+ } as ApiResource<ConversationResource>)
39
+
40
+ const cached = () => queryClient.getQueryData<ApiResource<ConversationResource>>(queryKey)!.data
41
+
42
+ const conversationUpdated = (attributes: Record<string, unknown>) =>
43
+ handlers['conversation.updated']({
44
+ event: 'conversation.updated',
45
+ data: { data: { id: conversationId, ...attributes } },
46
+ })
47
+
48
+ beforeEach(() => {
49
+ queryClient = buildTestQueryClient()
50
+ renderHook(() => useConversationJoltEvents({ conversationId }), { wrapper })
51
+ })
52
+
53
+ describe('useConversationJoltEvents on conversation.updated', () => {
54
+ it('switches the cached conversation into the archived state', () => {
55
+ seed({ disabled: false, disabledReason: null, disabledMessage: null })
56
+
57
+ conversationUpdated(archivedAttributes)
58
+
59
+ expect(cached()).toMatchObject({
60
+ title: 'Youth',
61
+ disabled: true,
62
+ disabledReason: 'archived',
63
+ disabledMessage: 'This conversation is archived.',
64
+ })
65
+ })
66
+
67
+ it('switches the cached conversation back out of the archived state', () => {
68
+ seed({ disabled: true, disabledReason: 'archived', disabledMessage: 'Archived' })
69
+
70
+ conversationUpdated(activeAttributes)
71
+
72
+ expect(cached()).toMatchObject({ disabled: false, disabledReason: null, disabledMessage: null })
73
+ })
74
+ })
@@ -101,8 +101,8 @@ describe('useConversationMessages', () => {
101
101
  await waitFor(() => expect(result.current.messages.map(m => m.id)).toContain(anchor))
102
102
  })
103
103
 
104
- it('reuses a cached window that already holds the anchor', async () => {
105
- seed(livePageParam, ['Z', anchor])
104
+ it('reuses a cached window that was opened at the anchor', async () => {
105
+ seed(anchoredPageParam(anchor), ['Z', anchor], behindMeta)
106
106
 
107
107
  const { result } = render({ conversation_id, anchor })
108
108
 
@@ -110,6 +110,14 @@ describe('useConversationMessages', () => {
110
110
  expect(mockGet).not.toHaveBeenCalled()
111
111
  })
112
112
 
113
+ it('refetches around the anchor when a live window still holds a stale copy of it', async () => {
114
+ seed(livePageParam, ['Z', anchor])
115
+
116
+ render({ conversation_id, anchor })
117
+
118
+ await waitFor(() => expect(requestedWhere()).toEqual([{ id_around: anchor }]))
119
+ })
120
+
113
121
  it('keeps a deleted anchor so the list can mark where it was', async () => {
114
122
  const deleted = {
115
123
  ...message(anchor),
@@ -1,12 +1,15 @@
1
+ import JoltClient from '@planningcenter/jolt-client'
2
+ import { JoltSubscription } from '@planningcenter/jolt-client/dist/types/JoltSubscription'
1
3
  import { QueryClientProvider } from '@tanstack/react-query'
2
4
  import { render, waitFor } from '@testing-library/react-native'
3
5
  import React, { PropsWithChildren, Suspense } from 'react'
6
+ import { AppState, AppStateStatus } from 'react-native'
4
7
  import { buildTestQueryClient } from '../../__utils__/query_client'
5
8
  import { ChatContext, ChatContextValue } from '../../contexts/chat_context'
6
9
  import { Session } from '../../utils'
7
10
  import { Client } from '../../utils/client'
8
11
  import { ResponseError } from '../../utils/response_error'
9
- import { useJoltClient } from '../use_jolt'
12
+ import { useJoltClient, useJoltForegroundPing } from '../use_jolt'
10
13
 
11
14
  let caught: unknown
12
15
 
@@ -68,3 +71,99 @@ describe('useJoltClient', () => {
68
71
  expect((caught as ResponseError).status).toBe(401)
69
72
  })
70
73
  })
74
+
75
+ describe('useJoltForegroundPing', () => {
76
+ const addEventListener = AppState.addEventListener as jest.Mock
77
+ const confirmedSubscription = {} as JoltSubscription
78
+
79
+ const PingProbe = () => {
80
+ useJoltForegroundPing()
81
+ return null
82
+ }
83
+
84
+ const renderForegroundPing = (joltClient: Partial<JoltClient>, { mounts = 1 } = {}) => {
85
+ const queryClient = buildTestQueryClient({ defaultOptions: { queries: { gcTime: Infinity } } })
86
+ queryClient.setQueryData(['jolt-client', 'wss://jolt.test', 'a-real-token'], joltClient)
87
+
88
+ const result = render(
89
+ <QueryClientProvider client={queryClient}>
90
+ {Array.from({ length: mounts }, (_, index) => (
91
+ <PingProbe key={index} />
92
+ ))}
93
+ </QueryClientProvider>
94
+ )
95
+
96
+ const [, onChange] = addEventListener.mock.calls.at(-1) as [
97
+ string,
98
+ (_state: AppStateStatus) => void,
99
+ ]
100
+
101
+ return { onChange, unmount: result.unmount }
102
+ }
103
+
104
+ beforeEach(() => {
105
+ addEventListener.mockClear()
106
+ })
107
+
108
+ it('pings the jolt client once when the app returns to the foreground', () => {
109
+ const ping = jest.fn()
110
+ const { onChange } = renderForegroundPing({ ping, subscriptions: [confirmedSubscription] })
111
+
112
+ onChange('background')
113
+ onChange('active')
114
+
115
+ expect(ping).toHaveBeenCalledTimes(1)
116
+ })
117
+
118
+ it('registers one listener and pings once no matter how many providers mount it', () => {
119
+ const ping = jest.fn()
120
+ const remove = jest.fn()
121
+ addEventListener.mockReturnValueOnce({ remove })
122
+ const { onChange, unmount } = renderForegroundPing(
123
+ { ping, subscriptions: [confirmedSubscription] },
124
+ { mounts: 3 }
125
+ )
126
+
127
+ onChange('background')
128
+ onChange('active')
129
+
130
+ expect(addEventListener).toHaveBeenCalledTimes(1)
131
+ expect(ping).toHaveBeenCalledTimes(1)
132
+
133
+ unmount()
134
+
135
+ expect(remove).toHaveBeenCalledTimes(1)
136
+ })
137
+
138
+ it('does not ping when there are no confirmed subscriptions', () => {
139
+ const ping = jest.fn()
140
+ const { onChange } = renderForegroundPing({ ping, subscriptions: [] })
141
+
142
+ onChange('background')
143
+ onChange('active')
144
+
145
+ expect(ping).not.toHaveBeenCalled()
146
+ })
147
+
148
+ it('does not ping when the app leaves the foreground', () => {
149
+ const ping = jest.fn()
150
+ const { onChange } = renderForegroundPing({ ping, subscriptions: [confirmedSubscription] })
151
+
152
+ onChange('inactive')
153
+ onChange('background')
154
+
155
+ expect(ping).not.toHaveBeenCalled()
156
+ })
157
+
158
+ it('swallows errors from pinging a closed socket', () => {
159
+ const ping = jest.fn(() => {
160
+ throw new Error('No WebSocket')
161
+ })
162
+ const { onChange } = renderForegroundPing({ ping, subscriptions: [confirmedSubscription] })
163
+
164
+ onChange('background')
165
+
166
+ expect(() => onChange('active')).not.toThrow()
167
+ expect(ping).toHaveBeenCalledTimes(1)
168
+ })
169
+ })
@@ -1,4 +1,4 @@
1
- import { renderHook } from '@testing-library/react-native'
1
+ import { act, renderHook } from '@testing-library/react-native'
2
2
  import type { ConversationResource } from '../../types'
3
3
  import { useMarkLatestMessageRead } from '../use_mark_latest_message_read'
4
4
 
@@ -19,15 +19,29 @@ describe('useMarkLatestMessageRead', () => {
19
19
  expect(mockMarkRead).toHaveBeenCalledTimes(1)
20
20
  })
21
21
 
22
- it('waits while disabled and marks read once enabled', () => {
23
- const { rerender } = renderHook<void, { enabled: boolean }>(
24
- ({ enabled }) => useMarkLatestMessageRead({ conversation, messages: [], enabled }),
25
- { initialProps: { enabled: false } }
22
+ it('marks read once on mount even while disabled', () => {
23
+ renderHook(() => useMarkLatestMessageRead({ conversation, messages: [], enabled: false }))
24
+
25
+ expect(mockMarkRead).toHaveBeenCalledTimes(1)
26
+ })
27
+
28
+ it('ignores unread bumps while disabled and marks read once enabled', () => {
29
+ jest.useFakeTimers()
30
+ const { rerender } = renderHook<void, { conversation: ConversationResource; enabled: boolean }>(
31
+ props => useMarkLatestMessageRead({ ...props, messages: [] }),
32
+ { initialProps: { conversation: { ...conversation, unreadCount: 0 }, enabled: false } }
26
33
  )
27
- expect(mockMarkRead).not.toHaveBeenCalled()
34
+ expect(mockMarkRead).toHaveBeenCalledTimes(1)
28
35
 
29
- rerender({ enabled: true })
36
+ rerender({ conversation: { ...conversation, unreadCount: 2 }, enabled: false })
37
+ act(() => jest.advanceTimersByTime(1000))
30
38
 
31
39
  expect(mockMarkRead).toHaveBeenCalledTimes(1)
40
+
41
+ rerender({ conversation: { ...conversation, unreadCount: 2 }, enabled: true })
42
+ act(() => jest.advanceTimersByTime(1000))
43
+
44
+ expect(mockMarkRead).toHaveBeenCalledTimes(2)
45
+ jest.useRealTimers()
32
46
  })
33
47
  })
@@ -0,0 +1,61 @@
1
+ import { act, renderHook, waitFor } from '@testing-library/react-native'
2
+ import { AccessibilityInfo, EmitterSubscription } from 'react-native'
3
+ import * as Reanimated from 'react-native-reanimated'
4
+ import { useReduceMotion } from '../use_reduce_motion'
5
+
6
+ type ReduceMotionListener = (enabled: boolean) => void
7
+
8
+ describe('useReduceMotion', () => {
9
+ let listener: ReduceMotionListener | undefined
10
+ const remove = jest.fn()
11
+
12
+ beforeEach(() => {
13
+ listener = undefined
14
+ remove.mockClear()
15
+ jest.spyOn(Reanimated, 'useReducedMotion').mockReturnValue(false)
16
+ jest.spyOn(AccessibilityInfo, 'isReduceMotionEnabled').mockResolvedValue(true)
17
+ const addEventListener: typeof AccessibilityInfo.addEventListener = (_event, handler) => {
18
+ listener = handler as ReduceMotionListener
19
+ return { remove } as unknown as EmitterSubscription
20
+ }
21
+ jest.spyOn(AccessibilityInfo, 'addEventListener').mockImplementation(addEventListener)
22
+ })
23
+
24
+ afterEach(() => jest.restoreAllMocks())
25
+
26
+ it('seeds synchronously from the launch-time setting', async () => {
27
+ jest.spyOn(Reanimated, 'useReducedMotion').mockReturnValue(true)
28
+ jest.mocked(AccessibilityInfo.isReduceMotionEnabled).mockResolvedValue(false)
29
+
30
+ const { result } = renderHook(() => useReduceMotion())
31
+
32
+ expect(result.current).toBe(true)
33
+ await waitFor(() => expect(result.current).toBe(false))
34
+ })
35
+
36
+ it('updates to the live setting once it resolves', async () => {
37
+ const { result } = renderHook(() => useReduceMotion())
38
+
39
+ expect(result.current).toBe(false)
40
+ await waitFor(() => expect(result.current).toBe(true))
41
+ })
42
+
43
+ it('follows reduceMotionChanged while mounted', async () => {
44
+ const { result } = renderHook(() => useReduceMotion())
45
+ await waitFor(() => expect(result.current).toBe(true))
46
+
47
+ act(() => listener?.(false))
48
+ expect(result.current).toBe(false)
49
+
50
+ act(() => listener?.(true))
51
+ expect(result.current).toBe(true)
52
+ })
53
+
54
+ it('removes the listener on unmount', () => {
55
+ const { unmount } = renderHook(() => useReduceMotion())
56
+
57
+ expect(remove).not.toHaveBeenCalled()
58
+ unmount()
59
+ expect(remove).toHaveBeenCalledTimes(1)
60
+ })
61
+ })
@@ -0,0 +1,67 @@
1
+ import { act, renderHook } from '@testing-library/react-native'
2
+ import type { ConversationResource } from '../../types'
3
+ import { availableFeatures } from '../use_features'
4
+ import { useUnarchiveConversationAction } from '../use_unarchive_conversation_action'
5
+
6
+ const mockSetArchived = jest.fn()
7
+ jest.mock('../use_conversation', () => ({
8
+ useConversationArchive: () => ({ setArchived: mockSetArchived, isPending: false }),
9
+ }))
10
+
11
+ let mockEnabledFeatures: string[] = []
12
+ jest.mock('../use_features', () => ({
13
+ ...jest.requireActual('../use_features'),
14
+ useFeatures: () => ({
15
+ features: [],
16
+ featureEnabled: (name: string) => mockEnabledFeatures.includes(name),
17
+ }),
18
+ }))
19
+
20
+ const conversation = ({
21
+ disabledReason = 'archived',
22
+ canUnarchive = true,
23
+ }: {
24
+ disabledReason?: string | null
25
+ canUnarchive?: boolean
26
+ } = {}) =>
27
+ ({
28
+ type: 'Conversation',
29
+ id: 7,
30
+ disabled: !!disabledReason,
31
+ disabledReason,
32
+ memberAbility: { type: 'MemberAbility', canUnarchive },
33
+ }) as unknown as ConversationResource
34
+
35
+ const renderAction = (resource: ConversationResource) =>
36
+ renderHook(() => useUnarchiveConversationAction(resource)).result.current
37
+
38
+ describe('useUnarchiveConversationAction', () => {
39
+ beforeEach(() => {
40
+ mockEnabledFeatures = [availableFeatures.manual_archiving_rollout]
41
+ jest.clearAllMocks()
42
+ })
43
+
44
+ it('unarchives an archived conversation the person can unarchive', () => {
45
+ const action = renderAction(conversation())
46
+
47
+ expect(action?.label).toBe('Unarchive')
48
+
49
+ act(() => action?.onPress())
50
+
51
+ expect(mockSetArchived).toHaveBeenCalledWith(false)
52
+ })
53
+
54
+ it.each([
55
+ ['neither mobile flag is on', () => (mockEnabledFeatures = []), conversation()],
56
+ ['the person cannot unarchive', () => {}, conversation({ canUnarchive: false })],
57
+ [
58
+ 'the conversation is disabled for another reason',
59
+ () => {},
60
+ conversation({ disabledReason: 'safety_check' }),
61
+ ],
62
+ ])('offers nothing when %s', (_case, setup, resource) => {
63
+ setup()
64
+
65
+ expect(renderAction(resource)).toBeUndefined()
66
+ })
67
+ })