@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
@@ -13,9 +13,10 @@ export function useFlattenedArrayOfServiceTypesWithTeams({
13
13
  firstRowStyle,
14
14
  lastRowStyle,
15
15
  }: Props) {
16
- const flattenedData: SectionListData = useMemo(
17
- () =>
18
- data.flatMap(serviceType => {
16
+ const flattenedData: SectionListData = useMemo(() => {
17
+ const serviceTypeRows = data
18
+ .filter(serviceType => serviceType.id > 0)
19
+ .flatMap(serviceType => {
19
20
  const teamIdsForServiceType = serviceType.teams.map(team => team.id)
20
21
 
21
22
  return [
@@ -28,23 +29,49 @@ export function useFlattenedArrayOfServiceTypesWithTeams({
28
29
  },
29
30
  sectionStyle: firstRowStyle,
30
31
  },
31
- ...serviceType.teams.map((team, teamIdx) => {
32
- const isLastTeamInServiceType = teamIdx === serviceType.teams.length - 1
33
-
34
- return {
35
- type: SectionTypes.team as const,
36
- data: {
37
- teamName: team.name,
38
- teamId: team.id,
39
- serviceTypeId: serviceType.id,
40
- },
41
- sectionStyle: isLastTeamInServiceType ? lastRowStyle : undefined,
42
- }
43
- }),
32
+ ...serviceType.teams.map((team, teamIdx) => ({
33
+ type: SectionTypes.team as const,
34
+ data: {
35
+ teamName: team.name,
36
+ teamId: team.id,
37
+ serviceTypeId: serviceType.id,
38
+ },
39
+ sectionStyle: teamIdx === serviceType.teams.length - 1 ? lastRowStyle : undefined,
40
+ })),
44
41
  ]
45
- }),
46
- [data, firstRowStyle, lastRowStyle]
47
- )
42
+ })
43
+
44
+ // Teams without a service type (id < 0) are merged under a single "No service type" section.
45
+ // Service type ID 0 is a safe sentinel — real IDs are always positive.
46
+ const serviceTypelessTeams = data
47
+ .filter(serviceType => serviceType.id < 0)
48
+ .flatMap(serviceType => serviceType.teams)
49
+
50
+ if (serviceTypelessTeams.length === 0) return serviceTypeRows
51
+
52
+ const serviceTypelessRows: SectionListData = [
53
+ {
54
+ type: SectionTypes.header as const,
55
+ data: {
56
+ serviceTypeName: null,
57
+ serviceTypeId: 0,
58
+ teamIdsForServiceType: serviceTypelessTeams.map(t => t.id),
59
+ },
60
+ sectionStyle: firstRowStyle,
61
+ },
62
+ ...serviceTypelessTeams.map((team, teamIdx) => ({
63
+ type: SectionTypes.team as const,
64
+ data: {
65
+ teamName: team.name,
66
+ teamId: team.id,
67
+ serviceTypeId: 0,
68
+ },
69
+ sectionStyle: teamIdx === serviceTypelessTeams.length - 1 ? lastRowStyle : undefined,
70
+ })),
71
+ ]
72
+
73
+ return [...serviceTypelessRows, ...serviceTypeRows]
74
+ }, [data, firstRowStyle, lastRowStyle])
48
75
 
49
76
  return flattenedData
50
77
  }
@@ -59,45 +59,47 @@ const useTeams = ({ filterType }: { filterType: TeamFilterTypes }) => {
59
59
  return { data: result || [], ...rest }
60
60
  }
61
61
 
