@planningcenter/chat-react-native 3.38.0-rc.8 → 3.38.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 (154) 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/reply_shadow_message.d.ts +2 -1
  6. package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
  7. package/build/components/conversation/reply_shadow_message.js.map +1 -1
  8. package/build/contexts/conversation_context.d.ts +1 -8
  9. package/build/contexts/conversation_context.d.ts.map +1 -1
  10. package/build/contexts/conversation_context.js +3 -21
  11. package/build/contexts/conversation_context.js.map +1 -1
  12. package/build/hooks/groups/use_group_chat_conversation_payload.d.ts.map +1 -1
  13. package/build/hooks/groups/use_group_chat_conversation_payload.js +1 -0
  14. package/build/hooks/groups/use_group_chat_conversation_payload.js.map +1 -1
  15. package/build/hooks/use_conversation_messages.d.ts +6 -15
  16. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  17. package/build/hooks/use_conversation_messages.js +9 -62
  18. package/build/hooks/use_conversation_messages.js.map +1 -1
  19. package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
  20. package/build/hooks/use_conversation_messages_jolt_events.js +4 -4
  21. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  22. package/build/hooks/use_conversations_actions.d.ts +0 -5
  23. package/build/hooks/use_conversations_actions.d.ts.map +1 -1
  24. package/build/hooks/use_conversations_actions.js +0 -12
  25. package/build/hooks/use_conversations_actions.js.map +1 -1
  26. package/build/hooks/use_features.d.ts +0 -1
  27. package/build/hooks/use_features.d.ts.map +1 -1
  28. package/build/hooks/use_features.js +0 -1
  29. package/build/hooks/use_features.js.map +1 -1
  30. package/build/hooks/use_mark_latest_message_read.d.ts +1 -1
  31. package/build/hooks/use_mark_latest_message_read.d.ts.map +1 -1
  32. package/build/hooks/use_mark_latest_message_read.js +1 -17
  33. package/build/hooks/use_mark_latest_message_read.js.map +1 -1
  34. package/build/hooks/use_suspense_api.d.ts +0 -1
  35. package/build/hooks/use_suspense_api.d.ts.map +1 -1
  36. package/build/hooks/use_suspense_api.js +1 -1
  37. package/build/hooks/use_suspense_api.js.map +1 -1
  38. package/build/screens/conversation_filter_recipients/components/header_row.d.ts.map +1 -1
  39. package/build/screens/conversation_filter_recipients/components/header_row.js +3 -2
  40. package/build/screens/conversation_filter_recipients/components/header_row.js.map +1 -1
  41. package/build/screens/conversation_filter_recipients/hooks/use_flattened_array_of_service_types_with_teams.d.ts.map +1 -1
  42. package/build/screens/conversation_filter_recipients/hooks/use_flattened_array_of_service_types_with_teams.js +47 -18
  43. package/build/screens/conversation_filter_recipients/hooks/use_flattened_array_of_service_types_with_teams.js.map +1 -1
  44. package/build/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.d.ts +2 -1
  45. package/build/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.d.ts.map +1 -1
  46. package/build/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.js +23 -26
  47. package/build/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.js.map +1 -1
  48. package/build/screens/conversation_filter_recipients/types.d.ts +1 -1
  49. package/build/screens/conversation_filter_recipients/types.d.ts.map +1 -1
  50. package/build/screens/conversation_filter_recipients/types.js.map +1 -1
  51. package/build/screens/conversation_screen.d.ts +0 -1
  52. package/build/screens/conversation_screen.d.ts.map +1 -1
  53. package/build/screens/conversation_screen.js +48 -95
  54. package/build/screens/conversation_screen.js.map +1 -1
  55. package/build/screens/conversation_select_recipients/components/recipient_link_row.d.ts +1 -1
  56. package/build/screens/conversation_select_recipients/components/recipient_link_row.d.ts.map +1 -1
  57. package/build/screens/conversation_select_recipients/components/recipient_link_row.js +3 -3
  58. package/build/screens/conversation_select_recipients/components/recipient_link_row.js.map +1 -1
  59. package/build/screens/conversation_select_recipients/components/team_recipient_row.d.ts.map +1 -1
  60. package/build/screens/conversation_select_recipients/components/team_recipient_row.js +1 -1
  61. package/build/screens/conversation_select_recipients/components/team_recipient_row.js.map +1 -1
  62. package/build/utils/cache/messages_cache.d.ts +0 -1
  63. package/build/utils/cache/messages_cache.d.ts.map +1 -1
  64. package/build/utils/cache/messages_cache.js +0 -4
  65. package/build/utils/cache/messages_cache.js.map +1 -1
  66. package/build/utils/group_messages.d.ts +2 -9
  67. package/build/utils/group_messages.d.ts.map +1 -1
  68. package/build/utils/group_messages.js +1 -20
  69. package/build/utils/group_messages.js.map +1 -1
  70. package/package.json +3 -3
  71. package/src/__tests__/hooks/use_group_chat_conversation_payload.test.tsx +50 -0
  72. package/src/components/conversation/jump_to_bottom_button.tsx +8 -57
  73. package/src/components/conversation/reply_shadow_message.tsx +1 -1
  74. package/src/contexts/conversation_context.tsx +2 -30
  75. package/src/hooks/groups/use_group_chat_conversation_payload.ts +1 -0
  76. package/src/hooks/use_conversation_messages.ts +20 -120
  77. package/src/hooks/use_conversation_messages_jolt_events.ts +3 -4
  78. package/src/hooks/use_conversations_actions.ts +0 -15
  79. package/src/hooks/use_features.ts +0 -1
  80. package/src/hooks/use_mark_latest_message_read.ts +2 -16
  81. package/src/hooks/use_suspense_api.ts +1 -1
  82. package/src/screens/conversation_filter_recipients/components/header_row.tsx +3 -2
  83. package/src/screens/conversation_filter_recipients/hooks/__tests__/use_service_types_with_teams.test.ts +108 -0
  84. package/src/screens/conversation_filter_recipients/hooks/use_flattened_array_of_service_types_with_teams.tsx +46 -19
  85. package/src/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.ts +31 -29
  86. package/src/screens/conversation_filter_recipients/types.tsx +1 -1
  87. package/src/screens/conversation_screen.tsx +76 -184
  88. package/src/screens/conversation_select_recipients/components/recipient_link_row.tsx +6 -4
  89. package/src/screens/conversation_select_recipients/components/team_recipient_row.tsx +2 -1
  90. package/src/utils/__tests__/group_messages.test.ts +0 -71
  91. package/src/utils/cache/messages_cache.ts +0 -5
  92. package/src/utils/group_messages.ts +2 -42
  93. package/build/components/conversation/unread_divider.d.ts +0 -6
  94. package/build/components/conversation/unread_divider.d.ts.map +0 -1
  95. package/build/components/conversation/unread_divider.js +0 -59
  96. package/build/components/conversation/unread_divider.js.map +0 -1
  97. package/build/hooks/use_flat_list_viewability.d.ts +0 -20
  98. package/build/hooks/use_flat_list_viewability.d.ts.map +0 -1
  99. package/build/hooks/use_flat_list_viewability.js +0 -30
  100. package/build/hooks/use_flat_list_viewability.js.map +0 -1
  101. package/build/hooks/use_jump_to_bottom_action.d.ts +0 -9
  102. package/build/hooks/use_jump_to_bottom_action.d.ts.map +0 -1
  103. package/build/hooks/use_jump_to_bottom_action.js +0 -62
  104. package/build/hooks/use_jump_to_bottom_action.js.map +0 -1
  105. package/build/hooks/use_jump_to_unread_anchor.d.ts +0 -20
  106. package/build/hooks/use_jump_to_unread_anchor.d.ts.map +0 -1
  107. package/build/hooks/use_jump_to_unread_anchor.js +0 -53
  108. package/build/hooks/use_jump_to_unread_anchor.js.map +0 -1
  109. package/build/hooks/use_jump_to_unread_gates.d.ts +0 -5
  110. package/build/hooks/use_jump_to_unread_gates.d.ts.map +0 -1
  111. package/build/hooks/use_jump_to_unread_gates.js +0 -10
  112. package/build/hooks/use_jump_to_unread_gates.js.map +0 -1
  113. package/build/hooks/use_scroll_tracking.d.ts +0 -13
  114. package/build/hooks/use_scroll_tracking.d.ts.map +0 -1
  115. package/build/hooks/use_scroll_tracking.js +0 -45
  116. package/build/hooks/use_scroll_tracking.js.map +0 -1
  117. package/build/hooks/use_track_highest_seen_message.d.ts +0 -4
  118. package/build/hooks/use_track_highest_seen_message.d.ts.map +0 -1
  119. package/build/hooks/use_track_highest_seen_message.js +0 -35
  120. package/build/hooks/use_track_highest_seen_message.js.map +0 -1
  121. package/build/utils/conversation_messages.d.ts +0 -10
  122. package/build/utils/conversation_messages.d.ts.map +0 -1
  123. package/build/utils/conversation_messages.js +0 -22
  124. package/build/utils/conversation_messages.js.map +0 -1
  125. package/build/utils/highest_seen_tracker.d.ts +0 -12
  126. package/build/utils/highest_seen_tracker.d.ts.map +0 -1
  127. package/build/utils/highest_seen_tracker.js +0 -37
  128. package/build/utils/highest_seen_tracker.js.map +0 -1
  129. package/build/utils/message_viewability.d.ts +0 -24
  130. package/build/utils/message_viewability.d.ts.map +0 -1
  131. package/build/utils/message_viewability.js +0 -29
  132. package/build/utils/message_viewability.js.map +0 -1
  133. package/build/utils/unread_divider_helpers.d.ts +0 -18
  134. package/build/utils/unread_divider_helpers.d.ts.map +0 -1
  135. package/build/utils/unread_divider_helpers.js +0 -13
  136. package/build/utils/unread_divider_helpers.js.map +0 -1
  137. package/src/__tests__/hooks/use_conversation_messages.test.tsx +0 -109
  138. package/src/__tests__/hooks/use_mark_latest_message_read.test.tsx +0 -154
  139. package/src/__tests__/utils/cache/messages_cache.test.ts +0 -54
  140. package/src/components/conversation/unread_divider.tsx +0 -90
  141. package/src/hooks/use_flat_list_viewability.ts +0 -50
  142. package/src/hooks/use_jump_to_bottom_action.ts +0 -75
  143. package/src/hooks/use_jump_to_unread_anchor.ts +0 -68
  144. package/src/hooks/use_jump_to_unread_gates.ts +0 -10
  145. package/src/hooks/use_scroll_tracking.ts +0 -64
  146. package/src/hooks/use_track_highest_seen_message.ts +0 -43
  147. package/src/utils/__tests__/conversation_messages.test.ts +0 -105
  148. package/src/utils/__tests__/highest_seen_tracker.test.ts +0 -82
  149. package/src/utils/__tests__/message_viewability.test.ts +0 -168
  150. package/src/utils/__tests__/unread_divider_helpers.test.ts +0 -85
  151. package/src/utils/conversation_messages.ts +0 -37
  152. package/src/utils/highest_seen_tracker.ts +0 -42
  153. package/src/utils/message_viewability.ts +0 -49
  154. package/src/utils/unread_divider_helpers.ts +0 -25
