@planningcenter/chat-react-native 4.4.0-rc.1 → 4.4.0-rc.2
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/hooks/use_product_analytics.d.ts +9 -0
- package/build/hooks/use_product_analytics.d.ts.map +1 -1
- package/build/hooks/use_product_analytics.js +4 -2
- package/build/hooks/use_product_analytics.js.map +1 -1
- package/build/hooks/use_search_enabled.d.ts +2 -0
- package/build/hooks/use_search_enabled.d.ts.map +1 -0
- package/build/hooks/use_search_enabled.js +18 -0
- package/build/hooks/use_search_enabled.js.map +1 -0
- package/build/screens/conversations/components/search_list_header_component.js +3 -3
- package/build/screens/conversations/components/search_list_header_component.js.map +1 -1
- package/build/screens/conversations/conversations_screen.d.ts.map +1 -1
- package/build/screens/conversations/conversations_screen.js +2 -5
- package/build/screens/conversations/conversations_screen.js.map +1 -1
- package/build/screens/search/components/conversation_results.d.ts +1 -1
- package/build/screens/search/components/conversation_results.d.ts.map +1 -1
- package/build/screens/search/components/conversation_results.js +2 -2
- package/build/screens/search/components/conversation_results.js.map +1 -1
- package/build/screens/search/components/search_results_list.d.ts +7 -2
- package/build/screens/search/components/search_results_list.d.ts.map +1 -1
- package/build/screens/search/components/search_results_list.js +135 -105
- package/build/screens/search/components/search_results_list.js.map +1 -1
- package/build/screens/search/search_screen.d.ts.map +1 -1
- package/build/screens/search/search_screen.js +69 -31
- package/build/screens/search/search_screen.js.map +1 -1
- package/package.json +2 -2
- package/src/hooks/use_product_analytics.ts +3 -1
- package/src/hooks/use_search_enabled.ts +9 -0
- package/src/screens/conversations/__tests__/conversations_screen.test.tsx +71 -0
- package/src/screens/conversations/components/search_list_header_component.tsx +1 -1
- package/src/screens/conversations/conversations_screen.tsx +2 -3
- package/src/screens/search/__tests__/search_screen.test.tsx +56 -2
- package/src/screens/search/components/__tests__/conversation_results.test.tsx +2 -2
- package/src/screens/search/components/__tests__/search_results_list.test.tsx +47 -2
- package/src/screens/search/components/conversation_results.tsx +7 -3
- package/src/screens/search/components/search_results_list.tsx +25 -4
- package/src/screens/search/search_screen.tsx +24 -4
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
+
import { render } from '@testing-library/react-native'
|
|
3
|
+
import React, { Suspense } from 'react'
|
|
4
|
+
import { buildTestQueryClient } from '../../../__utils__/query_client'
|
|
5
|
+
import { ConversationsScreen } from '../conversations_screen'
|
|
6
|
+
|
|
7
|
+
const mockGet = jest.fn()
|
|
8
|
+
jest.mock('../../../hooks/use_api_client', () => ({
|
|
9
|
+
useApiClient: () => ({ chat: { get: mockGet } }),
|
|
10
|
+
}))
|
|
11
|
+
jest.mock('../../../hooks/use_product_analytics', () => ({
|
|
12
|
+
...jest.requireActual('../../../hooks/use_product_analytics'),
|
|
13
|
+
usePublishProductAnalyticsEvent: () => {},
|
|
14
|
+
}))
|
|
15
|
+
jest.mock('../../../contexts/conversations_context', () => ({
|
|
16
|
+
ConversationsContextProvider: ({ children }: { children: React.ReactNode }) => children,
|
|
17
|
+
}))
|
|
18
|
+
jest.mock('../../../components', () => ({
|
|
19
|
+
Conversations: ({ ListHeaderComponent }: { ListHeaderComponent: React.ComponentType }) => (
|
|
20
|
+
<ListHeaderComponent />
|
|
21
|
+
),
|
|
22
|
+
}))
|
|
23
|
+
jest.mock('../components/list_header_component', () => {
|
|
24
|
+
const { Text } = require('react-native')
|
|
25
|
+
return { ListHeaderComponent: () => <Text>Default header</Text> }
|
|
26
|
+
})
|
|
27
|
+
jest.mock('../components/search_list_header_component', () => {
|
|
28
|
+
const { Text } = require('react-native')
|
|
29
|
+
return { SearchListHeaderComponent: () => <Text>Search header</Text> }
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const renderWithFeatures = (enabledFeatures: string[]) => {
|
|
33
|
+
mockGet.mockResolvedValue({
|
|
34
|
+
data: enabledFeatures.map((name, index) => ({
|
|
35
|
+
type: 'Feature',
|
|
36
|
+
id: String(index + 1),
|
|
37
|
+
name,
|
|
38
|
+
enabled: true,
|
|
39
|
+
})),
|
|
40
|
+
links: {},
|
|
41
|
+
meta: { count: enabledFeatures.length, totalCount: enabledFeatures.length },
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return render(
|
|
45
|
+
<QueryClientProvider client={buildTestQueryClient()}>
|
|
46
|
+
<Suspense fallback={null}>
|
|
47
|
+
<ConversationsScreen route={{ params: {} }} />
|
|
48
|
+
</Suspense>
|
|
49
|
+
</QueryClientProvider>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('ConversationsScreen header gating', () => {
|
|
54
|
+
it('shows the search header with only the QA flag on', async () => {
|
|
55
|
+
const view = renderWithFeatures(['QA_MOBILE_search'])
|
|
56
|
+
|
|
57
|
+
expect(await view.findByText('Search header')).toBeTruthy()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('shows the search header with only the rollout flag on', async () => {
|
|
61
|
+
const view = renderWithFeatures(['ROLLOUT_MOBILE_search'])
|
|
62
|
+
|
|
63
|
+
expect(await view.findByText('Search header')).toBeTruthy()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('shows the default header with neither flag on', async () => {
|
|
67
|
+
const view = renderWithFeatures(['ROLLOUT_WEB_search'])
|
|
68
|
+
|
|
69
|
+
expect(await view.findByText('Default header')).toBeTruthy()
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -264,7 +264,7 @@ const useStyles = ({
|
|
|
264
264
|
return StyleSheet.create({
|
|
265
265
|
listHeader: {
|
|
266
266
|
borderBottomWidth: 1,
|
|
267
|
-
borderBottomColor: theme.colors.
|
|
267
|
+
borderBottomColor: theme.colors.fillColorNeutral050Base,
|
|
268
268
|
paddingBottom: 16,
|
|
269
269
|
gap: 16,
|
|
270
270
|
},
|
|
@@ -3,8 +3,8 @@ import React from 'react'
|
|
|
3
3
|
import { StyleSheet, View } from 'react-native'
|
|
4
4
|
import { Conversations } from '../../components'
|
|
5
5
|
import { ConversationsContextProvider } from '../../contexts/conversations_context'
|
|
6
|
-
import { availableFeatures, useFeatures } from '../../hooks/use_features'
|
|
7
6
|
import { analyticsEvents, usePublishProductAnalyticsEvent } from '../../hooks/use_product_analytics'
|
|
7
|
+
import { useSearchEnabled } from '../../hooks/use_search_enabled'
|
|
8
8
|
import { AppName } from '../../types/resources/app_name'
|
|
9
9
|
import { GraphId } from '../../types/resources/group_resource'
|
|
10
10
|
import { ListHeaderComponent } from './components/list_header_component'
|
|
@@ -19,8 +19,7 @@ export type ConversationsScreenProps = StaticScreenProps<{
|
|
|
19
19
|
|
|
20
20
|
export function ConversationsScreen({ route }: ConversationsScreenProps) {
|
|
21
21
|
const styles = useStyles()
|
|
22
|
-
const
|
|
23
|
-
const searchEnabled = featureEnabled(availableFeatures.search)
|
|
22
|
+
const searchEnabled = useSearchEnabled()
|
|
24
23
|
const Header = searchEnabled ? SearchListHeaderComponent : ListHeaderComponent
|
|
25
24
|
|
|
26
25
|
usePublishProductAnalyticsEvent(analyticsEvents.conversation_index_opened)
|
|
@@ -18,6 +18,11 @@ const mockUseSearchResults = jest.fn<ReturnType<typeof useSearchResults>, [strin
|
|
|
18
18
|
jest.mock('../../../hooks/use_search_results', () => ({
|
|
19
19
|
useSearchResults: (term: string) => mockUseSearchResults(term),
|
|
20
20
|
}))
|
|
21
|
+
const mockPublishEvent = jest.fn()
|
|
22
|
+
jest.mock('../../../hooks/use_product_analytics', () => ({
|
|
23
|
+
...jest.requireActual('../../../hooks/use_product_analytics'),
|
|
24
|
+
useProductAnalytics: () => ({ publishEvent: mockPublishEvent }),
|
|
25
|
+
}))
|
|
21
26
|
jest.mock('../../../components/display/keyboard_view', () => ({
|
|
22
27
|
...jest.requireActual('../../../components/display/keyboard_view'),
|
|
23
28
|
KeyboardView: ({ children }: { children: React.ReactNode }) => children,
|
|
@@ -47,9 +52,17 @@ const messageResult: MessageSearchResultResource = {
|
|
|
47
52
|
},
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
const secondMessageResult: MessageSearchResultResource = {
|
|
56
|
+
...messageResult,
|
|
57
|
+
id: '01JSECONDULID',
|
|
58
|
+
title: 'Youth',
|
|
59
|
+
url: '/conversations/9/messages/01JSECONDULID',
|
|
60
|
+
extra: { ...messageResult.extra, conversationId: 9, authorName: 'Bob' },
|
|
61
|
+
}
|
|
62
|
+
|
|
50
63
|
const renderWithResults = () => {
|
|
51
64
|
mockUseSearchResults.mockReturnValue({
|
|
52
|
-
results: [conversationResult, messageResult],
|
|
65
|
+
results: [conversationResult, messageResult, secondMessageResult],
|
|
53
66
|
failedTypes: [],
|
|
54
67
|
isPending: false,
|
|
55
68
|
isError: false,
|
|
@@ -59,7 +72,9 @@ const renderWithResults = () => {
|
|
|
59
72
|
isFetchingNextPage: false,
|
|
60
73
|
})
|
|
61
74
|
const view = render(<SearchScreen />)
|
|
62
|
-
|
|
75
|
+
const input = view.getByLabelText(inputLabel)
|
|
76
|
+
fireEvent.changeText(input, 'bud')
|
|
77
|
+
fireEvent.changeText(input, 'budget')
|
|
63
78
|
act(() => jest.advanceTimersByTime(200))
|
|
64
79
|
return view
|
|
65
80
|
}
|
|
@@ -67,6 +82,7 @@ const renderWithResults = () => {
|
|
|
67
82
|
beforeEach(() => {
|
|
68
83
|
jest.useFakeTimers()
|
|
69
84
|
mockNavigate.mockReset()
|
|
85
|
+
mockPublishEvent.mockReset()
|
|
70
86
|
mockUseSearchResults.mockReset().mockReturnValue({
|
|
71
87
|
results: [],
|
|
72
88
|
failedTypes: [],
|
|
@@ -137,4 +153,42 @@ describe('SearchScreen', () => {
|
|
|
137
153
|
title: 'Finance',
|
|
138
154
|
})
|
|
139
155
|
})
|
|
156
|
+
|
|
157
|
+
it('publishes one search event per settled term with result counts, never the term', () => {
|
|
158
|
+
renderWithResults()
|
|
159
|
+
|
|
160
|
+
expect(mockPublishEvent).toHaveBeenCalledTimes(1)
|
|
161
|
+
expect(mockPublishEvent).toHaveBeenCalledWith('chat.mobile.search.performed', {
|
|
162
|
+
term_length: 6,
|
|
163
|
+
conversation_result_count: 1,
|
|
164
|
+
message_result_count: 2,
|
|
165
|
+
})
|
|
166
|
+
expect(JSON.stringify(mockPublishEvent.mock.calls)).not.toContain('budget')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('does not publish a retyped term whose results are already cached', () => {
|
|
170
|
+
const view = renderWithResults()
|
|
171
|
+
const input = view.getByLabelText(inputLabel)
|
|
172
|
+
|
|
173
|
+
fireEvent.changeText(input, 'budge')
|
|
174
|
+
act(() => jest.advanceTimersByTime(200))
|
|
175
|
+
fireEvent.changeText(input, 'budget')
|
|
176
|
+
act(() => jest.advanceTimersByTime(200))
|
|
177
|
+
|
|
178
|
+
const performed = mockPublishEvent.mock.calls.filter(
|
|
179
|
+
([name]) => name === 'chat.mobile.search.performed'
|
|
180
|
+
)
|
|
181
|
+
expect(performed.map(([, meta]) => meta.term_length)).toEqual([6, 5])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('publishes the opened result type and its position within its section', () => {
|
|
185
|
+
const view = renderWithResults()
|
|
186
|
+
|
|
187
|
+
fireEvent.press(view.getByLabelText(/^Youth, Bob: budget/))
|
|
188
|
+
|
|
189
|
+
expect(mockPublishEvent).toHaveBeenCalledWith('chat.mobile.search.result.opened', {
|
|
190
|
+
result_type: 'message',
|
|
191
|
+
position: 1,
|
|
192
|
+
})
|
|
193
|
+
})
|
|
140
194
|
})
|
|
@@ -43,13 +43,13 @@ describe('ConversationResults', () => {
|
|
|
43
43
|
expect(view.getByText('View less')).toBeTruthy()
|
|
44
44
|
})
|
|
45
45
|
|
|
46
|
-
it('passes the pressed result to onSelect', () => {
|
|
46
|
+
it('passes the pressed result and its position to onSelect', () => {
|
|
47
47
|
const onSelect = jest.fn()
|
|
48
48
|
const results = buildResults(2)
|
|
49
49
|
const view = render(<ConversationResults results={results} onSelect={onSelect} />)
|
|
50
50
|
|
|
51
51
|
fireEvent.press(view.getByText('Conversation 2'))
|
|
52
52
|
|
|
53
|
-
expect(onSelect).toHaveBeenCalledWith(results[1])
|
|
53
|
+
expect(onSelect).toHaveBeenCalledWith(results[1], 1)
|
|
54
54
|
})
|
|
55
55
|
})
|
|
@@ -49,9 +49,15 @@ const messageResult = (id: string): MessageSearchResultResource => ({
|
|
|
49
49
|
},
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
const renderList = (state: Partial<SearchState
|
|
52
|
+
const renderList = (state: Partial<SearchState>, onSettled = jest.fn()) => {
|
|
53
53
|
mockUseSearchResults.mockReturnValue(searchState(state))
|
|
54
|
-
|
|
54
|
+
const list = () => <SearchResultsList term="potluck" onSelect={jest.fn()} onSettled={onSettled} />
|
|
55
|
+
const view = render(list())
|
|
56
|
+
const rerenderWith = (next: Partial<SearchState>) => {
|
|
57
|
+
mockUseSearchResults.mockReturnValue(searchState(next))
|
|
58
|
+
view.rerender(list())
|
|
59
|
+
}
|
|
60
|
+
return { ...view, rerenderWith }
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
describe('SearchResultsList', () => {
|
|
@@ -119,6 +125,45 @@ describe('SearchResultsList', () => {
|
|
|
119
125
|
expect(loadMore).not.toHaveBeenCalled()
|
|
120
126
|
})
|
|
121
127
|
|
|
128
|
+
it('passes a message result and its position within the section to onSelect', () => {
|
|
129
|
+
const onSelect = jest.fn()
|
|
130
|
+
const results = [conversationResult('1'), messageResult('1'), messageResult('2')]
|
|
131
|
+
mockUseSearchResults.mockReturnValue(searchState({ results }))
|
|
132
|
+
const view = render(
|
|
133
|
+
<SearchResultsList term="potluck" onSelect={onSelect} onSettled={jest.fn()} />
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
fireEvent.press(view.getByLabelText(/^Message 2/))
|
|
137
|
+
|
|
138
|
+
expect(onSelect).toHaveBeenCalledWith(results[2], 1)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('reports the settled term and counts once, not while pending or after paging', () => {
|
|
142
|
+
const onSettled = jest.fn()
|
|
143
|
+
const view = renderList({ isPending: true }, onSettled)
|
|
144
|
+
expect(onSettled).not.toHaveBeenCalled()
|
|
145
|
+
|
|
146
|
+
const results = [conversationResult('1'), messageResult('1'), messageResult('2')]
|
|
147
|
+
view.rerenderWith({ results })
|
|
148
|
+
|
|
149
|
+
expect(onSettled).toHaveBeenCalledTimes(1)
|
|
150
|
+
expect(onSettled).toHaveBeenCalledWith('potluck', {
|
|
151
|
+
conversationResultCount: 1,
|
|
152
|
+
messageResultCount: 2,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
view.rerenderWith({ results: [...results, messageResult('3')] })
|
|
156
|
+
|
|
157
|
+
expect(onSettled).toHaveBeenCalledTimes(1)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('does not report a failed search', () => {
|
|
161
|
+
const onSettled = jest.fn()
|
|
162
|
+
renderList({ isError: true }, onSettled)
|
|
163
|
+
|
|
164
|
+
expect(onSettled).not.toHaveBeenCalled()
|
|
165
|
+
})
|
|
166
|
+
|
|
122
167
|
it('offers a retry after a paging failure', () => {
|
|
123
168
|
const loadMore = jest.fn()
|
|
124
169
|
const view = renderList({
|
|
@@ -8,7 +8,7 @@ const COLLAPSED_COUNT = 5
|
|
|
8
8
|
|
|
9
9
|
interface ConversationResultsProps {
|
|
10
10
|
results: ConversationSearchResultResource[]
|
|
11
|
-
onSelect: (result: ConversationSearchResultResource) => void
|
|
11
|
+
onSelect: (result: ConversationSearchResultResource, position: number) => void
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function ConversationResults({ results, onSelect }: ConversationResultsProps) {
|
|
@@ -31,8 +31,12 @@ export function ConversationResults({ results, onSelect }: ConversationResultsPr
|
|
|
31
31
|
|
|
32
32
|
return (
|
|
33
33
|
<ResultsSection title="Conversations" footer={footer}>
|
|
34
|
-
{visible.map(result => (
|
|
35
|
-
<ConversationResultRow
|
|
34
|
+
{visible.map((result, index) => (
|
|
35
|
+
<ConversationResultRow
|
|
36
|
+
key={result.id}
|
|
37
|
+
result={result}
|
|
38
|
+
onPress={() => onSelect(result, index)}
|
|
39
|
+
/>
|
|
36
40
|
))}
|
|
37
41
|
</ResultsSection>
|
|
38
42
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { useEffect, useRef } from 'react'
|
|
2
2
|
import { FlatList, StyleSheet, View } from 'react-native'
|
|
3
3
|
import { Banner, Spinner, Text, TextButton } from '../../../components'
|
|
4
4
|
import { useSearchResults } from '../../../hooks/use_search_results'
|
|
@@ -14,12 +14,18 @@ import { NoResultsBlankState } from './search_blank_states'
|
|
|
14
14
|
|
|
15
15
|
const SEARCH_ERROR_MESSAGE = "Couldn't search right now. Try again."
|
|
16
16
|
|
|
17
|
+
export interface SearchResultCounts {
|
|
18
|
+
conversationResultCount: number
|
|
19
|
+
messageResultCount: number
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
interface SearchResultsListProps {
|
|
18
23
|
term: string
|
|
19
|
-
onSelect: (result: SearchResultResource) => void
|
|
24
|
+
onSelect: (result: SearchResultResource, position: number) => void
|
|
25
|
+
onSettled: (term: string, counts: SearchResultCounts) => void
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
export function SearchResultsList({ term, onSelect }: SearchResultsListProps) {
|
|
28
|
+
export function SearchResultsList({ term, onSelect, onSettled }: SearchResultsListProps) {
|
|
23
29
|
const {
|
|
24
30
|
results,
|
|
25
31
|
failedTypes,
|
|
@@ -32,6 +38,19 @@ export function SearchResultsList({ term, onSelect }: SearchResultsListProps) {
|
|
|
32
38
|
} = useSearchResults(term)
|
|
33
39
|
const conversationResults = results.filter(isConversationResult)
|
|
34
40
|
const messageResults = results.filter(isMessageResult)
|
|
41
|
+
const settled = !isPending && !isError
|
|
42
|
+
|
|
43
|
+
const settledTermRef = useRef<string | undefined>(undefined)
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!settled || settledTermRef.current === term) return
|
|
47
|
+
|
|
48
|
+
settledTermRef.current = term
|
|
49
|
+
onSettled(term, {
|
|
50
|
+
conversationResultCount: conversationResults.length,
|
|
51
|
+
messageResultCount: messageResults.length,
|
|
52
|
+
})
|
|
53
|
+
}, [settled, term, conversationResults.length, messageResults.length, onSettled])
|
|
35
54
|
|
|
36
55
|
return (
|
|
37
56
|
<FlatList
|
|
@@ -40,7 +59,9 @@ export function SearchResultsList({ term, onSelect }: SearchResultsListProps) {
|
|
|
40
59
|
keyboardShouldPersistTaps="handled"
|
|
41
60
|
keyboardDismissMode="on-drag"
|
|
42
61
|
contentContainerStyle={styles.content}
|
|
43
|
-
renderItem={({ item }) =>
|
|
62
|
+
renderItem={({ item, index }) => (
|
|
63
|
+
<MessageResultRow result={item} onPress={() => onSelect(item, index)} />
|
|
64
|
+
)}
|
|
44
65
|
ListHeaderComponent={
|
|
45
66
|
<>
|
|
46
67
|
{conversationResults.length > 0 && (
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
NativeStackNavigationOptions,
|
|
4
4
|
NativeStackScreenProps,
|
|
5
5
|
} from '@react-navigation/native-stack'
|
|
6
|
-
import React, { useState } from 'react'
|
|
6
|
+
import React, { useRef, useState } from 'react'
|
|
7
7
|
import { Keyboard, StyleSheet, View } from 'react-native'
|
|
8
8
|
import { KeyboardView } from '../../components'
|
|
9
9
|
import {
|
|
@@ -11,10 +11,11 @@ import {
|
|
|
11
11
|
withMinimalBack,
|
|
12
12
|
} from '../../components/display/platform_modal_header_buttons'
|
|
13
13
|
import { useDebouncedSearchTerm } from '../../hooks/use_debounced_search_term'
|
|
14
|
+
import { analyticsEvents, useProductAnalytics } from '../../hooks/use_product_analytics'
|
|
14
15
|
import type { SearchResultResource } from '../../types/resources/search_result'
|
|
15
16
|
import { SearchPromptBlankState } from './components/search_blank_states'
|
|
16
17
|
import { SearchField } from './components/search_field'
|
|
17
|
-
import { SearchResultsList } from './components/search_results_list'
|
|
18
|
+
import { SearchResultsList, type SearchResultCounts } from './components/search_results_list'
|
|
18
19
|
|
|
19
20
|
export const SearchScreenOptions = ({
|
|
20
21
|
navigation,
|
|
@@ -28,9 +29,23 @@ export function SearchScreen() {
|
|
|
28
29
|
const navigation = useNavigation()
|
|
29
30
|
const [term, setTerm] = useState('')
|
|
30
31
|
const debouncedTerm = useDebouncedSearchTerm(term)
|
|
32
|
+
const { publishEvent } = useProductAnalytics()
|
|
33
|
+
const publishedTerms = useRef(new Set<string>())
|
|
31
34
|
|
|
32
|
-
const
|
|
35
|
+
const publishSearch = (settledTerm: string, counts: SearchResultCounts) => {
|
|
36
|
+
if (publishedTerms.current.has(settledTerm)) return
|
|
37
|
+
|
|
38
|
+
publishedTerms.current.add(settledTerm)
|
|
39
|
+
publishEvent(analyticsEvents.search_performed, {
|
|
40
|
+
term_length: settledTerm.length,
|
|
41
|
+
conversation_result_count: counts.conversationResultCount,
|
|
42
|
+
message_result_count: counts.messageResultCount,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const openResult = (result: SearchResultResource, position: number) => {
|
|
33
47
|
Keyboard.dismiss()
|
|
48
|
+
publishEvent(analyticsEvents.search_result_opened, { result_type: result.type, position })
|
|
34
49
|
|
|
35
50
|
if (result.type === 'conversation') {
|
|
36
51
|
navigation.navigate('Conversation', {
|
|
@@ -51,7 +66,12 @@ export function SearchScreen() {
|
|
|
51
66
|
<View style={styles.container} testID="search-screen">
|
|
52
67
|
<SearchField term={term} onChange={setTerm} />
|
|
53
68
|
{debouncedTerm.length > 0 ? (
|
|
54
|
-
<SearchResultsList
|
|
69
|
+
<SearchResultsList
|
|
70
|
+
key={debouncedTerm}
|
|
71
|
+
term={debouncedTerm}
|
|
72
|
+
onSelect={openResult}
|
|
73
|
+
onSettled={publishSearch}
|
|
74
|
+
/>
|
|
55
75
|
) : (
|
|
56
76
|
<SearchPromptBlankState />
|
|
57
77
|
)}
|