@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
@@ -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
+ })
@@ -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,17 +8,14 @@ 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, Platform, StyleSheet, View } from 'react-native'
13
- import type { FlatList } from 'react-native-gesture-handler'
14
- 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'
15
13
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
16
14
  import { Badge, Icon, Text } from '../components'
17
15
  import { EmptyConversationBlankState } from '../components/conversation/empty_conversation_blank_state'
18
16
  import { JumpToBottomButton } from '../components/conversation/jump_to_bottom_button'
19
17
  import { Message } from '../components/conversation/message'
20
18
  import { MessageForm } from '../components/conversation/message_form'
21
- import { MessageList } from '../components/conversation/message_list'
22
19
  import {
23
20
  ConversationDisabledBanner,
24
21
  LeaderMessagesDisabledBanner,
@@ -27,46 +24,25 @@ import {
27
24
  import { ReplyShadowMessage } from '../components/conversation/reply_shadow_message'
28
25
  import { SystemMessage } from '../components/conversation/system_message'
29
26
  import { TypingIndicator } from '../components/conversation/typing_indicator'
30
- import { UnreadDivider } from '../components/conversation/unread_divider'
31
27
  import { KeyboardView } from '../components/display/keyboard_view'
32
28
  import BlankState from '../components/primitive/blank_state_primitive'
33
- import {
34
- ConversationContextProvider,
35
- useConversationContext,
36
- } from '../contexts/conversation_context'
29
+ import { ConversationContextProvider } from '../contexts/conversation_context'
37
30
  import { useTheme } from '../hooks'
38
31
  import { useConversation } from '../hooks/use_conversation'
39
32
  import { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events'
40
33
  import { useConversationMessages } from '../hooks/use_conversation_messages'
41
34
  import { useConversationMessagesJoltEvents } from '../hooks/use_conversation_messages_jolt_events'
42
- import { availableFeatures, useFeatures } from '../hooks/use_features'
43
- import { useFlatListViewability } from '../hooks/use_flat_list_viewability'
44
- import { useJumpToBottomAction } from '../hooks/use_jump_to_bottom_action'
45
- import { useJumpToUnreadAnchor } from '../hooks/use_jump_to_unread_anchor'
46
- import { useJumpToUnreadGates } from '../hooks/use_jump_to_unread_gates'
47
35
  import { useMarkLatestMessageRead } from '../hooks/use_mark_latest_message_read'
48
36
  import {
49
37
  analyticsEvents,
50
38
  normalizeAnalyticsMetadata,
51
39
  usePublishProductAnalyticsEvent,
52
40
  } from '../hooks/use_product_analytics'
53
- import { useScrollTracking } from '../hooks/use_scroll_tracking'
54
- import { useTrackHighestSeenMessage } from '../hooks/use_track_highest_seen_message'
55
41
  import { ConversationResource } from '../types/resources/conversation'
56
42
  import { ConversationBadgeResource } from '../types/resources/conversation_badge'
57
43
  import { MessageResource } from '../types/resources/message'
58
44
  import { getRelativeDateStatus } from '../utils/date'
59
- import {
60
- groupMessages,
61
- UNREAD_DIVIDER_KEY,
62
- type DateSeparator,
63
- type EnrichedMessage,
64
- } from '../utils/group_messages'
65
- import {
66
- detectDividerExitTowardNewer,
67
- reportViewableMessages,
68
- type ViewabilityObserver,
69
- } from '../utils/message_viewability'
45
+ import { groupMessages, type DateSeparator } from '../utils/group_messages'
70
46
  import { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL } from '../utils/styles'
71
47
  import { isSystemMessage } from '../utils/system_messages'
72
48
 
@@ -77,7 +53,6 @@ export type ConversationRouteProps = {
77
53
  chat_group_graph_id?: string
78
54
  clear_input?: boolean
79
55
  editing_message_id?: number | null
80
- message_id?: string
81
56
  title?: string
82
57
  subtitle?: string
83
58
  badge?: ConversationBadgeResource
@@ -88,30 +63,19 @@ export type ConversationRouteProps = {
88
63
  export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
89
64
 
90
65
  export function ConversationScreen({ route }: ConversationScreenProps) {
91
- const { conversation_id, message_id, reply_root_id } = route.params
66
+ const { conversation_id, reply_root_id } = route.params
92
67
 
93
68
  const { data: conversation } = useConversation({ conversation_id })
94
- const { featureEnabled } = useFeatures()
95
69
 
96
70
  usePublishProductAnalyticsEvent(analyticsEvents.conversation_show_opened, {
97
71
  reply_root_id,
98
72
  ...normalizeAnalyticsMetadata(conversation),
99
73
  })
100
74
 
101
- const lastReadMessageSortKey = conversation.conversationMembership?.lastReadMessageSortKey ?? null
102
- const jumpToUnreadAnchor =
103
- featureEnabled(availableFeatures.jump_to_unread) && !reply_root_id
104
- ? lastReadMessageSortKey
105
- : null
106
- const initialMessageId = message_id ?? jumpToUnreadAnchor
107
- const initialMessageIdIsAnchor = !!initialMessageId && !message_id
108
-
109
75
  return (
110
76
  <ConversationContextProvider
111
77
  conversationId={conversation_id}
112
78
  currentPageReplyRootId={reply_root_id ?? null}
113
- initialMessageId={initialMessageId}
114
- initialMessageIdIsAnchor={initialMessageIdIsAnchor}
115
79
  >
116
80
  <ConversationScreenContent route={route} />
117
81
  </ConversationContextProvider>
@@ -121,49 +85,29 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
121
85
  function ConversationScreenContent({ route }: ConversationScreenProps) {
122
86
  const styles = useStyles()
123
87
  const navigation = useNavigation()
124
- const {
125
- conversation_id: conversationId,
126
- editing_message_id: editingMessageId,
127
- reply_root_id: replyRootId,
128
- reply_root_author_name: replyRootAuthorName,
129
- } = route.params
88
+ const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =
89
+ route.params
130
90
  const { data: conversation } = useConversation(route.params)
131
- const {
132
- messages,
133
- fetchOlderMessages,
134
- fetchNewerMessages,
135
- hasMoreNewerMessages,
136
- isFetchingNewerMessages,
137
- cancelFetchNewerMessages,
138
- } = useConversationMessages({ conversation_id: conversationId, reply_root_id: replyRootId })
139
-
140
- const { jumpToUnreadActive } = useJumpToUnreadGates()
141
- const { initialMessageId } = useConversationContext()
142
-
143
- useConversationJoltEvents({ conversationId })
144
- 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 })
145
97
  useEnsureConversationsRouteExists()
146
98
  useMarkLatestMessageRead({ conversation, messages })
147
- const { onMessageSeen } = useTrackHighestSeenMessage()
148
-
149
- const items = useMemo(
150
- () =>
151
- groupMessages({
152
- ms: messages,
153
- inReplyScreen: !!replyRootId,
154
- jumpToUnreadActive,
155
- initialMessageId,
156
- }),
157
- [messages, replyRootId, jumpToUnreadActive, initialMessageId]
158
- )
159
- const noMessages = items.length === 0
99
+ const messagesWithSeparators = groupMessages({
100
+ ms: messages,
101
+ inReplyScreen: !!reply_root_id,
102
+ })
103
+ const noMessages = messagesWithSeparators.length === 0
160
104
 
161
105
  const { repliesDisabled, memberAbility, badges, title } = conversation
162
106
  const canReply = memberAbility?.canReply
163
107
  const showLeaderDisabledReplyBanner = canReply && repliesDisabled
164
108
  const canDeleteNonAuthoredMessages = memberAbility?.canDeleteNonAuthoredMessages ?? false
165
- const currentlyEditingMessage = messages.find(m => String(m.id) === String(editingMessageId))
166
- 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]
167
111
  const replyHeaderTitle = replyRootAuthorFirstName
168
112
  ? `Reply to ${replyRootAuthorFirstName}`
169
113
  : 'Reply'
@@ -171,96 +115,21 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
171
115
  const muted = conversation.conversationMembership?.muted ?? conversation.muted
172
116
 
173
117
  const listRef = useRef<FlatList>(null)
174
- const [dividerScrolledPast, setDividerScrolledPast] = useState(false)
175
-
176
- const observers = useMemo<ViewabilityObserver<EnrichedMessage>[]>(
177
- () => [
178
- reportViewableMessages(onMessageSeen),
179
- detectDividerExitTowardNewer({
180
- dividerKey: UNREAD_DIVIDER_KEY,
181
- initialMessageId,
182
- onExited: () => setDividerScrolledPast(true),
183
- }),
184
- ],
185
- [onMessageSeen, initialMessageId]
186
- )
118
+ const [showJumpToBottomButton, setShowJumpToBottomButton] = useState(false)
187
119
 
188
- const { viewabilityConfigCallbackPairs, onScrollBeginDrag: viewabilityOnScrollBeginDrag } =
189
- useFlatListViewability({ observers })
190
- const {
191
- onContentSizeChange,
192
- onScrollToIndexFailed,
193
- onScrollBeginDrag: anchorOnScrollBeginDrag,
194
- } = useJumpToUnreadAnchor({ listRef, items })
195
- const onScrollBeginDrag = useCallback(() => {
196
- viewabilityOnScrollBeginDrag()
197
- anchorOnScrollBeginDrag()
198
- }, [viewabilityOnScrollBeginDrag, anchorOnScrollBeginDrag])
199
- const { onScroll, showJumpToBottomButton } = useScrollTracking({
200
- hasMoreNewerMessages,
201
- isFetchingNewerMessages,
202
- fetchNewerMessages,
203
- cancelFetchNewerMessages,
204
- })
205
- const { handleJumpToBottom, isJumpingToBottom } = useJumpToBottomAction({ listRef })
206
-
207
- const listHeader = useMemo(
208
- () => (
209
- <View>
210
- {isFetchingNewerMessages && (
211
- <Animated.View
212
- entering={FadeIn.duration(750)}
213
- exiting={FadeOut.duration(750)}
214
- style={styles.loadingFooter}
215
- accessibilityRole="progressbar"
216
- accessibilityLabel="Loading more messages"
217
- >
218
- <ActivityIndicator />
219
- </Animated.View>
220
- )}
221
- <View style={styles.listHeader} />
222
- </View>
223
- ),
224
- [isFetchingNewerMessages, styles.loadingFooter, styles.listHeader]
225
- )
120
+ const trackScroll = (event: any) => {
121
+ const offsetY = event.nativeEvent.contentOffset.y
122
+ setShowJumpToBottomButton(offsetY > 200)
123
+ }
226
124
 
227
- const renderItem = useCallback(
228
- ({ item }: { item: EnrichedMessage }) => {
229
- if (item.type === 'DateSeparator') return <InlineDateSeparator {...item} />
230
- if (item.type === 'UnreadDivider') return <UnreadDivider scrolledPast={dividerScrolledPast} />
231
- if (item.type === 'ReplyShadowMessage') {
232
- return (
233
- <ReplyShadowMessage
234
- {...item}
235
- conversation_id={conversationId}
236
- inReplyScreen={!!replyRootId}
237
- />
238
- )
239
- }
240
- if (isSystemMessage(item)) {
241
- return <SystemMessage message={item} conversationId={conversationId} />
242
- }
243
- return (
244
- <Message
245
- {...item}
246
- canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages}
247
- conversation_id={conversationId}
248
- latestReadMessageSortKey={conversation?.latestReadMessageSortKey}
249
- inReplyScreen={!!replyRootId}
250
- />
251
- )
252
- },
253
- [
254
- dividerScrolledPast,
255
- conversationId,
256
- replyRootId,
257
- canDeleteNonAuthoredMessages,
258
- conversation?.latestReadMessageSortKey,
259
- ]
260
- )
125
+ const handleReturnToBottom = useCallback(() => {
126
+ listRef.current?.scrollToOffset({
127
+ offset: 0,
128
+ })
129
+ }, [])
261
130
 
262
131
  useEffect(() => {
263
- if (replyRootId) {
132
+ if (reply_root_id) {
264
133
  navigation.setParams({
265
134
  title: replyHeaderTitle,
266
135
  })
@@ -272,7 +141,7 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
272
141
  muted,
273
142
  })
274
143
  }
275
- }, [navigation, title, badges, conversation?.deleted, replyRootId, replyHeaderTitle, muted])
144
+ }, [navigation, title, badges, conversation?.deleted, reply_root_id, replyHeaderTitle, muted])
276
145
 
277
146
  if (!conversation || conversation.deleted) {
278
147
  return (
@@ -299,31 +168,55 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
299
168
  {noMessages ? (
300
169
  <EmptyConversationBlankState />
301
170
  ) : (
302
- <MessageList
303
- listRef={listRef}
304
- data={items}
305
- onScroll={onScroll}
306
- onScrollBeginDrag={onScrollBeginDrag}
307
- viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs}
308
- onContentSizeChange={onContentSizeChange}
309
- onScrollToIndexFailed={onScrollToIndexFailed}
310
- renderItem={renderItem}
311
- onEndReached={() => fetchOlderMessages()}
312
- ListHeaderComponent={listHeader}
171
+ <FlatList
172
+ inverted
173
+ ref={listRef}
174
+ contentContainerStyle={styles.listContainer}
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} />}
313
210
  />
314
211
  )}