@@ -1,8 +1,7 @@
1
1
  import dayjs from './dayjs';
2
2
  import { isSystemMessage } from './system_messages';
3
3
  const FIVE_MINUTES_MS = 5 * 60 * 1000;
4
- export const UNREAD_DIVIDER_KEY = 'unread-divider';
5
- export function groupMessages({ ms, inReplyScreen, jumpToUnreadActive, initialMessageId, }) {
4
+ export function groupMessages({ ms, inReplyScreen }) {
6
5
  const items = [];
7
6
  let myLatestSeen = false;
8
7
  let nextNeighborEnriched;
@@ -12,9 +11,6 @@ export function groupMessages({ ms, inReplyScreen, jumpToUnreadActive, initialMe
12
11
  if (isSystemMessage(message)) {
13
12
  const enriched = enrichSystemMessage(message, next);
14
13
  items.push(enriched);
15
- if (crossesUnreadBoundary(message, prev, jumpToUnreadActive, initialMessageId)) {
16
- items.push(unreadDivider());
17
- }
18
14
  if (datesDifferBetween(message, prev))
19
15
  items.push(dateSeparator(message));
20
16
  nextNeighborEnriched = enriched;
@@ -25,9 +21,6 @@ export function groupMessages({ ms, inReplyScreen, jumpToUnreadActive, initialMe
25
21
  myLatestSeen = true;
26
22
  const enriched = enrichRegularMessage(message, prev, next, isMyLatest, !!inReplyScreen);
27
23
  items.push(enriched);
28
- if (crossesUnreadBoundary(message, prev, jumpToUnreadActive, initialMessageId)) {
29
- items.push(unreadDivider());
30
- }
31
24
  const shadow = replyShadowFor(enriched, prev);
32
25
  if (shadow)
33
26
  items.push(shadow);
@@ -38,18 +31,6 @@ export function groupMessages({ ms, inReplyScreen, jumpToUnreadActive, initialMe
38
31
  });
39
32
  return items;
40
33
  }
41
- function crossesUnreadBoundary(message, prev, jumpToUnreadActive, initialMessageId) {
42
- if (!jumpToUnreadActive)
43
- return false;
44
- if (!initialMessageId)
45
- return false;
46
- if (!prev)
47
- return false;
48
- return (prev.id.localeCompare(initialMessageId) <= 0 && message.id.localeCompare(initialMessageId) > 0);
49
- }
50
- function unreadDivider() {
51
- return { type: 'UnreadDivider', id: UNREAD_DIVIDER_KEY };
52
- }
53
34
  function neighborsOf(arr, i) {
54
35
  return { prev: arr[i + 1], next: arr[i - 1] };
55
36
  }
@@ -1 +1 @@
1
- {"version":3,"file":"group_messages.js","sourceRoot":"","sources":["../../src/utils/group_messages.ts"],"names":[],"mappings":"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","sourcesContent":["import type { MessageResource } from '../types/resources/message'\nimport dayjs from './dayjs'\nimport { isSystemMessage } from './system_messages'\n\nconst FIVE_MINUTES_MS = 5 * 60 * 1000\n\nexport const UNREAD_DIVIDER_KEY = 'unread-divider'\n\nexport type DateSeparator = { type: 'DateSeparator'; id: string; date: string }\n\nexport type UnreadDividerItem = { type: 'UnreadDivider'; id: typeof UNREAD_DIVIDER_KEY }\n\nexport type ReplyShadowMessage = {\n type: 'ReplyShadowMessage'\n id: string\n messageId: string\n isReplyShadowMessage: boolean\n nextRendersAuthor: boolean\n}\n\nexport type EnrichedMessage =\n | MessageResource\n | DateSeparator\n | UnreadDividerItem\n | ReplyShadowMessage\n\ninterface GroupMessagesProps {\n ms: MessageResource[]\n inReplyScreen?: boolean\n jumpToUnreadActive?: boolean\n initialMessageId?: string | null\n}\n\nexport function groupMessages({\n ms,\n inReplyScreen,\n jumpToUnreadActive,\n initialMessageId,\n}: GroupMessagesProps): EnrichedMessage[] {\n const items: EnrichedMessage[] = []\n let myLatestSeen = false\n let nextNeighborEnriched: MessageResource | undefined\n\n ms.forEach((message, i) => {\n const { prev } = neighborsOf(ms, i)\n const next = nextNeighborEnriched\n\n if (isSystemMessage(message)) {\n const enriched = enrichSystemMessage(message, next)\n items.push(enriched)\n if (crossesUnreadBoundary(message, prev, jumpToUnreadActive, initialMessageId)) {\n items.push(unreadDivider())\n }\n if (datesDifferBetween(message, prev)) items.push(dateSeparator(message))\n nextNeighborEnriched = enriched\n return\n }\n\n const isMyLatest = !myLatestSeen && message.mine\n if (isMyLatest) myLatestSeen = true\n\n const enriched = enrichRegularMessage(message, prev, next, isMyLatest, !!inReplyScreen)\n items.push(enriched)\n\n if (crossesUnreadBoundary(message, prev, jumpToUnreadActive, initialMessageId)) {\n items.push(unreadDivider())\n }\n\n const shadow = replyShadowFor(enriched, prev)\n if (shadow) items.push(shadow)\n\n if (!prev || datesDifferBetween(message, prev)) {\n items.push(dateSeparator(message))\n }\n\n nextNeighborEnriched = enriched\n })\n\n return items\n}\n\nfunction crossesUnreadBoundary(\n message: MessageResource,\n prev: MessageResource | undefined,\n jumpToUnreadActive: boolean | undefined,\n initialMessageId: string | null | undefined\n): boolean {\n if (!jumpToUnreadActive) return false\n if (!initialMessageId) return false\n if (!prev) return false\n return (\n prev.id.localeCompare(initialMessageId) <= 0 && message.id.localeCompare(initialMessageId) > 0\n )\n}\n\nfunction unreadDivider(): UnreadDividerItem {\n return { type: 'UnreadDivider', id: UNREAD_DIVIDER_KEY }\n}\n\nfunction neighborsOf<T>(arr: T[], i: number): { prev: T | undefined; next: T | undefined } {\n return { prev: arr[i + 1], next: arr[i - 1] }\n}\n\nfunction enrichSystemMessage(\n message: MessageResource,\n next: MessageResource | undefined\n): MessageResource {\n return {\n ...message,\n myLatestInConversation: false,\n lastInGroup: true,\n renderAuthor: false,\n nextRendersAuthor: next?.renderAuthor,\n isReplyShadowMessage: false,\n nextIsReplyShadowMessage: false,\n threadPosition: null,\n }\n}\n\nfunction enrichRegularMessage(\n message: MessageResource,\n prev: MessageResource | undefined,\n next: MessageResource | undefined,\n isMyLatest: boolean,\n inReplyScreen: boolean\n): MessageResource {\n const inThread = message.replyRootId !== null\n const showThreadDetails = !inReplyScreen && inThread\n\n return {\n ...message,\n myLatestInConversation: isMyLatest,\n lastInGroup: !next || startsNewGroup(next, message),\n renderAuthor: !message.mine && (!prev || startsNewGroup(message, prev)),\n nextRendersAuthor: next?.renderAuthor,\n isReplyShadowMessage: false,\n nextIsReplyShadowMessage: nextIntroducesReplyShadow(next, message),\n threadPosition: showThreadDetails ? threadPositionFor(message, next) : null,\n prevIsMyReply: showThreadDetails ? prev?.mine : undefined,\n nextIsMyReply: showThreadDetails ? next?.mine : undefined,\n }\n}\n\nfunction startsNewGroup(a: MessageResource, b: MessageResource): boolean {\n return (\n a.author?.id !== b.author?.id ||\n differsByMoreThan5Min(a, b) ||\n a.replyRootId !== b.replyRootId ||\n datesDifferBetween(a, b)\n )\n}\n\nfunction differsByMoreThan5Min(a: MessageResource, b: MessageResource): boolean {\n return Math.abs(toMillis(a.createdAt) - toMillis(b.createdAt)) > FIVE_MINUTES_MS\n}\n\nfunction toMillis(iso: string): number {\n return new Date(iso).getTime()\n}\n\nfunction datesDifferBetween(a: MessageResource, b: MessageResource | undefined): boolean {\n if (!b) return false\n return dateKey(a) !== dateKey(b)\n}\n\nfunction dateKey(message: MessageResource): string {\n return dayjs(message.createdAt).format('YYYY-MM-DD')\n}\n\nfunction dateSeparator(message: MessageResource): DateSeparator {\n return { type: 'DateSeparator', id: `day-divider-${message.id}`, date: dateKey(message) }\n}\n\nfunction threadPositionFor(\n message: MessageResource,\n next: MessageResource | undefined\n): 'first' | 'center' | 'last' | null {\n const isThreadRoot = message.replyRootId === message.id\n const isLast =\n !next || next.replyRootId !== message.replyRootId || datesDifferBetween(next, message)\n\n if (isThreadRoot && isLast) return null\n if (isThreadRoot) return 'first'\n if (isLast) return 'last'\n return 'center'\n}\n\nfunction nextIntroducesReplyShadow(\n next: MessageResource | undefined,\n current: MessageResource\n): boolean {\n if (!next) return false\n const nextInThread = next.replyRootId !== null\n const nextIsThreadRoot = next.replyRootId === next.id\n const differentThread = next.replyRootId !== current.replyRootId\n return nextInThread && !nextIsThreadRoot && (differentThread || datesDifferBetween(next, current))\n}\n\nfunction replyShadowFor(\n message: MessageResource,\n prev: MessageResource | undefined\n): ReplyShadowMessage | undefined {\n if (!message.replyRootId) return undefined\n if (message.replyRootId === message.id) return undefined\n\n const enteringNewThread =\n !prev || prev.replyRootId !== message.replyRootId || datesDifferBetween(prev, message)\n if (!enteringNewThread) return undefined\n\n return {\n type: 'ReplyShadowMessage',\n id: `${message.id}-${message.replyRootId}`,\n messageId: message.replyRootId,\n isReplyShadowMessage: true,\n nextRendersAuthor: message.renderAuthor ?? false,\n }\n}\n"]}
1
+ {"version":3,"file":"group_messages.js","sourceRoot":"","sources":["../../src/utils/group_messages.ts"],"names":[],"mappings":"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","sourcesContent":["import type { MessageResource } from '../types/resources/message'\nimport dayjs from './dayjs'\nimport { isSystemMessage } from './system_messages'\n\nconst FIVE_MINUTES_MS = 5 * 60 * 1000\n\nexport type DateSeparator = { type: 'DateSeparator'; id: string; date: string }\n\nexport type ReplyShadowMessage = {\n type: 'ReplyShadowMessage'\n id: string\n messageId: string\n isReplyShadowMessage: boolean\n nextRendersAuthor: boolean\n}\n\nexport type EnrichedMessage = MessageResource | DateSeparator | ReplyShadowMessage\n\ninterface GroupMessagesProps {\n ms: MessageResource[]\n inReplyScreen?: boolean\n}\n\nexport function groupMessages({ ms, inReplyScreen }: GroupMessagesProps): EnrichedMessage[] {\n const items: EnrichedMessage[] = []\n let myLatestSeen = false\n let nextNeighborEnriched: MessageResource | undefined\n\n ms.forEach((message, i) => {\n const { prev } = neighborsOf(ms, i)\n const next = nextNeighborEnriched\n\n if (isSystemMessage(message)) {\n const enriched = enrichSystemMessage(message, next)\n items.push(enriched)\n if (datesDifferBetween(message, prev)) items.push(dateSeparator(message))\n nextNeighborEnriched = enriched\n return\n }\n\n const isMyLatest = !myLatestSeen && message.mine\n if (isMyLatest) myLatestSeen = true\n\n const enriched = enrichRegularMessage(message, prev, next, isMyLatest, !!inReplyScreen)\n items.push(enriched)\n\n const shadow = replyShadowFor(enriched, prev)\n if (shadow) items.push(shadow)\n\n if (!prev || datesDifferBetween(message, prev)) {\n items.push(dateSeparator(message))\n }\n\n nextNeighborEnriched = enriched\n })\n\n return items\n}\n\nfunction neighborsOf<T>(arr: T[], i: number): { prev: T | undefined; next: T | undefined } {\n return { prev: arr[i + 1], next: arr[i - 1] }\n}\n\nfunction enrichSystemMessage(\n message: MessageResource,\n next: MessageResource | undefined\n): MessageResource {\n return {\n ...message,\n myLatestInConversation: false,\n lastInGroup: true,\n renderAuthor: false,\n nextRendersAuthor: next?.renderAuthor,\n isReplyShadowMessage: false,\n nextIsReplyShadowMessage: false,\n threadPosition: null,\n }\n}\n\nfunction enrichRegularMessage(\n message: MessageResource,\n prev: MessageResource | undefined,\n next: MessageResource | undefined,\n isMyLatest: boolean,\n inReplyScreen: boolean\n): MessageResource {\n const inThread = message.replyRootId !== null\n const showThreadDetails = !inReplyScreen && inThread\n\n return {\n ...message,\n myLatestInConversation: isMyLatest,\n lastInGroup: !next || startsNewGroup(next, message),\n renderAuthor: !message.mine && (!prev || startsNewGroup(message, prev)),\n nextRendersAuthor: next?.renderAuthor,\n isReplyShadowMessage: false,\n nextIsReplyShadowMessage: nextIntroducesReplyShadow(next, message),\n threadPosition: showThreadDetails ? threadPositionFor(message, next) : null,\n prevIsMyReply: showThreadDetails ? prev?.mine : undefined,\n nextIsMyReply: showThreadDetails ? next?.mine : undefined,\n }\n}\n\nfunction startsNewGroup(a: MessageResource, b: MessageResource): boolean {\n return (\n a.author?.id !== b.author?.id ||\n differsByMoreThan5Min(a, b) ||\n a.replyRootId !== b.replyRootId ||\n datesDifferBetween(a, b)\n )\n}\n\nfunction differsByMoreThan5Min(a: MessageResource, b: MessageResource): boolean {\n return Math.abs(toMillis(a.createdAt) - toMillis(b.createdAt)) > FIVE_MINUTES_MS\n}\n\nfunction toMillis(iso: string): number {\n return new Date(iso).getTime()\n}\n\nfunction datesDifferBetween(a: MessageResource, b: MessageResource | undefined): boolean {\n if (!b) return false\n return dateKey(a) !== dateKey(b)\n}\n\nfunction dateKey(message: MessageResource): string {\n return dayjs(message.createdAt).format('YYYY-MM-DD')\n}\n\nfunction dateSeparator(message: MessageResource): DateSeparator {\n return { type: 'DateSeparator', id: `day-divider-${message.id}`, date: dateKey(message) }\n}\n\nfunction threadPositionFor(\n message: MessageResource,\n next: MessageResource | undefined\n): 'first' | 'center' | 'last' | null {\n const isThreadRoot = message.replyRootId === message.id\n const isLast =\n !next || next.replyRootId !== message.replyRootId || datesDifferBetween(next, message)\n\n if (isThreadRoot && isLast) return null\n if (isThreadRoot) return 'first'\n if (isLast) return 'last'\n return 'center'\n}\n\nfunction nextIntroducesReplyShadow(\n next: MessageResource | undefined,\n current: MessageResource\n): boolean {\n if (!next) return false\n const nextInThread = next.replyRootId !== null\n const nextIsThreadRoot = next.replyRootId === next.id\n const differentThread = next.replyRootId !== current.replyRootId\n return nextInThread && !nextIsThreadRoot && (differentThread || datesDifferBetween(next, current))\n}\n\nfunction replyShadowFor(\n message: MessageResource,\n prev: MessageResource | undefined\n): ReplyShadowMessage | undefined {\n if (!message.replyRootId) return undefined\n if (message.replyRootId === message.id) return undefined\n\n const enteringNewThread =\n !prev || prev.replyRootId !== message.replyRootId || datesDifferBetween(prev, message)\n if (!enteringNewThread) return undefined\n\n return {\n type: 'ReplyShadowMessage',\n id: `${message.id}-${message.replyRootId}`,\n messageId: message.replyRootId,\n isReplyShadowMessage: true,\n nextRendersAuthor: message.renderAuthor ?? false,\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.38.0-rc.8",
3
+ "version": "3.38.0",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "react-native": "./src/index.tsx",
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "@fortawesome/fontawesome-svg-core": "^7.2.0",
28
28
  "@fortawesome/react-native-fontawesome": "^0.3.2",
29
- "@planningcenter/emoji-keyboard": "3.38.0-rc.8",
29
+ "@planningcenter/emoji-keyboard": "3.38.0",
30
30
  "lodash-inflection": "^1.5.0",
31
31
  "react-compiler-runtime": "^1.0.0"
32
32
  },
@@ -72,5 +72,5 @@
72
72
  "react-native-url-polyfill": "^2.0.0",
73
73
  "typescript": "~5.9.2"
74
74
  },
75
- "gitHead": "5986334e882cda2952bb37cffcc931ed54858d30"
75
+ "gitHead": "8b9d4026df5abb9838772a28e85ded7b059020b5"
76
76
  }
@@ -0,0 +1,50 @@
1
+ import { QueryClientProvider } from '@tanstack/react-query'
2
+ import { renderHook, waitFor } from '@testing-library/react-native'
3
+ import React from 'react'
4
+ import { buildTestQueryClient } from '../../__utils__/query_client'
5
+ import { useGroupChatConversationPayload } from '../../hooks/groups/use_group_chat_conversation_payload'
6
+ import * as useApiClientModule from '../../hooks/use_api_client'
7
+
8
+ const mockGroupsPost = (impl: jest.Mock) => {
9
+ jest.spyOn(useApiClientModule, 'useApiClient').mockReturnValue({
10
+ groups: { post: impl },
11
+ } as unknown as ReturnType<typeof useApiClientModule.useApiClient>)
12
+ return impl
13
+ }
14
+
15
+ const resolveWith = (value: string) =>
16
+ jest.fn().mockResolvedValue({
17
+ data: { type: 'GroupChatConversationPayload', id: '1', value },
18
+ links: {},
19
+ meta: {},
20
+ })
21
+
22
+ describe('useGroupChatConversationPayload', () => {
23
+ afterEach(() => {
24
+ jest.restoreAllMocks()
25
+ })
26
+
27
+ it('refetches on mount even when a fresh payload is already cached', async () => {
28
+ // Prime the cache as if a payload was fetched moments ago. The hook's 25-min staleTime
29
+ // means the default refetchOnMount would treat this as fresh and skip the request, so a
30
+ // group composition change on web would stay invisible until the cache is dropped (app
31
+ // restart). refetchOnMount: 'always' must override that and refetch on every mount.
32
+ const queryClient = buildTestQueryClient()
33
+ queryClient.setQueryData(['groups', '/me/groups/1/chat_conversation_payload'], {
34
+ data: { type: 'GroupChatConversationPayload', id: '1', value: 'stale' },
35
+ links: {},
36
+ meta: {},
37
+ })
38
+ const post = mockGroupsPost(resolveWith('fresh'))
39
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
40
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
41
+ )
42
+
43
+ const { result } = renderHook(() => useGroupChatConversationPayload({ groupId: 1 }), {
44
+ wrapper,
45
+ })
46
+
47
+ await waitFor(() => expect(post).toHaveBeenCalledTimes(1))
48
+ await waitFor(() => expect(result.current.payload).toBe('fresh'))
49
+ })
50
+ })
@@ -1,13 +1,12 @@
1
1
  import { useEffect } from 'react'
2
- import { ActivityIndicator, Pressable, StyleSheet, View } from 'react-native'
2
+ import { View, Pressable, StyleSheet } from 'react-native'
3
3
  import Animated, {
4
- Extrapolation,
4
+ useSharedValue,
5
+ useAnimatedStyle,
5
6
  interpolate,
7
+ Extrapolation,
6
8
  ReduceMotion,
7
- useAnimatedStyle,
8
- useSharedValue,
9
9
  withSpring,
10
- withTiming,
11
10
  } from 'react-native-reanimated'
12
11
  import { useTheme } from '../../hooks'
13
12
  import { platformFontWeightMedium } from '../../utils'
@@ -16,17 +15,11 @@ import { Icon, Text } from '../display'
16
15
  interface JumpToBottomButtonProps {
17
16
  onPress: () => void
18
17
  visible: boolean
19
- loading?: boolean
20
18
  }
21
19
 
22
- export const JumpToBottomButton = ({
23
- onPress,
24
- visible,
25
- loading = false,
26
- }: JumpToBottomButtonProps) => {
20
+ export const JumpToBottomButton = ({ onPress, visible }: JumpToBottomButtonProps) => {
27
21
  const styles = useStyles()
28
22
  const progress = useSharedValue(0)
29
- const loadingProgress = useSharedValue(0)
30
23
 
31
24
  useEffect(() => {
32
25
  progress.value = withSpring(visible ? 1 : 0, {
@@ -38,13 +31,6 @@ export const JumpToBottomButton = ({
38
31
  })
39
32
  }, [visible, progress])
40
33
 
41
- useEffect(() => {
42
- loadingProgress.value = withTiming(loading ? 1 : 0, {
43
- duration: 750,
44
- reduceMotion: ReduceMotion.System,
45
- })
46
- }, [loading, loadingProgress])
47
-
48
34
  const animatedStyle = useAnimatedStyle(() => {
49
35
  return {
50
36
  opacity: progress.value,
@@ -59,34 +45,16 @@ export const JumpToBottomButton = ({
59
45
  }
60
46
  })
61
47
 
62
- const iconStyle = useAnimatedStyle(() => ({ opacity: 1 - loadingProgress.value }))
63
- const spinnerStyle = useAnimatedStyle(() => ({ opacity: loadingProgress.value }))
64
-
65
48
  return (
66
49
  <View>
67
- <Animated.View
68
- style={[styles.container, animatedStyle]}
69
- pointerEvents={visible ? 'auto' : 'none'}
70
- >
50
+ <Animated.View style={[styles.container, animatedStyle]}>
71
51
  <Pressable
72
52
  onPress={onPress}
73
- disabled={loading}
74
- accessibilityRole="button"
75
- accessibilityLabel="Jump to most recent message"
76
- accessibilityState={{ busy: loading }}
77
- hitSlop={hitSlop}
78
53
  style={({ pressed }) => [styles.button, pressed && styles.pressed]}
79
54
  >
80
- <View style={styles.glyph}>
81
- <Animated.View style={[styles.glyphLayer, iconStyle]}>
82
- <Icon name="general.downArrow" style={styles.icon} />
83
- </Animated.View>
84
- <Animated.View style={[styles.glyphLayer, spinnerStyle]}>
85
- <ActivityIndicator size="small" color={styles.icon.color} style={styles.spinner} />
86
- </Animated.View>
87
- </View>
55
+ <Icon name="general.downArrow" style={styles.icon} />
88
56
  <Text variant="tertiary" style={styles.text}>
89
- {loading ? 'Jumping to latest…' : 'Jump to bottom'}
57
+ Jump to bottom
90
58
  </Text>
91
59
  </Pressable>
92
60
  </Animated.View>
@@ -94,8 +62,6 @@ export const JumpToBottomButton = ({
94
62
  )
95
63
  }
96
64
 
97
- const hitSlop = { top: 12, bottom: 12, left: 12, right: 12 }
98
-
99
65
  const useStyles = () => {
100
66
  const { colors } = useTheme()
101
67
 
@@ -127,25 +93,10 @@ const useStyles = () => {
127
93
  color: colors.fillColorNeutral100Inverted,
128
94
  fontWeight: platformFontWeightMedium,
129
95
  },
130
- glyph: {
131
- width: 14,
132
- height: 14,
133
- alignItems: 'center',
134
- justifyContent: 'center',
135
- },
136
- glyphLayer: {
137
- position: 'absolute',
138
- alignItems: 'center',
139
- justifyContent: 'center',
140
- },
141
96
  icon: {
142
97
  color: colors.fillColorNeutral100Inverted,
143
98
  fontSize: 14,
144
99
  },
145
- spinner: {
146
- width: 14,
147
- height: 14,
148
- },
149
100
  pressed: {
150
101
  transform: [{ scale: 0.95 }],
151
102
  },
@@ -28,7 +28,7 @@ import {
28
28
  import { Avatar, Icon, IconProps, Image, Text } from '../display'
29
29
  import { TheirReplyConnector, MyReplyConnector } from './reply_connectors'
30
30
 
31
- interface ReplyShadowMessageProps {
31
+ interface ReplyShadowMessageProps extends MessageResource {
32
32
  messageId: string
33
33
  conversation_id: number
34
34
  inReplyScreen?: boolean
@@ -1,59 +1,31 @@
1
- import React, { createContext, PropsWithChildren, useContext, useMemo, useState } from 'react'
1
+ import React, { createContext, PropsWithChildren, useContext, useMemo } from 'react'
2
2
 
3
3
  interface ConversationContextValue {
4
4
  conversationId: number
5
5
  currentPageReplyRootId: string | null
6
- initialMessageId: string | null
7
- setInitialMessageId: (id: string | null) => void
8
- initialMessageIdIsAnchor: boolean
9
- atEndOfMessageHistory: boolean
10
- setAtEndOfMessageHistory: (atEnd: boolean) => void
11
6
  }
12
7
 
13
8
  interface ConversationContextProviderProps extends PropsWithChildren {
14
9
  conversationId: number
15
10
  currentPageReplyRootId: string | null
16
- initialMessageId?: string | null
17
- initialMessageIdIsAnchor?: boolean
18
11
  }
19
12
 
20
13
  const ConversationContext = createContext<ConversationContextValue>({
21
14
  conversationId: 0,
22
15
  currentPageReplyRootId: null,
23
- initialMessageId: null,
24
- setInitialMessageId: () => {},
25
- initialMessageIdIsAnchor: false,
26
- atEndOfMessageHistory: false,
27
- setAtEndOfMessageHistory: () => {},
28
16
  })
29
17
 
30
18
  export const ConversationContextProvider = ({
31
19
  children,
32
20
  conversationId,
33
21
  currentPageReplyRootId,
34
- initialMessageId: initialMessageIdProp = null,
35
- initialMessageIdIsAnchor = false,
36
22
  }: ConversationContextProviderProps) => {
37
- const [initialMessageId, setInitialMessageId] = useState(initialMessageIdProp)
38
- const [atEndOfMessageHistory, setAtEndOfMessageHistory] = useState(false)
39
-
40
23
  const value = useMemo(
41
24
  () => ({
42
25
  conversationId,
43
26
  currentPageReplyRootId,
44
- initialMessageId,
45
- setInitialMessageId,
46
- initialMessageIdIsAnchor,
47
- atEndOfMessageHistory,
48
- setAtEndOfMessageHistory,
49
27
  }),
50
- [
51
- conversationId,
52
- currentPageReplyRootId,
53
- initialMessageId,
54
- initialMessageIdIsAnchor,
55
- atEndOfMessageHistory,
56
- ]
28
+ [conversationId, currentPageReplyRootId]
57
29
  )
58
30
 
59
31
  return <ConversationContext.Provider value={value}>{children}</ConversationContext.Provider>
@@ -26,6 +26,7 @@ export function useGroupChatConversationPayload({ groupId, enabled = true }: Pro
26
26
  },
27
27
  }),
28
28
  staleTime: STALE_TIME_MS,
29
+ refetchOnMount: 'always',
29
30
  enabled,
30
31
  })
31
32
 
@@ -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
  }
@@ -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
  }
@@ -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