62
- function decorateTeamResponseItems(teamResponseItems: TeamResponseItem[], searchQuery?: string) {
63
- return teamResponseItems
64
- .filter(item => {
65
- if (!searchQuery) return true
62
+ export function decorateTeamResponseItems(
63
+ teamResponseItems: TeamResponseItem[],
64
+ searchQuery?: string
65
+ ) {
66
+ const filtered = teamResponseItems.filter(item => {
67
+ if (!searchQuery) return true
68
+ const evalMatch = (str: string) => str.toLowerCase().includes(searchQuery.toLowerCase())
69
+ return evalMatch(item.name) || evalMatch(item.serviceTypeNames?.join(',') || '')
70
+ })
66
71
 
67
- const evalMatch = (str: string) => str.toLowerCase().includes(searchQuery.toLowerCase())
68
- const teamNameMatch = evalMatch(item.name)
69
- const serviceTypeNamesMatch = evalMatch(item.serviceTypeNames?.join(',') || '')
72
+ const withServiceTypes = filtered.filter(item => item.value.serviceTypeIds.length > 0)
73
+ const withoutServiceTypes = filtered.filter(item => item.value.serviceTypeIds.length === 0)
70
74
 
71
- return teamNameMatch || serviceTypeNamesMatch
72
- })
73
- .map(({ value, serviceTypeNames, teamName }) => {
74
- return {
75
- service_types: value.serviceTypeIds.map((serviceTypeId, i) => ({
76
- id: serviceTypeId,
77
- name: serviceTypeNames[i],
78
- })),
79
- team: {
80
- id: value.teamId,
81
- name: teamName,
82
- },
83
- }
75
+ // Negative team ID is used as a unique sentinel — real service type IDs are always positive.
76
+ const typelessEntries: ServiceTypeWithTeams[] = withoutServiceTypes.map(
77
+ ({ value, teamName }) => ({
78
+ id: -value.teamId,
79
+ name: teamName,
80
+ teams: [{ id: value.teamId, name: teamName }],
84
81
  })
82
+ )
83
+
84
+ const typedEntries = withServiceTypes
85
+ .map(({ value, serviceTypeNames, teamName }) => ({
86
+ service_types: value.serviceTypeIds.map((serviceTypeId, i) => ({
87
+ id: serviceTypeId,
88
+ name: serviceTypeNames[i],
89
+ })),
90
+ team: { id: value.teamId, name: teamName },
91
+ }))
85
92
  .reduce((acc: ServiceTypeWithTeams[], { service_types, team }) => {
86
93
  service_types.forEach(serviceType => {
87
94
  let serviceTypeEntry = acc.find(entry => entry.id === serviceType.id)
88
-
89
95
  if (!serviceTypeEntry) {
90
- serviceTypeEntry = {
91
- id: serviceType.id,
92
- name: serviceType.name,
93
- teams: [],
94
- }
96
+ serviceTypeEntry = { id: serviceType.id, name: serviceType.name, teams: [] }
95
97
  acc.push(serviceTypeEntry)
96
98
  }
97
-
98
- const initialTeams = serviceTypeEntry.teams
99
- serviceTypeEntry.teams = uniqBy([...initialTeams, team], 'id')
99
+ serviceTypeEntry.teams = uniqBy([...serviceTypeEntry.teams, team], 'id')
100
100
  })
101
101
  return acc
102
102
  }, [])
103
+
104
+ return [...typelessEntries, ...typedEntries]
103
105
  }
@@ -17,7 +17,7 @@ export enum SectionTypes {
17
17
  }
18
18
 
19
19
  export interface ServiceTypeProps {
20
- serviceTypeName: string
20
+ serviceTypeName: string | null
21
21
  serviceTypeId: number
22
22
  teamIdsForServiceType: number[]
23
23
  }
@@ -8,9 +8,8 @@ import {
8
8
  useTheme as useNavigationTheme,
9
9
  useRoute,
10
10
  } from '@react-navigation/native'
11
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
12
- import { ActivityIndicator, FlatList, Platform, StyleSheet, View } from 'react-native'
13
- import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
11
+ import React, { useCallback, useEffect, useRef, useState } from 'react'
12
+ import { FlatList, Platform, StyleSheet, View } from 'react-native'
14
13
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
15
14
  import { Badge, Icon, Text } from '../components'
16
15
  import { EmptyConversationBlankState } from '../components/conversation/empty_conversation_blank_state'
@@ -25,46 +24,25 @@ import {
25
24
  import { ReplyShadowMessage } from '../components/conversation/reply_shadow_message'
26
25
  import { SystemMessage } from '../components/conversation/system_message'
27
26
  import { TypingIndicator } from '../components/conversation/typing_indicator'
28
- import { UnreadDivider } from '../components/conversation/unread_divider'
29
27
  import { KeyboardView } from '../components/display/keyboard_view'
30
28
  import BlankState from '../components/primitive/blank_state_primitive'
31
- import {
32
- ConversationContextProvider,
33
- useConversationContext,
34
- } from '../contexts/conversation_context'
29
+ import { ConversationContextProvider } from '../contexts/conversation_context'
35
30
  import { useTheme } from '../hooks'
36
31
  import { useConversation } from '../hooks/use_conversation'
37
32
  import { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events'
38
33
  import { useConversationMessages } from '../hooks/use_conversation_messages'
39
34
  import { useConversationMessagesJoltEvents } from '../hooks/use_conversation_messages_jolt_events'
40
- import { availableFeatures, useFeatures } from '../hooks/use_features'
41
- import { useFlatListViewability } from '../hooks/use_flat_list_viewability'
42
- import { useJumpToBottomAction } from '../hooks/use_jump_to_bottom_action'
43
- import { useJumpToUnreadAnchor } from '../hooks/use_jump_to_unread_anchor'
44
- import { useJumpToUnreadGates } from '../hooks/use_jump_to_unread_gates'
45
35
  import { useMarkLatestMessageRead } from '../hooks/use_mark_latest_message_read'
46
36
  import {
47
37
  analyticsEvents,
48
38
  normalizeAnalyticsMetadata,
49
39
  usePublishProductAnalyticsEvent,
50
40
  } from '../hooks/use_product_analytics'
51
- import { useScrollTracking } from '../hooks/use_scroll_tracking'
52
- import { useTrackHighestSeenMessage } from '../hooks/use_track_highest_seen_message'
53
41
  import { ConversationResource } from '../types/resources/conversation'
54
42
  import { ConversationBadgeResource } from '../types/resources/conversation_badge'
55
43
  import { MessageResource } from '../types/resources/message'
56
44
  import { getRelativeDateStatus } from '../utils/date'
57
- import {
58
- groupMessages,
59
- UNREAD_DIVIDER_KEY,
60
- type DateSeparator,
61
- type EnrichedMessage,
62
- } from '../utils/group_messages'
63
- import {
64
- detectDividerExitTowardNewer,
65
- reportViewableMessages,
66
- type ViewabilityObserver,
67
- } from '../utils/message_viewability'
45
+ import { groupMessages, type DateSeparator } from '../utils/group_messages'
68
46
  import { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL } from '../utils/styles'
69
47
  import { isSystemMessage } from '../utils/system_messages'
70
48
 
@@ -75,7 +53,6 @@ export type ConversationRouteProps = {
75
53
  chat_group_graph_id?: string
76
54
  clear_input?: boolean
77
55
  editing_message_id?: number | null
78
- message_id?: string
79
56
  title?: string
80
57
  subtitle?: string
81
58
  badge?: ConversationBadgeResource
@@ -85,34 +62,20 @@ export type ConversationRouteProps = {
85
62
 
86
63
  export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
87
64
 
88
- const extractItemKey = (item: EnrichedMessage) => String(item.id)
89
- const maintainVisibleContentPosition = { minIndexForVisible: 0 }
90
-
91
65
  export function ConversationScreen({ route }: ConversationScreenProps) {
92
- const { conversation_id, message_id, reply_root_id } = route.params
66
+ const { conversation_id, reply_root_id } = route.params
93
67
 
94
68
  const { data: conversation } = useConversation({ conversation_id })
95
- const { featureEnabled } = useFeatures()
96
69
 
97
70
  usePublishProductAnalyticsEvent(analyticsEvents.conversation_show_opened, {
98
71
  reply_root_id,
99
72
  ...normalizeAnalyticsMetadata(conversation),
100
73
  })
101
74
 
102
- const lastReadMessageSortKey = conversation.conversationMembership?.lastReadMessageSortKey ?? null
103
- const jumpToUnreadAnchor =
104
- featureEnabled(availableFeatures.jump_to_unread) && !reply_root_id
105
- ? lastReadMessageSortKey
106
- : null
107
- const initialMessageId = message_id ?? jumpToUnreadAnchor
108
- const initialMessageIdIsAnchor = !!initialMessageId && !message_id
109
-
110
75
  return (
111
76
  <ConversationContextProvider
112
77
  conversationId={conversation_id}
113
78
  currentPageReplyRootId={reply_root_id ?? null}
114
- initialMessageId={initialMessageId}
115
- initialMessageIdIsAnchor={initialMessageIdIsAnchor}
116
79
  >
117
80
  <ConversationScreenContent route={route} />
118
81
  </ConversationContextProvider>
@@ -122,49 +85,29 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
122
85
  function ConversationScreenContent({ route }: ConversationScreenProps) {
123
86
  const styles = useStyles()
124
87
  const navigation = useNavigation()
125
- const {
126
- conversation_id: conversationId,
127
- editing_message_id: editingMessageId,
128
- reply_root_id: replyRootId,
129
- reply_root_author_name: replyRootAuthorName,
130
- } = route.params
88
+ const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =
89
+ route.params
131
90
  const { data: conversation } = useConversation(route.params)
132
- const {
133
- messages,
134
- fetchOlderMessages,
135
- fetchNewerMessages,
136
- hasMoreNewerMessages,
137
- isFetchingNewerMessages,
138
- cancelFetchNewerMessages,
139
- } = useConversationMessages({ conversation_id: conversationId, reply_root_id: replyRootId })
140
-
141
- const { jumpToUnreadActive } = useJumpToUnreadGates()
142
- const { initialMessageId } = useConversationContext()
143
-
144
- useConversationJoltEvents({ conversationId })
145
- useConversationMessagesJoltEvents({ conversationId })
91
+ const { messages, fetchNextPage } = useConversationMessages({
92
+ conversation_id,
93
+ reply_root_id,
94
+ })
95
+ useConversationJoltEvents({ conversationId: conversation_id })
96
+ useConversationMessagesJoltEvents({ conversationId: conversation_id })
146
97
  useEnsureConversationsRouteExists()
147
98
  useMarkLatestMessageRead({ conversation, messages })
148
- const { onMessageSeen } = useTrackHighestSeenMessage()
149
-
150
- const items = useMemo(
151
- () =>
152
- groupMessages({
153
- ms: messages,
154
- inReplyScreen: !!replyRootId,
155
- jumpToUnreadActive,
156
- initialMessageId,
157
- }),
158
- [messages, replyRootId, jumpToUnreadActive, initialMessageId]
159
- )
160
- const noMessages = items.length === 0
99
+ const messagesWithSeparators = groupMessages({
100
+ ms: messages,
101
+ inReplyScreen: !!reply_root_id,
102
+ })
103
+ const noMessages = messagesWithSeparators.length === 0
161
104
 
162
105
  const { repliesDisabled, memberAbility, badges, title } = conversation
163
106
  const canReply = memberAbility?.canReply
164
107
  const showLeaderDisabledReplyBanner = canReply && repliesDisabled
165
108
  const canDeleteNonAuthoredMessages = memberAbility?.canDeleteNonAuthoredMessages ?? false
166
- const currentlyEditingMessage = messages.find(m => String(m.id) === String(editingMessageId))
167
- const replyRootAuthorFirstName = replyRootAuthorName?.split(' ')[0]
109
+ const currentlyEditingMessage = messages.find(m => String(m.id) === String(editing_message_id))
110
+ const replyRootAuthorFirstName = reply_root_author_name?.split(' ')[0]
168
111
  const replyHeaderTitle = replyRootAuthorFirstName
169
112
  ? `Reply to ${replyRootAuthorFirstName}`
170
113
  : 'Reply'
@@ -172,96 +115,21 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
172
115
  const muted = conversation.conversationMembership?.muted ?? conversation.muted
173
116
 
174
117
  const listRef = useRef<FlatList>(null)
175
- const [dividerScrolledPast, setDividerScrolledPast] = useState(false)
176
-
177
- const observers = useMemo<ViewabilityObserver<EnrichedMessage>[]>(
178
- () => [
179
- reportViewableMessages(onMessageSeen),
180
- detectDividerExitTowardNewer({
181
- dividerKey: UNREAD_DIVIDER_KEY,
182
- initialMessageId,
183
- onExited: () => setDividerScrolledPast(true),
184
- }),
185
- ],
186
- [onMessageSeen, initialMessageId]
187
- )
118
+ const [showJumpToBottomButton, setShowJumpToBottomButton] = useState(false)
188
119
 
189
- const { viewabilityConfigCallbackPairs, onScrollBeginDrag: viewabilityOnScrollBeginDrag } =
190
- useFlatListViewability({ observers })
191
- const {
192
- onContentSizeChange,
193
- onScrollToIndexFailed,
194
- onScrollBeginDrag: anchorOnScrollBeginDrag,
195
- } = useJumpToUnreadAnchor({ listRef, items })
196
- const onScrollBeginDrag = useCallback(() => {
197
- viewabilityOnScrollBeginDrag()
198
- anchorOnScrollBeginDrag()
199
- }, [viewabilityOnScrollBeginDrag, anchorOnScrollBeginDrag])
200
- const { onScroll, showJumpToBottomButton } = useScrollTracking({
201
- hasMoreNewerMessages,
202
- isFetchingNewerMessages,
203
- fetchNewerMessages,
204
- cancelFetchNewerMessages,
205
- })
206
- const { handleJumpToBottom, isJumpingToBottom } = useJumpToBottomAction({ listRef })
207
-
208
- const listHeader = useMemo(
209
- () => (
210
- <View>
211
- {isFetchingNewerMessages && (
212
- <Animated.View
213
- entering={FadeIn.duration(750)}
214
- exiting={FadeOut.duration(750)}
215
- style={styles.loadingFooter}
216
- accessibilityRole="progressbar"
217
- accessibilityLabel="Loading more messages"
218
- >
219
- <ActivityIndicator />
220
- </Animated.View>
221
- )}
222
- <View style={styles.listHeader} />
223
- </View>
224
- ),
225
- [isFetchingNewerMessages, styles.loadingFooter, styles.listHeader]
226
- )
120
+ const trackScroll = (event: any) => {
121
+ const offsetY = event.nativeEvent.contentOffset.y
122
+ setShowJumpToBottomButton(offsetY > 200)
123
+ }
227
124
 
228
- const renderItem = useCallback(
229
- ({ item }: { item: EnrichedMessage }) => {
230
- if (item.type === 'DateSeparator') return <InlineDateSeparator {...item} />
231
- if (item.type === 'UnreadDivider') return <UnreadDivider scrolledPast={dividerScrolledPast} />
232
- if (item.type === 'ReplyShadowMessage') {
233
- return (
234
- <ReplyShadowMessage
235
- {...item}
236
- conversation_id={conversationId}
237
- inReplyScreen={!!replyRootId}
238
- />
239
- )
240
- }
241
- if (isSystemMessage(item)) {
242
- return <SystemMessage message={item} conversationId={conversationId} />
243
- }
244
- return (
245
- <Message
246
- {...item}
247
- canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages}
248
- conversation_id={conversationId}
249
- latestReadMessageSortKey={conversation?.latestReadMessageSortKey}
250
- inReplyScreen={!!replyRootId}
251
- />
252
- )
253
- },
254
- [
255
- dividerScrolledPast,
256
- conversationId,
257
- replyRootId,
258
- canDeleteNonAuthoredMessages,
259
- conversation?.latestReadMessageSortKey,
260
- ]
261
- )
125
+ const handleReturnToBottom = useCallback(() => {
126
+ listRef.current?.scrollToOffset({
127
+ offset: 0,
128
+ })
129
+ }, [])
262
130
 
263
131
  useEffect(() => {
264
- if (replyRootId) {
132
+ if (reply_root_id) {
265
133
  navigation.setParams({
266
134
  title: replyHeaderTitle,
267
135
  })
@@ -273,7 +141,7 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
273
141
  muted,
274
142
  })
275
143
  }
276
- }, [navigation, title, badges, conversation?.deleted, replyRootId, replyHeaderTitle, muted])
144
+ }, [navigation, title, badges, conversation?.deleted, reply_root_id, replyHeaderTitle, muted])
277
145
 
278
146
  if (!conversation || conversation.deleted) {
279
147
  return (
@@ -304,32 +172,51 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
304
172
  inverted
305
173
  ref={listRef}
306
174
  contentContainerStyle={styles.listContainer}
307
- maintainVisibleContentPosition={maintainVisibleContentPosition}
308
- data={items}
309
- keyExtractor={extractItemKey}
310
- onScroll={onScroll}
311
- onScrollBeginDrag={onScrollBeginDrag}
312
- scrollEventThrottle={64}
313
- viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs}
314
- onContentSizeChange={onContentSizeChange}
315
- onScrollToIndexFailed={onScrollToIndexFailed}
316
- renderItem={renderItem}
317
- onEndReached={() => fetchOlderMessages()}
318
- ListHeaderComponent={listHeader}
175
+ data={messagesWithSeparators}
176
+ keyExtractor={item => item.id}
177
+ onScroll={trackScroll}
178
+ scrollEventThrottle={10}
179
+ renderItem={({ item }) => {
180
+ if (item.type === 'DateSeparator') {
181
+ return <InlineDateSeparator {...item} />
182
+ }
183
+
184
+ if (item.type === 'ReplyShadowMessage') {
185
+ return (
186
+ <ReplyShadowMessage
187
+ {...(item as any)}
188
+ conversation_id={conversation_id}
189
+ inReplyScreen={!!reply_root_id}
190
+ />
191
+ )
192
+ }
193
+
194
+ if (isSystemMessage(item)) {
195
+ return <SystemMessage message={item} conversationId={conversation_id} />
196
+ }
197
+
198
+ return (
199
+ <Message
200
+ {...item}
201
+ canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages}
202
+ conversation_id={conversation_id}
203
+ latestReadMessageSortKey={conversation?.latestReadMessageSortKey}
204
+ inReplyScreen={!!reply_root_id}
205
+ />
206
+ )
207
+ }}
208
+ onEndReached={() => fetchNextPage()}
209
+ ListHeaderComponent={<View style={styles.listHeader} />}
319
210
  />
320
211
  )}
321
- <JumpToBottomButton
322
- onPress={handleJumpToBottom}
323
- visible={showJumpToBottomButton}
324
- loading={isJumpingToBottom}
325
- />
212
+ <JumpToBottomButton onPress={handleReturnToBottom} visible={showJumpToBottomButton} />
326
213
  {!noMessages && <TypingIndicator />}
327
214
  {showLeaderDisabledReplyBanner && <LeaderMessagesDisabledBanner />}
328
215
  <ConversationBottomBar
329
216
  conversation={conversation}
330
217
  canReply={canReply}
331
218
  replyRootAuthorFirstName={replyRootAuthorFirstName}
332
- replyRootId={replyRootId}
219
+ replyRootId={reply_root_id}
333
220
  currentlyEditingMessage={currentlyEditingMessage}
334
221
  />
335
222
  </KeyboardView>
@@ -506,10 +393,6 @@ const useStyles = () => {
506
393
  // Just whitespace to provide space where the typing indicator can be
507
394
  height: 16,
508
395
  },
509
- loadingFooter: {
510
- paddingVertical: 12,
511
- alignItems: 'center',
512
- },
513
396
  })
514
397
  }
515
398
 
@@ -11,7 +11,7 @@ interface RecipientLinkRowProps {
11
11
  accessibilityHint: string
12
12
  imageUri?: string
13
13
  title: string
14
- subtitle: string
14
+ subtitle?: string
15
15
  }
16
16
 
17
17
  export const RecipientLinkRow = ({
@@ -40,9 +40,11 @@ export const RecipientLinkRow = ({
40
40
  <Text style={styles.title} numberOfLines={2}>
41
41
  {title}
42
42
  </Text>
43
- <Text variant="tertiary" numberOfLines={1}>
44
- {subtitle}
45
- </Text>
43
+ {subtitle && (
44
+ <Text variant="tertiary" numberOfLines={1}>
45
+ {subtitle}
46
+ </Text>
47
+ )}
46
48
  </View>
47
49
  {Platform.OS === 'ios' && (
48
50
  <Icon name="general.rightChevron" size={16} style={styles.icon} />
@@ -9,7 +9,8 @@ interface TeamRecipientRowProps {
9
9
 
10
10
  export const TeamRecipientRow = ({ serviceType, onPress }: TeamRecipientRowProps) => {
11
11
  const serviceTypeAccessibilityLabel = `Select ${pluralize(serviceType.teams.length, 'team')} for ${serviceType.name}`
12
- const teamNames = serviceType.teams.map(team => team.name).join(', ')
12
+ const teamNames =
13
+ serviceType.id > 0 ? serviceType.teams.map(team => team.name).join(', ') : undefined
13
14
 
14
15
  return (
15
16
  <RecipientLinkRow
@@ -2,8 +2,10 @@ import { StackActions, StaticScreenProps, useNavigation } from '@react-navigatio
2
2
  import { useQuery, useQueryClient } from '@tanstack/react-query'
3
3
  import { useEffect } from 'react'
4
4
  import { DefaultLoading } from '../components/page/loading'
5
+ import BlankState from '../components/primitive/blank_state_primitive'
5
6
  import { useApiClient } from '../hooks'
6
7
  import { findOrCreateServicesConversation } from '../hooks/services/use_find_or_create_services_conversation'
8
+ import { ResponseError } from '../utils/response_error'
7
9
 
8
10
  export type TeamConversationRouteProps = {
9
11
  plan_id?: number
@@ -16,7 +18,11 @@ export const TeamConversationScreen = ({ route }: TeamConversationScreenProps) =
16
18
  const apiClient = useApiClient()
17
19
  const queryClient = useQueryClient()
18
20
  const navigation = useNavigation()
19
- const { data: conversation } = useQuery({
21
+ const {
22
+ data: conversation,
23
+ isError,
24
+ error,
25
+ } = useQuery({
20
26
  queryKey: ['team-conversation', route.params.team_ids, route.params.plan_id],
21
27
  queryFn: () =>
22
28
  findOrCreateServicesConversation({
@@ -24,6 +30,7 @@ export const TeamConversationScreen = ({ route }: TeamConversationScreenProps) =
24
30
  teamIds: route.params.team_ids ?? [],
25
31
  planId: route.params.plan_id,
26
32
  }).then(r => r.conversation),
33
+ retry: (failureCount, err) => !(err instanceof ResponseError) && failureCount < 3,
27
34
  })
28
35
 
29
36
  useEffect(() => {
@@ -41,5 +48,30 @@ export const TeamConversationScreen = ({ route }: TeamConversationScreenProps) =
41
48
  }
42
49
  }, [conversation?.id, conversation?.title, navigation, queryClient])
43
50
 
51
+ if (isError) return <TeamConversationError error={error} onGoBack={navigation.goBack} />
52
+
44
53
  return <DefaultLoading />
45
54
  }
55
+
56
+ function TeamConversationError({ error, onGoBack }: { error: Error; onGoBack: () => void }) {
57
+ const detail =
58
+ error instanceof ResponseError
59
+ ? error.errors
60
+ .map(e => e.detail)
61
+ .filter(Boolean)
62
+ .join('\n')
63
+ : ''
64
+
65
+ return (
66
+ <BlankState.Root>
67
+ <BlankState.Imagery name="people.noTextMessage" />
68
+ <BlankState.Content>
69
+ <BlankState.Heading>Can't start this conversation</BlankState.Heading>
70
+ <BlankState.Text>
71
+ {detail || 'Something went wrong while starting this conversation.'}
72
+ </BlankState.Text>
73
+ </BlankState.Content>
74
+ <BlankState.Button title="Go back" onPress={onGoBack} size="md" accessibilityRole="link" />
75
+ </BlankState.Root>
76
+ )
77
+ }