@planningcenter/chat-react-native 3.38.0-rc.1 → 3.38.0-rc.11

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 (223) hide show
  1. package/build/components/conversation/jump_to_bottom_button.d.ts +1 -2
  2. package/build/components/conversation/jump_to_bottom_button.d.ts.map +1 -1
  3. package/build/components/conversation/jump_to_bottom_button.js +7 -39
  4. package/build/components/conversation/jump_to_bottom_button.js.map +1 -1
  5. package/build/components/conversation/message_list.d.ts +10 -0
  6. package/build/components/conversation/message_list.d.ts.map +1 -0
  7. package/build/components/conversation/message_list.js +13 -0
  8. package/build/components/conversation/message_list.js.map +1 -0
  9. package/build/components/conversation/reply_shadow_message.d.ts +2 -1
  10. package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
  11. package/build/components/conversation/reply_shadow_message.js.map +1 -1
  12. package/build/components/display/conversation_avatar.d.ts +2 -1
  13. package/build/components/display/conversation_avatar.d.ts.map +1 -1
  14. package/build/components/display/conversation_avatar.js +6 -5
  15. package/build/components/display/conversation_avatar.js.map +1 -1
  16. package/build/components/display/emoji_avatar.d.ts +3 -1
  17. package/build/components/display/emoji_avatar.d.ts.map +1 -1
  18. package/build/components/display/emoji_avatar.js +2 -2
  19. package/build/components/display/emoji_avatar.js.map +1 -1
  20. package/build/components/display/icon_avatar.d.ts +3 -1
  21. package/build/components/display/icon_avatar.d.ts.map +1 -1
  22. package/build/components/display/icon_avatar.js +2 -2
  23. package/build/components/display/icon_avatar.js.map +1 -1
  24. package/build/contexts/conversation_context.d.ts +1 -8
  25. package/build/contexts/conversation_context.d.ts.map +1 -1
  26. package/build/contexts/conversation_context.js +3 -21
  27. package/build/contexts/conversation_context.js.map +1 -1
  28. package/build/hooks/groups/use_group_chat_conversation_payload.d.ts.map +1 -1
  29. package/build/hooks/groups/use_group_chat_conversation_payload.js +1 -0
  30. package/build/hooks/groups/use_group_chat_conversation_payload.js.map +1 -1
  31. package/build/hooks/index.d.ts +1 -0
  32. package/build/hooks/index.d.ts.map +1 -1
  33. package/build/hooks/index.js +1 -0
  34. package/build/hooks/index.js.map +1 -1
  35. package/build/hooks/use_conversation_messages.d.ts +6 -15
  36. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  37. package/build/hooks/use_conversation_messages.js +9 -62
  38. package/build/hooks/use_conversation_messages.js.map +1 -1
  39. package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
  40. package/build/hooks/use_conversation_messages_jolt_events.js +4 -4
  41. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  42. package/build/hooks/use_conversations_actions.d.ts +0 -5
  43. package/build/hooks/use_conversations_actions.d.ts.map +1 -1
  44. package/build/hooks/use_conversations_actions.js +0 -12
  45. package/build/hooks/use_conversations_actions.js.map +1 -1
  46. package/build/hooks/use_features.d.ts +0 -1
  47. package/build/hooks/use_features.d.ts.map +1 -1
  48. package/build/hooks/use_features.js +0 -1
  49. package/build/hooks/use_features.js.map +1 -1
  50. package/build/hooks/use_mark_latest_message_read.d.ts +1 -1
  51. package/build/hooks/use_mark_latest_message_read.d.ts.map +1 -1
  52. package/build/hooks/use_mark_latest_message_read.js +1 -17
  53. package/build/hooks/use_mark_latest_message_read.js.map +1 -1
  54. package/build/hooks/use_preview_avatar_diameter.d.ts +2 -0
  55. package/build/hooks/use_preview_avatar_diameter.d.ts.map +1 -0
  56. package/build/hooks/use_preview_avatar_diameter.js +11 -0
  57. package/build/hooks/use_preview_avatar_diameter.js.map +1 -0
  58. package/build/hooks/use_suspense_api.d.ts +0 -1
  59. package/build/hooks/use_suspense_api.d.ts.map +1 -1
  60. package/build/hooks/use_suspense_api.js +1 -1
  61. package/build/hooks/use_suspense_api.js.map +1 -1
  62. package/build/jest.js +1 -1
  63. package/build/jest.js.map +1 -1
  64. package/build/screens/avatar_picker/avatar_picker_screen.d.ts.map +1 -1
  65. package/build/screens/avatar_picker/avatar_picker_screen.js +11 -9
  66. package/build/screens/avatar_picker/avatar_picker_screen.js.map +1 -1
  67. package/build/screens/avatar_picker/avatar_preview.d.ts.map +1 -1
  68. package/build/screens/avatar_picker/avatar_preview.js +13 -5
  69. package/build/screens/avatar_picker/avatar_preview.js.map +1 -1
  70. package/build/screens/avatar_picker/emoji_tab.d.ts.map +1 -1
  71. package/build/screens/avatar_picker/emoji_tab.js +3 -7
  72. package/build/screens/avatar_picker/emoji_tab.js.map +1 -1
  73. package/build/screens/avatar_picker/upload_tab.d.ts.map +1 -1
  74. package/build/screens/avatar_picker/upload_tab.js +2 -1
  75. package/build/screens/avatar_picker/upload_tab.js.map +1 -1
  76. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  77. package/build/screens/conversation_details_screen.js +5 -2
  78. package/build/screens/conversation_details_screen.js.map +1 -1
  79. package/build/screens/conversation_filter_recipients/components/header_row.d.ts.map +1 -1
  80. package/build/screens/conversation_filter_recipients/components/header_row.js +3 -2
  81. package/build/screens/conversation_filter_recipients/components/header_row.js.map +1 -1
  82. package/build/screens/conversation_filter_recipients/hooks/use_flattened_array_of_service_types_with_teams.d.ts.map +1 -1
  83. package/build/screens/conversation_filter_recipients/hooks/use_flattened_array_of_service_types_with_teams.js +47 -18
  84. package/build/screens/conversation_filter_recipients/hooks/use_flattened_array_of_service_types_with_teams.js.map +1 -1
  85. package/build/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.d.ts +2 -1
  86. package/build/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.d.ts.map +1 -1
  87. package/build/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.js +23 -26
  88. package/build/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.js.map +1 -1
  89. package/build/screens/conversation_filter_recipients/types.d.ts +1 -1
  90. package/build/screens/conversation_filter_recipients/types.d.ts.map +1 -1
  91. package/build/screens/conversation_filter_recipients/types.js.map +1 -1
  92. package/build/screens/conversation_screen.d.ts +0 -1
  93. package/build/screens/conversation_screen.d.ts.map +1 -1
  94. package/build/screens/conversation_screen.js +45 -96
  95. package/build/screens/conversation_screen.js.map +1 -1
  96. package/build/screens/conversation_select_recipients/components/recipient_link_row.d.ts +1 -1
  97. package/build/screens/conversation_select_recipients/components/recipient_link_row.d.ts.map +1 -1
  98. package/build/screens/conversation_select_recipients/components/recipient_link_row.js +3 -3
  99. package/build/screens/conversation_select_recipients/components/recipient_link_row.js.map +1 -1
  100. package/build/screens/conversation_select_recipients/components/team_recipient_row.d.ts.map +1 -1
  101. package/build/screens/conversation_select_recipients/components/team_recipient_row.js +1 -1
  102. package/build/screens/conversation_select_recipients/components/team_recipient_row.js.map +1 -1
  103. package/build/screens/team_conversation_screen.d.ts.map +1 -1
  104. package/build/screens/team_conversation_screen.js +24 -1
  105. package/build/screens/team_conversation_screen.js.map +1 -1
  106. package/build/utils/cache/messages_cache.d.ts +0 -1
  107. package/build/utils/cache/messages_cache.d.ts.map +1 -1
  108. package/build/utils/cache/messages_cache.js +0 -4
  109. package/build/utils/cache/messages_cache.js.map +1 -1
  110. package/build/utils/client/client.d.ts +1 -1
  111. package/build/utils/client/client.d.ts.map +1 -1
  112. package/build/utils/client/client.js +7 -6
  113. package/build/utils/client/client.js.map +1 -1
  114. package/build/utils/client/instrumented_fetch.js +3 -5
  115. package/build/utils/client/instrumented_fetch.js.map +1 -1
  116. package/build/utils/group_messages.d.ts +2 -9
  117. package/build/utils/group_messages.d.ts.map +1 -1
  118. package/build/utils/group_messages.js +1 -20
  119. package/build/utils/group_messages.js.map +1 -1
  120. package/package.json +4 -4
  121. package/src/__tests__/hooks/use_group_chat_conversation_payload.test.tsx +50 -0
  122. package/src/__tests__/jest.ts +1 -1
  123. package/src/__tests__/utils/client.ts +32 -0
  124. package/src/components/conversation/__tests__/message_list.test.tsx +14 -0
  125. package/src/components/conversation/jump_to_bottom_button.tsx +8 -57
  126. package/src/components/conversation/message_list.tsx +42 -0
  127. package/src/components/conversation/reply_shadow_message.tsx +1 -1
  128. package/src/components/display/conversation_avatar.tsx +7 -5
  129. package/src/components/display/emoji_avatar.tsx +10 -2
  130. package/src/components/display/icon_avatar.tsx +10 -2
  131. package/src/contexts/conversation_context.tsx +2 -30
  132. package/src/hooks/groups/use_group_chat_conversation_payload.ts +1 -0
  133. package/src/hooks/index.ts +1 -0
  134. package/src/hooks/use_conversation_messages.ts +20 -120
  135. package/src/hooks/use_conversation_messages_jolt_events.ts +3 -4
  136. package/src/hooks/use_conversations_actions.ts +0 -15
  137. package/src/hooks/use_features.ts +0 -1
  138. package/src/hooks/use_mark_latest_message_read.ts +2 -16
  139. package/src/hooks/use_preview_avatar_diameter.ts +12 -0
  140. package/src/hooks/use_suspense_api.ts +1 -1
  141. package/src/jest.ts +1 -1
  142. package/src/screens/avatar_picker/avatar_picker_screen.tsx +25 -9
  143. package/src/screens/avatar_picker/avatar_preview.tsx +14 -5
  144. package/src/screens/avatar_picker/emoji_tab.tsx +3 -6
  145. package/src/screens/avatar_picker/upload_tab.tsx +2 -0
  146. package/src/screens/conversation_details_screen.tsx +10 -1
  147. package/src/screens/conversation_filter_recipients/components/header_row.tsx +3 -2
  148. package/src/screens/conversation_filter_recipients/hooks/__tests__/use_service_types_with_teams.test.ts +108 -0
  149. package/src/screens/conversation_filter_recipients/hooks/use_flattened_array_of_service_types_with_teams.tsx +46 -19
  150. package/src/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.ts +31 -29
  151. package/src/screens/conversation_filter_recipients/types.tsx +1 -1
  152. package/src/screens/conversation_screen.tsx +69 -186
  153. package/src/screens/conversation_select_recipients/components/recipient_link_row.tsx +6 -4
  154. package/src/screens/conversation_select_recipients/components/team_recipient_row.tsx +2 -1
  155. package/src/screens/team_conversation_screen.tsx +33 -1
  156. package/src/utils/__tests__/group_messages.test.ts +0 -71
  157. package/src/utils/cache/messages_cache.ts +0 -5
  158. package/src/utils/client/__tests__/instrumented_fetch.test.ts +9 -5
  159. package/src/utils/client/client.ts +9 -7
  160. package/src/utils/client/instrumented_fetch.ts +3 -6
  161. package/src/utils/group_messages.ts +2 -42
  162. package/build/components/conversation/unread_divider.d.ts +0 -6
  163. package/build/components/conversation/unread_divider.d.ts.map +0 -1
  164. package/build/components/conversation/unread_divider.js +0 -59
  165. package/build/components/conversation/unread_divider.js.map +0 -1
  166. package/build/hooks/use_flat_list_viewability.d.ts +0 -20
  167. package/build/hooks/use_flat_list_viewability.d.ts.map +0 -1
  168. package/build/hooks/use_flat_list_viewability.js +0 -30
  169. package/build/hooks/use_flat_list_viewability.js.map +0 -1
  170. package/build/hooks/use_jump_to_bottom_action.d.ts +0 -9
  171. package/build/hooks/use_jump_to_bottom_action.d.ts.map +0 -1
  172. package/build/hooks/use_jump_to_bottom_action.js +0 -62
  173. package/build/hooks/use_jump_to_bottom_action.js.map +0 -1
  174. package/build/hooks/use_jump_to_unread_anchor.d.ts +0 -20
  175. package/build/hooks/use_jump_to_unread_anchor.d.ts.map +0 -1
  176. package/build/hooks/use_jump_to_unread_anchor.js +0 -53
  177. package/build/hooks/use_jump_to_unread_anchor.js.map +0 -1
  178. package/build/hooks/use_jump_to_unread_gates.d.ts +0 -5
  179. package/build/hooks/use_jump_to_unread_gates.d.ts.map +0 -1
  180. package/build/hooks/use_jump_to_unread_gates.js +0 -10
  181. package/build/hooks/use_jump_to_unread_gates.js.map +0 -1
  182. package/build/hooks/use_scroll_tracking.d.ts +0 -13
  183. package/build/hooks/use_scroll_tracking.d.ts.map +0 -1
  184. package/build/hooks/use_scroll_tracking.js +0 -45
  185. package/build/hooks/use_scroll_tracking.js.map +0 -1
  186. package/build/hooks/use_track_highest_seen_message.d.ts +0 -4
  187. package/build/hooks/use_track_highest_seen_message.d.ts.map +0 -1
  188. package/build/hooks/use_track_highest_seen_message.js +0 -35
  189. package/build/hooks/use_track_highest_seen_message.js.map +0 -1
  190. package/build/utils/conversation_messages.d.ts +0 -10
  191. package/build/utils/conversation_messages.d.ts.map +0 -1
  192. package/build/utils/conversation_messages.js +0 -22
  193. package/build/utils/conversation_messages.js.map +0 -1
  194. package/build/utils/highest_seen_tracker.d.ts +0 -12
  195. package/build/utils/highest_seen_tracker.d.ts.map +0 -1
  196. package/build/utils/highest_seen_tracker.js +0 -37
  197. package/build/utils/highest_seen_tracker.js.map +0 -1
  198. package/build/utils/message_viewability.d.ts +0 -24
  199. package/build/utils/message_viewability.d.ts.map +0 -1
  200. package/build/utils/message_viewability.js +0 -29
  201. package/build/utils/message_viewability.js.map +0 -1
  202. package/build/utils/unread_divider_helpers.d.ts +0 -18
  203. package/build/utils/unread_divider_helpers.d.ts.map +0 -1
  204. package/build/utils/unread_divider_helpers.js +0 -13
  205. package/build/utils/unread_divider_helpers.js.map +0 -1
  206. package/src/__tests__/hooks/use_conversation_messages.test.tsx +0 -109
  207. package/src/__tests__/hooks/use_mark_latest_message_read.test.tsx +0 -154
  208. package/src/__tests__/utils/cache/messages_cache.test.ts +0 -54
  209. package/src/components/conversation/unread_divider.tsx +0 -90
  210. package/src/hooks/use_flat_list_viewability.ts +0 -50
  211. package/src/hooks/use_jump_to_bottom_action.ts +0 -75
  212. package/src/hooks/use_jump_to_unread_anchor.ts +0 -68
  213. package/src/hooks/use_jump_to_unread_gates.ts +0 -10
  214. package/src/hooks/use_scroll_tracking.ts +0 -64
  215. package/src/hooks/use_track_highest_seen_message.ts +0 -43
  216. package/src/utils/__tests__/conversation_messages.test.ts +0 -105
  217. package/src/utils/__tests__/highest_seen_tracker.test.ts +0 -82
  218. package/src/utils/__tests__/message_viewability.test.ts +0 -168
  219. package/src/utils/__tests__/unread_divider_helpers.test.ts +0 -85
  220. package/src/utils/conversation_messages.ts +0 -37
  221. package/src/utils/highest_seen_tracker.ts +0 -42
  222. package/src/utils/message_viewability.ts +0 -49
  223. package/src/utils/unread_divider_helpers.ts +0 -25