315
- <JumpToBottomButton
316
- onPress={handleJumpToBottom}
317
- visible={showJumpToBottomButton}
318
- loading={isJumpingToBottom}
319
- />
212
+ <JumpToBottomButton onPress={handleReturnToBottom} visible={showJumpToBottomButton} />
320
213
  {!noMessages && <TypingIndicator />}
321
214
  {showLeaderDisabledReplyBanner && <LeaderMessagesDisabledBanner />}
322
215
  <ConversationBottomBar
323
216
  conversation={conversation}
324
217
  canReply={canReply}
325
218
  replyRootAuthorFirstName={replyRootAuthorFirstName}
326
- replyRootId={replyRootId}
219
+ replyRootId={reply_root_id}
327
220
  currentlyEditingMessage={currentlyEditingMessage}
328
221
  />
329
222
  </KeyboardView>
@@ -493,14 +386,13 @@ const useStyles = () => {
493
386
  backgroundColor: navigationTheme.colors.card,
494
387
  paddingBottom: bottom,
495
388
  },
389
+ listContainer: {
390
+ paddingVertical: 12,
391
+ },
496
392
  listHeader: {
497
393
  // Just whitespace to provide space where the typing indicator can be
498
394
  height: 16,
499
395
  },
500
- loadingFooter: {
501
- paddingVertical: 12,
502
- alignItems: 'center',
503
- },
504
396
  })
505
397
  }
506
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} />