@planningcenter/chat-react-native 4.3.1-rc.0 → 4.4.0-rc.1
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/message.d.ts +3 -1
- package/build/components/conversation/message.d.ts.map +1 -1
- package/build/components/conversation/message.js +183 -158
- package/build/components/conversation/message.js.map +1 -1
- package/build/components/conversation/message_list.d.ts.map +1 -1
- package/build/components/conversation/message_list.js +143 -88
- package/build/components/conversation/message_list.js.map +1 -1
- package/build/hooks/use_animated_message_background_color.d.ts +1 -0
- package/build/hooks/use_animated_message_background_color.d.ts.map +1 -1
- package/build/hooks/use_animated_message_background_color.js +26 -9
- package/build/hooks/use_animated_message_background_color.js.map +1 -1
- package/build/screens/search/search_screen.d.ts.map +1 -1
- package/build/screens/search/search_screen.js +48 -23
- package/build/screens/search/search_screen.js.map +1 -1
- package/package.json +2 -2
- package/src/components/conversation/__tests__/message_list.test.tsx +71 -3
- package/src/components/conversation/message.tsx +12 -1
- package/src/components/conversation/message_list.tsx +26 -1
- package/src/hooks/use_animated_message_background_color.ts +37 -12
- package/src/screens/search/__tests__/search_screen.test.tsx +76 -0
- package/src/screens/search/search_screen.tsx +22 -4
|
@@ -1,43 +1,68 @@
|
|
|
1
|
+
import { useCallback } from 'react'
|
|
1
2
|
import { Platform } from 'react-native'
|
|
2
3
|
import {
|
|
3
4
|
useSharedValue,
|
|
4
5
|
useAnimatedStyle,
|
|
6
|
+
withDelay,
|
|
7
|
+
withSequence,
|
|
5
8
|
withTiming,
|
|
6
9
|
interpolateColor,
|
|
7
10
|
Easing,
|
|
11
|
+
ReduceMotion,
|
|
8
12
|
} from 'react-native-reanimated'
|
|
9
13
|
import { useTheme } from './use_theme'
|
|
10
14
|
|
|
15
|
+
const FLASH_IN_MS = 150
|
|
16
|
+
const FLASH_HOLD_MS = 500
|
|
17
|
+
const FLASH_OUT_MS = 1500
|
|
18
|
+
|
|
11
19
|
export function useAnimatedMessageBackgroundColor() {
|
|
12
20
|
const { colors } = useTheme()
|
|
13
|
-
const
|
|
21
|
+
const pressProgress = useSharedValue(0)
|
|
22
|
+
const flashProgress = useSharedValue(0)
|
|
14
23
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
default: 'transparent',
|
|
18
|
-
})
|
|
24
|
+
const tintColor = colors.fillColorNeutral050Base
|
|
25
|
+
const tintsOnPress = Platform.OS === 'ios'
|
|
19
26
|
|
|
20
27
|
const animatedBackgroundColor = useAnimatedStyle(() => {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
['transparent', pressedColor]
|
|
25
|
-
)
|
|
28
|
+
const pressTint = tintsOnPress ? pressProgress.value : 0
|
|
29
|
+
const progress = Math.max(pressTint, flashProgress.value)
|
|
30
|
+
const backgroundColor = interpolateColor(progress, [0, 1], ['transparent', tintColor])
|
|
26
31
|
return {
|
|
27
32
|
backgroundColor,
|
|
28
33
|
}
|
|
29
34
|
})
|
|
30
35
|
|
|
31
36
|
const handleMessagePressIn = () => {
|
|
32
|
-
|
|
37
|
+
pressProgress.value = withTiming(1, { duration: 300, easing: Easing.inOut(Easing.ease) })
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
const handleMessagePressOut = () => {
|
|
36
|
-
|
|
41
|
+
pressProgress.value = withTiming(0, { duration: 300, easing: Easing.inOut(Easing.ease) })
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
const flash = useCallback(() => {
|
|
45
|
+
flashProgress.value = withSequence(
|
|
46
|
+
withTiming(1, {
|
|
47
|
+
duration: FLASH_IN_MS,
|
|
48
|
+
easing: Easing.out(Easing.ease),
|
|
49
|
+
reduceMotion: ReduceMotion.Never,
|
|
50
|
+
}),
|
|
51
|
+
withDelay(
|
|
52
|
+
FLASH_HOLD_MS,
|
|
53
|
+
withTiming(0, {
|
|
54
|
+
duration: FLASH_OUT_MS,
|
|
55
|
+
easing: Easing.out(Easing.ease),
|
|
56
|
+
reduceMotion: ReduceMotion.System,
|
|
57
|
+
}),
|
|
58
|
+
ReduceMotion.Never
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
}, [flashProgress])
|
|
62
|
+
|
|
39
63
|
return {
|
|
40
64
|
animatedBackgroundColor,
|
|
65
|
+
flash,
|
|
41
66
|
handleMessagePressIn,
|
|
42
67
|
handleMessagePressOut,
|
|
43
68
|
}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { act, fireEvent, render } from '@testing-library/react-native'
|
|
2
2
|
import React from 'react'
|
|
3
|
+
import { Keyboard } from 'react-native'
|
|
3
4
|
import type { useSearchResults } from '../../../hooks/use_search_results'
|
|
5
|
+
import type {
|
|
6
|
+
ConversationSearchResultResource,
|
|
7
|
+
MessageSearchResultResource,
|
|
8
|
+
} from '../../../types/resources/search_result'
|
|
4
9
|
import { SearchScreen } from '../search_screen'
|
|
5
10
|
|
|
11
|
+
const mockNavigate = jest.fn()
|
|
12
|
+
jest.mock('@react-navigation/native', () => ({
|
|
13
|
+
...jest.requireActual('@react-navigation/native'),
|
|
14
|
+
useNavigation: () => ({ navigate: mockNavigate }),
|
|
15
|
+
}))
|
|
16
|
+
|
|
6
17
|
const mockUseSearchResults = jest.fn<ReturnType<typeof useSearchResults>, [string]>()
|
|
7
18
|
jest.mock('../../../hooks/use_search_results', () => ({
|
|
8
19
|
useSearchResults: (term: string) => mockUseSearchResults(term),
|
|
@@ -14,8 +25,48 @@ jest.mock('../../../components/display/keyboard_view', () => ({
|
|
|
14
25
|
|
|
15
26
|
const inputLabel = 'Search titles and messages'
|
|
16
27
|
|
|
28
|
+
const conversationResult: ConversationSearchResultResource = {
|
|
29
|
+
type: 'conversation',
|
|
30
|
+
id: '42',
|
|
31
|
+
title: 'Budget Team',
|
|
32
|
+
url: '/conversations/42',
|
|
33
|
+
extra: { avatar: { previewAvatarUrls: [] } },
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const messageResult: MessageSearchResultResource = {
|
|
37
|
+
type: 'message',
|
|
38
|
+
id: '01JMESSAGEULID',
|
|
39
|
+
title: 'Finance',
|
|
40
|
+
url: '/conversations/7/messages/01JMESSAGEULID',
|
|
41
|
+
extra: {
|
|
42
|
+
conversationId: 7,
|
|
43
|
+
authorName: 'Ann',
|
|
44
|
+
snippet: [{ text: 'budget', match: true }],
|
|
45
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
46
|
+
avatar: { previewAvatarUrls: [] },
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const renderWithResults = () => {
|
|
51
|
+
mockUseSearchResults.mockReturnValue({
|
|
52
|
+
results: [conversationResult, messageResult],
|
|
53
|
+
failedTypes: [],
|
|
54
|
+
isPending: false,
|
|
55
|
+
isError: false,
|
|
56
|
+
isFetchNextPageError: false,
|
|
57
|
+
loadMore: jest.fn(),
|
|
58
|
+
hasNextPage: false,
|
|
59
|
+
isFetchingNextPage: false,
|
|
60
|
+
})
|
|
61
|
+
const view = render(<SearchScreen />)
|
|
62
|
+
fireEvent.changeText(view.getByLabelText(inputLabel), 'budget')
|
|
63
|
+
act(() => jest.advanceTimersByTime(200))
|
|
64
|
+
return view
|
|
65
|
+
}
|
|
66
|
+
|
|
17
67
|
beforeEach(() => {
|
|
18
68
|
jest.useFakeTimers()
|
|
69
|
+
mockNavigate.mockReset()
|
|
19
70
|
mockUseSearchResults.mockReset().mockReturnValue({
|
|
20
71
|
results: [],
|
|
21
72
|
failedTypes: [],
|
|
@@ -61,4 +112,29 @@ describe('SearchScreen', () => {
|
|
|
61
112
|
|
|
62
113
|
expect(view.getByText('Search chat')).toBeTruthy()
|
|
63
114
|
})
|
|
115
|
+
|
|
116
|
+
it('opens a conversation result at the latest message', () => {
|
|
117
|
+
const dismiss = jest.spyOn(Keyboard, 'dismiss')
|
|
118
|
+
const view = renderWithResults()
|
|
119
|
+
|
|
120
|
+
fireEvent.press(view.getByLabelText('Budget Team'))
|
|
121
|
+
|
|
122
|
+
expect(dismiss).toHaveBeenCalled()
|
|
123
|
+
expect(mockNavigate).toHaveBeenCalledWith('Conversation', {
|
|
124
|
+
conversation_id: 42,
|
|
125
|
+
title: 'Budget Team',
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('opens a message result anchored at that message', () => {
|
|
130
|
+
const view = renderWithResults()
|
|
131
|
+
|
|
132
|
+
fireEvent.press(view.getByLabelText(/^Finance, Ann: budget/))
|
|
133
|
+
|
|
134
|
+
expect(mockNavigate).toHaveBeenCalledWith('Conversation', {
|
|
135
|
+
conversation_id: 7,
|
|
136
|
+
initial_message_id: '01JMESSAGEULID',
|
|
137
|
+
title: 'Finance',
|
|
138
|
+
})
|
|
139
|
+
})
|
|
64
140
|
})
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { ParamListBase } from '@react-navigation/native'
|
|
1
|
+
import { ParamListBase, useNavigation } from '@react-navigation/native'
|
|
2
2
|
import {
|
|
3
3
|
NativeStackNavigationOptions,
|
|
4
4
|
NativeStackScreenProps,
|
|
5
5
|
} from '@react-navigation/native-stack'
|
|
6
|
-
import { noop } from 'lodash'
|
|
7
6
|
import React, { useState } from 'react'
|
|
8
|
-
import { StyleSheet, View } from 'react-native'
|
|
7
|
+
import { Keyboard, StyleSheet, View } from 'react-native'
|
|
9
8
|
import { KeyboardView } from '../../components'
|
|
10
9
|
import {
|
|
11
10
|
composeOptions,
|
|
12
11
|
withMinimalBack,
|
|
13
12
|
} from '../../components/display/platform_modal_header_buttons'
|
|
14
13
|
import { useDebouncedSearchTerm } from '../../hooks/use_debounced_search_term'
|
|
14
|
+
import type { SearchResultResource } from '../../types/resources/search_result'
|
|
15
15
|
import { SearchPromptBlankState } from './components/search_blank_states'
|
|
16
16
|
import { SearchField } from './components/search_field'
|
|
17
17
|
import { SearchResultsList } from './components/search_results_list'
|
|
@@ -25,15 +25,33 @@ export const SearchScreenOptions = ({
|
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
export function SearchScreen() {
|
|
28
|
+
const navigation = useNavigation()
|
|
28
29
|
const [term, setTerm] = useState('')
|
|
29
30
|
const debouncedTerm = useDebouncedSearchTerm(term)
|
|
30
31
|
|
|
32
|
+
const openResult = (result: SearchResultResource) => {
|
|
33
|
+
Keyboard.dismiss()
|
|
34
|
+
|
|
35
|
+
if (result.type === 'conversation') {
|
|
36
|
+
navigation.navigate('Conversation', {
|
|
37
|
+
conversation_id: Number(result.id),
|
|
38
|
+
title: result.title,
|
|
39
|
+
})
|
|
40
|
+
} else {
|
|
41
|
+
navigation.navigate('Conversation', {
|
|
42
|
+
conversation_id: result.extra.conversationId,
|
|
43
|
+
initial_message_id: result.id,
|
|
44
|
+
title: result.title,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
return (
|
|
32
50
|
<KeyboardView>
|
|
33
51
|
<View style={styles.container} testID="search-screen">
|
|
34
52
|
<SearchField term={term} onChange={setTerm} />
|
|
35
53
|
{debouncedTerm.length > 0 ? (
|
|
36
|
-
<SearchResultsList key={debouncedTerm} term={debouncedTerm} onSelect={
|
|
54
|
+
<SearchResultsList key={debouncedTerm} term={debouncedTerm} onSelect={openResult} />
|
|
37
55
|
) : (
|
|
38
56
|
<SearchPromptBlankState />
|
|
39
57
|
)}
|