@@ -1,128 +1,28 @@
1
- import {
2
- AnyUseSuspenseInfiniteQueryOptions,
3
- InfiniteData,
4
- useQueryClient,
5
- useSuspenseInfiniteQuery,
6
- useSuspenseQueries,
7
- } from '@tanstack/react-query'
8
- import { useCallback, useMemo } from 'react'
9
- import { useConversationContext } from '../contexts/conversation_context'
10
- import { ApiCollection, MessageResource } from '../types'
11
- import {
12
- anchoredSeedPageParams,
13
- MessagesPageParam,
14
- newerPageParam,
15
- olderPageParam,
16
- sortAndFilterMessages,
17
- } from '../utils/conversation_messages'
1
+ import { useMemo } from 'react'
2
+ import { MessageResource } from '../types'
18
3
  import { getMessagesQueryKey, getMessagesRequestArgs } from '../utils/request/get_messages'
19
- import { useApiClient } from './use_api_client'
20
- import { throwResponseError } from './use_suspense_api'
21
-
22
- type Args = { conversation_id: number; reply_root_id?: string | null }
23
-
24
- export type ConversationMessagesOptions = Omit<
25
- AnyUseSuspenseInfiniteQueryOptions,
26
- | 'getNextPageParam'
27
- | 'getPreviousPageParam'
28
- | 'initialData'
29
- | 'initialPageParam'
30
- | 'queryFn'
31
- | 'queryKey'
32
- >
4
+ import { SuspensePaginatorOptions, useSuspensePaginator } from './use_suspense_api'
33
5
 
