@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.
- package/build/components/conversation/jump_to_bottom_button.d.ts +1 -2
- package/build/components/conversation/jump_to_bottom_button.d.ts.map +1 -1
- package/build/components/conversation/jump_to_bottom_button.js +7 -39
- package/build/components/conversation/jump_to_bottom_button.js.map +1 -1
- package/build/components/conversation/reply_shadow_message.d.ts +2 -1
- package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
- package/build/components/conversation/reply_shadow_message.js.map +1 -1
- package/build/contexts/conversation_context.d.ts +1 -8
- package/build/contexts/conversation_context.d.ts.map +1 -1
- package/build/contexts/conversation_context.js +3 -21
- package/build/contexts/conversation_context.js.map +1 -1
- package/build/hooks/groups/use_group_chat_conversation_payload.d.ts.map +1 -1
- package/build/hooks/groups/use_group_chat_conversation_payload.js +1 -0
- package/build/hooks/groups/use_group_chat_conversation_payload.js.map +1 -1
- package/build/hooks/use_conversation_messages.d.ts +6 -15
- package/build/hooks/use_conversation_messages.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages.js +9 -62
- package/build/hooks/use_conversation_messages.js.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.js +4 -4
- package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
- package/build/hooks/use_conversations_actions.d.ts +0 -5
- package/build/hooks/use_conversations_actions.d.ts.map +1 -1
- package/build/hooks/use_conversations_actions.js +0 -12
- package/build/hooks/use_conversations_actions.js.map +1 -1
- package/build/hooks/use_features.d.ts +0 -1
- package/build/hooks/use_features.d.ts.map +1 -1
- package/build/hooks/use_features.js +0 -1
- package/build/hooks/use_features.js.map +1 -1
- package/build/hooks/use_mark_latest_message_read.d.ts +1 -1
- package/build/hooks/use_mark_latest_message_read.d.ts.map +1 -1
- package/build/hooks/use_mark_latest_message_read.js +1 -17
- package/build/hooks/use_mark_latest_message_read.js.map +1 -1
- package/build/hooks/use_suspense_api.d.ts +0 -1
- package/build/hooks/use_suspense_api.d.ts.map +1 -1
- package/build/hooks/use_suspense_api.js +1 -1
- package/build/hooks/use_suspense_api.js.map +1 -1
- package/build/screens/conversation_filter_recipients/components/header_row.d.ts.map +1 -1
- package/build/screens/conversation_filter_recipients/components/header_row.js +3 -2
- package/build/screens/conversation_filter_recipients/components/header_row.js.map +1 -1
- package/build/screens/conversation_filter_recipients/hooks/use_flattened_array_of_service_types_with_teams.d.ts.map +1 -1
- package/build/screens/conversation_filter_recipients/hooks/use_flattened_array_of_service_types_with_teams.js +47 -18
- package/build/screens/conversation_filter_recipients/hooks/use_flattened_array_of_service_types_with_teams.js.map +1 -1
- package/build/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.d.ts +2 -1
- package/build/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.d.ts.map +1 -1
- package/build/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.js +23 -26
- package/build/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.js.map +1 -1
- package/build/screens/conversation_filter_recipients/types.d.ts +1 -1
- package/build/screens/conversation_filter_recipients/types.d.ts.map +1 -1
- package/build/screens/conversation_filter_recipients/types.js.map +1 -1
- package/build/screens/conversation_screen.d.ts +0 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +48 -95
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/screens/conversation_select_recipients/components/recipient_link_row.d.ts +1 -1
- package/build/screens/conversation_select_recipients/components/recipient_link_row.d.ts.map +1 -1
- package/build/screens/conversation_select_recipients/components/recipient_link_row.js +3 -3
- package/build/screens/conversation_select_recipients/components/recipient_link_row.js.map +1 -1
- package/build/screens/conversation_select_recipients/components/team_recipient_row.d.ts.map +1 -1
- package/build/screens/conversation_select_recipients/components/team_recipient_row.js +1 -1
- package/build/screens/conversation_select_recipients/components/team_recipient_row.js.map +1 -1
- package/build/utils/cache/messages_cache.d.ts +0 -1
- package/build/utils/cache/messages_cache.d.ts.map +1 -1
- package/build/utils/cache/messages_cache.js +0 -4
- package/build/utils/cache/messages_cache.js.map +1 -1
- package/build/utils/group_messages.d.ts +2 -9
- package/build/utils/group_messages.d.ts.map +1 -1
- package/build/utils/group_messages.js +1 -20
- package/build/utils/group_messages.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/hooks/use_group_chat_conversation_payload.test.tsx +50 -0
- package/src/components/conversation/jump_to_bottom_button.tsx +8 -57
- package/src/components/conversation/reply_shadow_message.tsx +1 -1
- package/src/contexts/conversation_context.tsx +2 -30
- package/src/hooks/groups/use_group_chat_conversation_payload.ts +1 -0
- package/src/hooks/use_conversation_messages.ts +20 -120
- package/src/hooks/use_conversation_messages_jolt_events.ts +3 -4
- package/src/hooks/use_conversations_actions.ts +0 -15
- package/src/hooks/use_features.ts +0 -1
- package/src/hooks/use_mark_latest_message_read.ts +2 -16
- package/src/hooks/use_suspense_api.ts +1 -1
- package/src/screens/conversation_filter_recipients/components/header_row.tsx +3 -2
- package/src/screens/conversation_filter_recipients/hooks/__tests__/use_service_types_with_teams.test.ts +108 -0
- package/src/screens/conversation_filter_recipients/hooks/use_flattened_array_of_service_types_with_teams.tsx +46 -19
- package/src/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.ts +31 -29
- package/src/screens/conversation_filter_recipients/types.tsx +1 -1
- package/src/screens/conversation_screen.tsx +76 -184
- package/src/screens/conversation_select_recipients/components/recipient_link_row.tsx +6 -4
- package/src/screens/conversation_select_recipients/components/team_recipient_row.tsx +2 -1
- package/src/utils/__tests__/group_messages.test.ts +0 -71
- package/src/utils/cache/messages_cache.ts +0 -5
- package/src/utils/group_messages.ts +2 -42
- package/build/components/conversation/unread_divider.d.ts +0 -6
- package/build/components/conversation/unread_divider.d.ts.map +0 -1
- package/build/components/conversation/unread_divider.js +0 -59
- package/build/components/conversation/unread_divider.js.map +0 -1
- package/build/hooks/use_flat_list_viewability.d.ts +0 -20
- package/build/hooks/use_flat_list_viewability.d.ts.map +0 -1
- package/build/hooks/use_flat_list_viewability.js +0 -30
- package/build/hooks/use_flat_list_viewability.js.map +0 -1
- package/build/hooks/use_jump_to_bottom_action.d.ts +0 -9
- package/build/hooks/use_jump_to_bottom_action.d.ts.map +0 -1
- package/build/hooks/use_jump_to_bottom_action.js +0 -62
- package/build/hooks/use_jump_to_bottom_action.js.map +0 -1
- package/build/hooks/use_jump_to_unread_anchor.d.ts +0 -20
- package/build/hooks/use_jump_to_unread_anchor.d.ts.map +0 -1
- package/build/hooks/use_jump_to_unread_anchor.js +0 -53
- package/build/hooks/use_jump_to_unread_anchor.js.map +0 -1
- package/build/hooks/use_jump_to_unread_gates.d.ts +0 -5
- package/build/hooks/use_jump_to_unread_gates.d.ts.map +0 -1
- package/build/hooks/use_jump_to_unread_gates.js +0 -10
- package/build/hooks/use_jump_to_unread_gates.js.map +0 -1
- package/build/hooks/use_scroll_tracking.d.ts +0 -13
- package/build/hooks/use_scroll_tracking.d.ts.map +0 -1
- package/build/hooks/use_scroll_tracking.js +0 -45
- package/build/hooks/use_scroll_tracking.js.map +0 -1
- package/build/hooks/use_track_highest_seen_message.d.ts +0 -4
- package/build/hooks/use_track_highest_seen_message.d.ts.map +0 -1
- package/build/hooks/use_track_highest_seen_message.js +0 -35
- package/build/hooks/use_track_highest_seen_message.js.map +0 -1
- package/build/utils/conversation_messages.d.ts +0 -10
- package/build/utils/conversation_messages.d.ts.map +0 -1
- package/build/utils/conversation_messages.js +0 -22
- package/build/utils/conversation_messages.js.map +0 -1
- package/build/utils/highest_seen_tracker.d.ts +0 -12
- package/build/utils/highest_seen_tracker.d.ts.map +0 -1
- package/build/utils/highest_seen_tracker.js +0 -37
- package/build/utils/highest_seen_tracker.js.map +0 -1
- package/build/utils/message_viewability.d.ts +0 -24
- package/build/utils/message_viewability.d.ts.map +0 -1
- package/build/utils/message_viewability.js +0 -29
- package/build/utils/message_viewability.js.map +0 -1
- package/build/utils/unread_divider_helpers.d.ts +0 -18
- package/build/utils/unread_divider_helpers.d.ts.map +0 -1
- package/build/utils/unread_divider_helpers.js +0 -13
- package/build/utils/unread_divider_helpers.js.map +0 -1
- package/src/__tests__/hooks/use_conversation_messages.test.tsx +0 -109
- package/src/__tests__/hooks/use_mark_latest_message_read.test.tsx +0 -154
- package/src/__tests__/utils/cache/messages_cache.test.ts +0 -54
- package/src/components/conversation/unread_divider.tsx +0 -90
- package/src/hooks/use_flat_list_viewability.ts +0 -50
- package/src/hooks/use_jump_to_bottom_action.ts +0 -75
- package/src/hooks/use_jump_to_unread_anchor.ts +0 -68
- package/src/hooks/use_jump_to_unread_gates.ts +0 -10
- package/src/hooks/use_scroll_tracking.ts +0 -64
- package/src/hooks/use_track_highest_seen_message.ts +0 -43
- package/src/utils/__tests__/conversation_messages.test.ts +0 -105
- package/src/utils/__tests__/highest_seen_tracker.test.ts +0 -82
- package/src/utils/__tests__/message_viewability.test.ts +0 -168
- package/src/utils/__tests__/unread_divider_helpers.test.ts +0 -85
- package/src/utils/conversation_messages.ts +0 -37
- package/src/utils/highest_seen_tracker.ts +0 -42
- package/src/utils/message_viewability.ts +0 -49
- 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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
}
|
|
@@ -8,17 +8,14 @@ import {
|
|
|
8
8
|
useTheme as useNavigationTheme,
|
|
9
9
|
useRoute,
|
|
10
10
|
} from '@react-navigation/native'
|
|
11
|
-
import React, { useCallback, useEffect,
|
|
12
|
-
import {
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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(
|
|
166
|
-
const replyRootAuthorFirstName =
|
|
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 [
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
228
|
-
({
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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 (
|
|
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,
|
|
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
|
-
<
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
renderItem={
|
|
311
|
-
|
|
312
|
-
|
|
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={
|
|
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
|
|
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
|
-
|
|
44
|
-
{
|
|
45
|
-
|
|
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} />
|