34
6
  export const useConversationMessages = (
35
- { conversation_id, reply_root_id }: Args,
36
- opts?: ConversationMessagesOptions
7
+ { conversation_id, reply_root_id }: { conversation_id: number; reply_root_id?: string | null },
8
+ opts?: SuspensePaginatorOptions
37
9
  ) => {
38
- const apiClient = useApiClient()
39
- const { initialMessageId } = useConversationContext()
40
- const anchored = !reply_root_id && !!initialMessageId
41
-
42
- const requestArgs = useMemo(
43
- () => getMessagesRequestArgs({ conversation_id, reply_root_id }),
44
- [conversation_id, reply_root_id]
10
+ const { data, refetch, isRefetching, fetchNextPage } = useSuspensePaginator<MessageResource>(
11
+ getMessagesRequestArgs({ conversation_id, reply_root_id }),
12
+ opts
45
13
  )
46
- const queryKey = useMemo(
47
- () => getMessagesQueryKey({ conversation_id, reply_root_id }),
48
- [conversation_id, reply_root_id]
49
- )
50
-
51
- const fetchPage = (pageParam: MessagesPageParam) => {
52
- const data = {
53
- ...requestArgs.data,
54
- ...(pageParam.where ? { where: pageParam.where } : {}),
55
- ...(pageParam.order ? { order: pageParam.order } : {}),
56
- }
57
- return apiClient.chat
58
- .get<ApiCollection<MessageResource>>({ url: requestArgs.url, data })
59
- .catch(throwResponseError)
60
- }
61
-
62
- const seedPageParams = anchored ? anchoredSeedPageParams(initialMessageId) : []
63
- const seedQueries = useSuspenseQueries({
64
- queries: seedPageParams.map((pageParam, index) => ({
65
- queryKey: [...queryKey, 'seed', index],
66
- queryFn: () => fetchPage(pageParam),
67
- staleTime: Infinity,
68
- gcTime: 0,
69
- })),
70
- })
71
-
72
- const initialData: InfiniteData<ApiCollection<MessageResource>, MessagesPageParam> | undefined =
73
- anchored
74
- ? {
75
- pages: seedQueries.map(q => q.data),
76
- pageParams: seedPageParams,
77
- }
78
- : undefined
79
-
80
- const initialPageParam: MessagesPageParam = anchored ? seedPageParams[0] : {}
81
-
82
- const {
83
- data,
84
- refetch,
85
- isRefetching,
86
- fetchNextPage,
87
- hasNextPage,
88
- fetchPreviousPage,
89
- hasPreviousPage,
90
- isFetchingPreviousPage,
91
- } = useSuspenseInfiniteQuery<
92
- ApiCollection<MessageResource>,
93
- Response,
94
- InfiniteData<ApiCollection<MessageResource>, MessagesPageParam>,
95
- typeof queryKey,
96
- MessagesPageParam
97
- >({
98
- queryKey,
99
- queryFn: ({ pageParam }) => fetchPage(pageParam),
100
- initialPageParam,
101
- initialData,
102
- getNextPageParam: olderPageParam,
103
- getPreviousPageParam: anchored ? newerPageParam : () => undefined,
104
- ...(opts || {}),
105
- ...(anchored ? { staleTime: Infinity, refetchOnMount: false } : {}),
106
- })
107
-
108
- const messages = useMemo(() => sortAndFilterMessages(data.pages), [data.pages])
109
-
110
- const queryClient = useQueryClient()
111
- const cancelFetchNewerMessages = useCallback(
112
- () => queryClient.cancelQueries({ queryKey }),
113
- [queryClient, queryKey]
14
+ const queryKey = getMessagesQueryKey({ conversation_id, reply_root_id })
15
+ const messages = useMemo(
16
+ () =>
17
+ data
18
+ .filter(
19
+ message =>
20
+ (!message.deletedAt || message.replyRootId) &&
21
+ (message.attachments?.length || message.text?.length)
22
+ )
23
+ .sort((a, b) => -a.id.localeCompare(b.id)),
24
+ [data]
114
25
  )
115
26
 
116
- return {
117
- messages,
118
- refetch,
119
- isRefetching,
120
- fetchOlderMessages: fetchNextPage,
121
- hasMoreOlderMessages: hasNextPage,
122
- fetchNewerMessages: fetchPreviousPage,
123
- hasMoreNewerMessages: hasPreviousPage,
124
- isFetchingNewerMessages: isFetchingPreviousPage,
125
- cancelFetchNewerMessages,
126
- queryKey,
127
- }
27
+ return { messages, refetch, isRefetching, fetchNextPage, queryKey }
128
28
  }
@@ -12,7 +12,6 @@ import {
12
12
  updateCacheWithIndividualMessage,
13
13
  updateCacheWithReaction,
14
14
  getThreadedMessagesQueryKey,
15
- hasUnloadedNewerPages,
16
15
  } from '../utils/cache/messages_cache'
17
16
  import { transformMessageEventDataToMessageResource } from '../utils/jolt/transform_message_event_data_to_message_resource'
18
17
  import { completeMessageCreationTracking } from '../utils/performance_tracking'
@@ -53,10 +52,10 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
53
52
  }
54
53
  }
55
54
 
56
- if (e.event === 'message.updated' || !hasUnloadedNewerPages(queryClient, messagesQueryKey)) {
57
- updateCacheWithMessage(queryClient, messagesQueryKey, message, e.event)
58
- }
55
+ // Update the main conversation cache
56
+ updateCacheWithMessage(queryClient, messagesQueryKey, message, e.event)
59
57
 
58
+ // If message has a reply_root_id, also update the threaded cache
60
59
  if (data.reply_root_id) {
61
60
  const threadedMessagesQueryKey = getThreadedMessagesQueryKey(
62
61
  conversationId,
@@ -98,21 +98,6 @@ export const useConversationsMute = ({ conversation }: { conversation: Conversat
98
98
  }
99
99
  }
100
100
 
101
- export const useConversationsMarkReadUpTo = ({ conversationId }: { conversationId: number }) => {
102
- const apiClient = useApiClient()
103
-
104
- return useMutation({
105
- mutationKey: ['markReadUpTo', conversationId],
106
- mutationFn: async ({ sortKey }: { sortKey: string }) =>
107
- apiClient.chat.post({
108
- url: `/me/conversations/${conversationId}/mark_read_up_to`,
109
- data: {
110
- data: { type: 'Conversation', attributes: { sort_key: sortKey } },
111
- },
112
- }),
113
- })
114
- }
115
-
116
101
  export const useMarkAllRead = () => {
117
102
  const apiClient = useApiClient()
118
103
  const { args } = useConversationsContext()
@@ -40,7 +40,6 @@ export const availableFeatures = {
40
40
  message_reporting: 'ROLLOUT_MOBILE_message_reporting',
41
41
  granular_notifications_ui: 'ROLLOUT_granular_notification_preferences_ui',
42
42
  custom_conversation_avatars: 'ROLLOUT_custom_conversation_avatars',
43
- jump_to_unread: 'ROLLOUT_jump_to_unread',
44
43
  conversation_safety_lock: 'ROLLOUT_conversation_safety_lock',
45
44
  video_moderation: 'ROLLOUT_MOBILE_video_moderation',
46
45
  } as const satisfies Record<string, `ROLLOUT_${string}`>
@@ -1,19 +1,15 @@
1
1
  import { debounce } from 'lodash'
2
2
  import { useEffect, useMemo, useRef } from 'react'
3
- import { useConversationContext } from '../contexts/conversation_context'
4
3
  import { ConversationResource, MessageResource } from '../types'
5
4
  import { useAppState } from './use_app_state'
6
5
  import { useConversationsMarkRead } from './use_conversations_actions'
7
- import { useJumpToUnreadGates } from './use_jump_to_unread_gates'
8
6
 
9
7
  interface Props {
10
8
  conversation: ConversationResource
11
- messages?: MessageResource[]
9
+ messages: MessageResource[]
12
10
  }
13
11
 
14
12
  export function useMarkLatestMessageRead({ conversation }: Props) {
15
- const { jumpToUnreadActive } = useJumpToUnreadGates()
16
- const { currentPageReplyRootId, atEndOfMessageHistory } = useConversationContext()
17
13
  const firedOnce = useRef<boolean>(false)
18
14
  const { markRead } = useConversationsMarkRead({ conversation })
19
15
  const debouncedMarkRead = useMemo(
@@ -29,20 +25,10 @@ export function useMarkLatestMessageRead({ conversation }: Props) {
29
25
 
30
26
  useEffect(() => {
31
27
  if (!isActive || !shouldMarkRead) return
32
- if (currentPageReplyRootId) return
33
- if (jumpToUnreadActive && !atEndOfMessageHistory) return
34
28
 
35
29
  firedOnce.current = true
36
30
 
37
31
  debouncedMarkRead(true)
38
32
  // keeping unreadReactionCount in the dependency array to watch for changes
39
- }, [
40
- debouncedMarkRead,
41
- isActive,
42
- shouldMarkRead,
43
- unreadReactionCount,
44
- currentPageReplyRootId,
45
- jumpToUnreadActive,
46
- atEndOfMessageHistory,
47
- ])
33
+ }, [debouncedMarkRead, isActive, shouldMarkRead, unreadReactionCount])
48
34
  }
@@ -0,0 +1,12 @@
1
+ import { useWindowDimensions } from 'react-native'
2
+ import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../utils'
3
+ import { useFontScale } from './use_font_scale'
4
+
5
+ const VIEWPORT_FRACTION = 0.22
6
+ const BASE_DIAMETER = 80
7
+
8
+ export function usePreviewAvatarDiameter() {
9
+ const { width } = useWindowDimensions()
10
+ const fontScale = useFontScale({ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER_LANDMARK })
11
+ return Math.min(BASE_DIAMETER * fontScale, width * VIEWPORT_FRACTION)
12
+ }
@@ -90,7 +90,7 @@ export const useSuspensePaginator = <T extends ResourceObject>(
90
90
  return { ...query, data, totalCount }
91
91
  }
92
92
 
93
- export const throwResponseError = (error: unknown) => {
93
+ const throwResponseError = (error: unknown) => {
94
94
  if (error instanceof Response) {
95
95
  throw new ResponseError(error as FailedResponse)
96
96
  }
package/src/jest.ts CHANGED
@@ -16,6 +16,6 @@
16
16
  */
17
17
  export const jestTransformPackages = [
18
18
  '@planningcenter/chat-react-native',
19
+ '@planningcenter/emoji-keyboard',
19
20
  '@fortawesome',
20
- 'rn-emoji-keyboard',
21
21
  ]
@@ -13,6 +13,8 @@ import {
13
13
  type AvatarUpdatePayload,
14
14
  } from '../../hooks/use_conversation_avatar_update'
15
15
  import { useFontScale } from '../../hooks/use_font_scale'
16
+ import { usePreviewAvatarDiameter } from '../../hooks/use_preview_avatar_diameter'
17
+ import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../../utils'
16
18
  import type { ImagePickerAsset } from '../../utils/native_adapters/image_picker'
17
19
  import {
18
20
  avatarPickerReducer,
@@ -71,6 +73,7 @@ function EditModeContent({ conversationId }: { conversationId: number }) {
71
73
  const navigation = useNavigation()
72
74
  const { data: conversation } = useConversation({ conversation_id: conversationId })
73
75
  const mutation = useConversationAvatarUpdate({ conversationId })
76
+ const previewDiameter = usePreviewAvatarDiameter()
74
77
 
75
78
  const [state, dispatch] = useReducer(avatarPickerReducer, conversation, initAvatarPickerState)
76
79
 
@@ -110,7 +113,14 @@ function EditModeContent({ conversationId }: { conversationId: number }) {
110
113
  </FormSheet.HeaderTextButton>
111
114
  </>
112
115
  }
113
- fallbackPreview={<ConversationAvatar conversation={conversation} size="2xl" />}
116
+ fallbackPreview={
117
+ <ConversationAvatar
118
+ conversation={conversation}
119
+ size="2xl"
120
+ maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
121
+ style={{ width: previewDiameter, height: previewDiameter }}
122
+ />
123
+ }
114
124
  />
115
125
  )
116
126
  }
@@ -187,7 +197,10 @@ function AvatarPickerFormSheet({
187
197
  activeTab={state.activeTab}
188
198
  onTabPress={tab => dispatch({ type: 'SELECT_TAB', payload: tab })}
189
199
  renderItem={({ item }) => (
190
- <Text style={[styles.tabLabel, item === state.activeTab && styles.tabLabelActive]}>
200
+ <Text
201
+ style={[styles.tabLabel, item === state.activeTab && styles.tabLabelActive]}
202
+ maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
203
+ >
191
204
  {TAB_LABELS[item]}
192
205
  </Text>
193
206
  )}
@@ -226,7 +239,15 @@ function AvatarPickerFormSheet({
226
239
 
227
240
  function EmptyAvatarPlaceholder() {
228
241
  const styles = useStyles()
229
- return <View style={styles.emptyAvatarPlaceholder} />
242
+ const diameter = usePreviewAvatarDiameter()
243
+ return (
244
+ <View
245
+ style={[
246
+ styles.emptyAvatarPlaceholder,
247
+ { width: diameter, height: diameter, borderRadius: diameter / 2 },
248
+ ]}
249
+ />
250
+ )
230
251
  }
231
252
 
232
253
  type ValidAvatarPickerState = AvatarPickerState &
@@ -264,9 +285,7 @@ function buildPayload(state: AvatarPickerState): AvatarUpdatePayload | null {
264
285
 
265
286
  const useStyles = () => {
266
287
  const { colors } = useTheme()
267
- const fontScale = useFontScale({ maxFontSizeMultiplier: 1.3 })
268
- const uncappedFontScale = useFontScale()
269
- const emptyAvatarDiameter = 80 * uncappedFontScale
288
+ const fontScale = useFontScale({ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER_LANDMARK })
270
289
 
271
290
  return StyleSheet.create({
272
291
  formSheetRoot: {
@@ -301,9 +320,6 @@ const useStyles = () => {
301
320
  borderTopColor: colors.borderColorDefaultBase,
302
321
  },
303
322
  emptyAvatarPlaceholder: {
304
- width: emptyAvatarDiameter,
305
- height: emptyAvatarDiameter,
306
- borderRadius: emptyAvatarDiameter / 2,
307
323
  borderWidth: 2,
308
324
  borderStyle: 'dashed',
309
325
  borderColor: colors.borderColorDefaultBase,
@@ -3,6 +3,8 @@ import { StyleSheet, View } from 'react-native'
3
3
  import { EmojiAvatar } from '../../components/display/emoji_avatar'
4
4
  import { IconAvatar } from '../../components/display/icon_avatar'
5
5
  import AvatarPrimitive from '../../components/primitive/avatar_primitive'
6
+ import { usePreviewAvatarDiameter } from '../../hooks/use_preview_avatar_diameter'
7
+ import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../../utils'
6
8
  import type { AvatarPickerState } from './avatar_picker_state'
7
9
 
8
10
  interface AvatarPreviewProps {
@@ -10,18 +12,24 @@ interface AvatarPreviewProps {
10
12
  fallback: React.ReactNode
11
13
  }
12
14
 
13
- function getPreviewNode(state: AvatarPickerState): React.ReactNode {
15
+ function getPreviewNode(state: AvatarPickerState, diameter: number): React.ReactNode {
16
+ const sizeProps = {
17
+ size: '2xl',
18
+ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER_LANDMARK,
19
+ style: { width: diameter, height: diameter },
20
+ } as const
21
+
14
22
  switch (state.selectedType) {
15
23
  case 'icon':
16
24
  if (!state.selectedKey) return null
17
- return <IconAvatar iconKey={state.selectedKey} color={state.selectedColor} size="2xl" />
25
+ return <IconAvatar iconKey={state.selectedKey} color={state.selectedColor} {...sizeProps} />
18
26
  case 'emoji':
19
27
  if (!state.selectedKey) return null
20
- return <EmojiAvatar emoji={state.selectedKey} color={state.selectedColor} size="2xl" />
28
+ return <EmojiAvatar emoji={state.selectedKey} color={state.selectedColor} {...sizeProps} />
21
29
  case 'image':
22
30
  if (!state.imagePreviewUri) return null
23
31
  return (
24
- <AvatarPrimitive.Root size="2xl">
32
+ <AvatarPrimitive.Root {...sizeProps}>
25
33
  <AvatarPrimitive.Mask>
26
34
  <AvatarPrimitive.Image sourceUri={state.imagePreviewUri} />
27
35
  </AvatarPrimitive.Mask>
@@ -33,7 +41,8 @@ function getPreviewNode(state: AvatarPickerState): React.ReactNode {
33
41
  }
34
42
 
35
43
  export function AvatarPreview({ state, fallback }: AvatarPreviewProps) {
36
- const preview = getPreviewNode(state)
44
+ const diameter = usePreviewAvatarDiameter()
45
+ const preview = getPreviewNode(state, diameter)
37
46
  return <View style={styles.container}>{preview ?? fallback}</View>
38
47
  }
39
48
 
@@ -1,15 +1,11 @@
1
+ import { EmojiKeyboard, emojisByCategory, type EmojiType } from '@planningcenter/emoji-keyboard'
1
2
  import React, { useCallback } from 'react'
2
3
  import { StyleSheet, View } from 'react-native'
3
- import { EmojiKeyboard, type EmojiType, type EmojisByCategory } from 'rn-emoji-keyboard'
4
- // rn-emoji-keyboard exposes no public exclusion API, so we reach into its
5
- // internal src/ tree for the emoji JSON. Version is pinned in package.json
6
- // — verify this path still resolves before bumping rn-emoji-keyboard.
7
- import emojiData from 'rn-emoji-keyboard/src/assets/emojis.json'
8
4
  import { useTheme } from '../../hooks'
9
5
 
10
6
  const BLOCKED_EMOJIS = new Set(['🖕'])
11
7
 
12
- const filteredEmojis = (emojiData as EmojisByCategory[]).map(category => ({
8
+ const filteredEmojis = emojisByCategory.map(category => ({
13
9
  ...category,
14
10
  data: category.data.filter(e => !BLOCKED_EMOJIS.has(e.emoji)),
15
11
  }))
@@ -57,6 +53,7 @@ export function EmojiTab({ onEmojiSelect }: EmojiTabProps) {
57
53
  categoryPosition="top"
58
54
  emojisByCategory={filteredEmojis}
59
55
  onEmojiSelected={handleEmojiSelected}
56
+ hideHeader
60
57
  enableSearchBar
61
58
  enableRecentlyUsed
62
59
  theme={emojiTheme}
@@ -2,6 +2,7 @@ import React, { useCallback } from 'react'
2
2
  import { Alert, StyleSheet, View } from 'react-native'
3
3
  import { Button } from '../../components'
4
4
  import { useTheme } from '../../hooks'
5
+ import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../../utils'
5
6
  import { ImagePicker } from '../../utils/native_adapters'
6
7
  import type { ImagePickerAsset } from '../../utils/native_adapters/image_picker'
7
8
 
@@ -43,6 +44,7 @@ export function UploadTab({ imagePreviewUri, onImageSelect }: UploadTabProps) {
43
44
  variant="outline"
44
45
  appearance="interaction"
45
46
  size="md"
47
+ maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
46
48
  />
47
49
  </View>
48
50
  )
@@ -49,8 +49,10 @@ import {
49
49
  useConversationUpdate,
50
50
  } from '../hooks/use_conversation'
51
51
  import { availableFeatures, useFeatures } from '../hooks/use_features'
52
+ import { usePreviewAvatarDiameter } from '../hooks/use_preview_avatar_diameter'
52
53
  import { type ConversationResource, MemberResource, isDefined } from '../types'
53
54
  import { GroupResource } from '../types/resources/group_resource'
55
+ import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../utils'
54
56
  import { genderDisplayLabel } from '../utils/gender_display_label'
55
57
  import { tokens } from '../vendor/tapestry/tokens'
56
58
  // =========================================
@@ -634,10 +636,16 @@ function AvatarCard({
634
636
  onPress: () => void
635
637
  }) {
636
638
  const styles = useStyles()
639
+ const diameter = usePreviewAvatarDiameter()
637
640
 
638
641
  return (
639
642
  <View style={styles.avatarCard}>
640
- <ConversationAvatar conversation={conversation} size="2xl" />
643
+ <ConversationAvatar
644
+ conversation={conversation}
645
+ size="2xl"
646
+ maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
647
+ style={{ width: diameter, height: diameter }}
648
+ />
641
649
  <Button
642
650
  title="Update avatar"
643
651
  iconNameLeft="general.pencil"
@@ -645,6 +653,7 @@ function AvatarCard({
645
653
  variant="outline"
646
654
  appearance="interaction"
647
655
  size="sm"
656
+ maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
648
657
  />
649
658
  </View>
650
659
  )
@@ -24,6 +24,7 @@ export const HeaderRow = ({ data, nativeID, style, setTeamFilters }: HeaderRowPr
24
24
  const route = useRoute<RouteProp<ConversationFilterRecipientsScreenProps['route']>>()
25
25
 
26
26
  const { serviceTypeName, teamIdsForServiceType } = data
27
+ const displayName = serviceTypeName ?? 'No service type'
27
28
  const { team_ids: currentTeamIds = [] } = route.params
28
29
 
29
30
  const newTeamIdsAdded = [...new Set([...currentTeamIds, ...teamIdsForServiceType])]
@@ -34,7 +35,7 @@ export const HeaderRow = ({ data, nativeID, style, setTeamFilters }: HeaderRowPr
34
35
  const selectLabel = allTeamsSelected ? 'Deselect' : 'Select'
35
36
 
36
37
  const headingAccessibilityHint = `${pluralize(teamIdsForServiceType.length, 'team')} available to select`
37
- const selectAllAccessibilityLabel = `${selectLabel} ${pluralize(teamIdsForServiceType.length, 'team')} for ${serviceTypeName}`
38
+ const selectAllAccessibilityLabel = `${selectLabel} ${pluralize(teamIdsForServiceType.length, 'team')} for ${displayName}`
38
39
 
39
40
  const handleSelectAll = () => {
40
41
  setTeamFilters({
@@ -53,7 +54,7 @@ export const HeaderRow = ({ data, nativeID, style, setTeamFilters }: HeaderRowPr
53
54
  nativeID={nativeID}
54
55
  accessibilityHint={headingAccessibilityHint}
55
56
  >
56
- {serviceTypeName}
57
+ {displayName}
57
58
  </Heading>
58
59
  </View>
59
60
 
@@ -0,0 +1,108 @@
1
+ import type { TeamResponseItem } from '../../../../types'
2
+ import { decorateTeamResponseItems } from '../use_service_types_with_teams'
3
+
4
+ const makeTeam = (
5
+ overrides: Partial<TeamResponseItem> & { teamId: number; teamName: string }
6
+ ): TeamResponseItem => ({
7
+ name: overrides.teamName,
8
+ value: {
9
+ teamId: overrides.teamId,
10
+ serviceTypeId: overrides.value?.serviceTypeId ?? 0,
11
+ serviceTypeIds: overrides.value?.serviceTypeIds ?? [],
12
+ },
13
+ serviceTypeName: overrides.serviceTypeName ?? '',
14
+ serviceTypeNames: overrides.serviceTypeNames ?? [],
15
+ serviceTypeAcronyms: overrides.serviceTypeAcronyms ?? [],
16
+ teamName: overrides.teamName,
17
+ order: overrides.order ?? [0, '', overrides.teamName],
18
+ })
19
+
20
+ describe('decorateTeamResponseItems', () => {
21
+ it('groups teams under their service types', () => {
22
+ const items = [
23
+ makeTeam({
24
+ teamId: 1,
25
+ teamName: 'Worship',
26
+ value: { teamId: 1, serviceTypeId: 10, serviceTypeIds: [10] },
27
+ serviceTypeNames: ['Sunday Morning'],
28
+ }),
29
+ ]
30
+
31
+ const result = decorateTeamResponseItems(items)
32
+
33
+ expect(result).toHaveLength(1)
34
+ expect(result[0]).toMatchObject({ id: 10, name: 'Sunday Morning', teams: [{ id: 1 }] })
35
+ })
36
+
37
+ it('gives each team without a service type its own bucket', () => {
38
+ const items = [
39
+ makeTeam({
40
+ teamId: 58,
41
+ teamName: 'Services Team 58',
42
+ value: { teamId: 58, serviceTypeId: 0, serviceTypeIds: [] },
43
+ serviceTypeNames: [],
44
+ }),
45
+ makeTeam({
46
+ teamId: 99,
47
+ teamName: 'Another Typeless Team',
48
+ value: { teamId: 99, serviceTypeId: 0, serviceTypeIds: [] },
49
+ serviceTypeNames: [],
50
+ }),
51
+ ]
52
+
53
+ const result = decorateTeamResponseItems(items)
54
+
55
+ expect(result).toHaveLength(2)
56
+ expect(result[0]).toMatchObject({ id: -58, name: 'Services Team 58', teams: [{ id: 58 }] })
57
+ expect(result[1]).toMatchObject({
58
+ id: -99,
59
+ name: 'Another Typeless Team',
60
+ teams: [{ id: 99 }],
61
+ })
62
+ })
63
+
64
+ it('places teams with and without service types in the right buckets', () => {
65
+ const items = [
66
+ makeTeam({
67
+ teamId: 1,
68
+ teamName: 'Worship',
69
+ value: { teamId: 1, serviceTypeId: 10, serviceTypeIds: [10] },
70
+ serviceTypeNames: ['Sunday Morning'],
71
+ }),
72
+ makeTeam({
73
+ teamId: 58,
74
+ teamName: 'Services Team 58',
75
+ value: { teamId: 58, serviceTypeId: 0, serviceTypeIds: [] },
76
+ serviceTypeNames: [],
77
+ }),
78
+ ]
79
+
80
+ const result = decorateTeamResponseItems(items)
81
+
82
+ expect(result).toHaveLength(2)
83
+ expect(result[0]).toMatchObject({ id: -58, name: 'Services Team 58', teams: [{ id: 58 }] })
84
+ expect(result[1]).toMatchObject({ id: 10, name: 'Sunday Morning' })
85
+ })
86
+
87
+ it('filters by search query, matching teams without service types by name', () => {
88
+ const items = [
89
+ makeTeam({
90
+ teamId: 58,
91
+ teamName: 'Services Team 58',
92
+ value: { teamId: 58, serviceTypeId: 0, serviceTypeIds: [] },
93
+ serviceTypeNames: [],
94
+ }),
95
+ makeTeam({
96
+ teamId: 99,
97
+ teamName: 'Unrelated Team',
98
+ value: { teamId: 99, serviceTypeId: 0, serviceTypeIds: [] },
99
+ serviceTypeNames: [],
100
+ }),
101
+ ]
102
+
103
+ const result = decorateTeamResponseItems(items, 'Services Team')
104
+
105
+ expect(result).toHaveLength(1)
106
+ expect(result[0]).toMatchObject({ id: -58, name: 'Services Team 58', teams: [{ id: 58 }] })
107
+ })
108
+ })