@planningcenter/chat-react-native 3.2.0-rc.7 → 3.2.0-rc.8
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/conversations/conversation_actions.d.ts.map +1 -1
- package/build/components/conversations/conversation_actions.js +14 -15
- package/build/components/conversations/conversation_actions.js.map +1 -1
- package/build/components/conversations/conversations.d.ts.map +1 -1
- package/build/components/conversations/conversations.js +5 -8
- package/build/components/conversations/conversations.js.map +1 -1
- package/build/contexts/chat_context.d.ts +1 -0
- package/build/contexts/chat_context.d.ts.map +1 -1
- package/build/contexts/chat_context.js +3 -0
- package/build/contexts/chat_context.js.map +1 -1
- package/build/contexts/conversations_context.d.ts.map +1 -1
- package/build/contexts/conversations_context.js +3 -12
- package/build/contexts/conversations_context.js.map +1 -1
- package/build/hooks/use_conversations_actions.d.ts +221 -0
- package/build/hooks/use_conversations_actions.d.ts.map +1 -0
- package/build/hooks/use_conversations_actions.js +93 -0
- package/build/hooks/use_conversations_actions.js.map +1 -0
- package/build/hooks/use_conversations_cache.d.ts +18 -0
- package/build/hooks/use_conversations_cache.d.ts.map +1 -0
- package/build/hooks/{use_conversation_jolt_events.js → use_conversations_cache.js} +27 -17
- package/build/hooks/use_conversations_cache.js.map +1 -0
- package/build/hooks/use_conversations_jolt_events.d.ts +3 -0
- package/build/hooks/use_conversations_jolt_events.d.ts.map +1 -0
- package/build/hooks/use_conversations_jolt_events.js +12 -0
- package/build/hooks/use_conversations_jolt_events.js.map +1 -0
- package/build/hooks/use_jolt.d.ts.map +1 -1
- package/build/hooks/use_jolt.js +39 -10
- package/build/hooks/use_jolt.js.map +1 -1
- package/build/screens/conversations/components/list_header_component.d.ts.map +1 -1
- package/build/screens/conversations/components/list_header_component.js +5 -1
- package/build/screens/conversations/components/list_header_component.js.map +1 -1
- package/build/utils/cache/page_mutations.d.ts +18 -0
- package/build/utils/cache/page_mutations.d.ts.map +1 -1
- package/build/utils/cache/page_mutations.js +13 -0
- package/build/utils/cache/page_mutations.js.map +1 -1
- package/build/utils/request/conversation.d.ts +1 -3
- package/build/utils/request/conversation.d.ts.map +1 -1
- package/build/utils/request/conversation.js +37 -30
- package/build/utils/request/conversation.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/utils/cache/page_mutations.ts +49 -15
- package/src/components/conversations/conversation_actions.tsx +21 -17
- package/src/components/conversations/conversations.tsx +23 -26
- package/src/contexts/chat_context.tsx +4 -0
- package/src/contexts/conversations_context.tsx +3 -13
- package/src/hooks/use_conversations_actions.ts +108 -0
- package/src/hooks/{use_conversation_jolt_events.ts → use_conversations_cache.ts} +35 -20
- package/src/hooks/use_conversations_jolt_events.ts +21 -0
- package/src/hooks/use_jolt.ts +51 -10
- package/src/screens/conversations/components/list_header_component.tsx +6 -1
- package/src/utils/cache/page_mutations.ts +22 -0
- package/src/utils/request/conversation.ts +39 -34
- package/build/contexts/swipeable_active_conversation.d.ts +0 -11
- package/build/contexts/swipeable_active_conversation.d.ts.map +0 -1
- package/build/contexts/swipeable_active_conversation.js +0 -16
- package/build/contexts/swipeable_active_conversation.js.map +0 -1
- package/build/hooks/use_conversation_jolt_events.d.ts +0 -2
- package/build/hooks/use_conversation_jolt_events.d.ts.map +0 -1
- package/build/hooks/use_conversation_jolt_events.js.map +0 -1
- package/src/contexts/swipeable_active_conversation.tsx +0 -27
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
deleteRecordInPagesData,
|
|
3
|
+
updateAllRecordsInPagesData,
|
|
3
4
|
updateOrCreateRecordInPagesData,
|
|
4
5
|
updateRecordInPagesData,
|
|
5
6
|
} from '../../../utils/'
|
|
@@ -28,11 +29,44 @@ const data = {
|
|
|
28
29
|
],
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
describe('
|
|
32
|
+
describe('updateAllRecordsInPagesData', () => {
|
|
33
|
+
it('should update all records in the pages data', () => {
|
|
34
|
+
const result = updateAllRecordsInPagesData({
|
|
35
|
+
data,
|
|
36
|
+
processRecord: r => ({ ...r, text: 'updated ' + r.text }),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
expect(result).toEqual({
|
|
40
|
+
pageParams: {},
|
|
41
|
+
pages: [
|
|
42
|
+
{
|
|
43
|
+
data: [
|
|
44
|
+
{ id: '1', type: 'Message', text: 'updated message 1' },
|
|
45
|
+
{ id: '2', type: 'Message', text: 'updated message 2' },
|
|
46
|
+
],
|
|
47
|
+
included: [],
|
|
48
|
+
links: {},
|
|
49
|
+
meta: { count: 2, totalCount: 2 },
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
data: [
|
|
53
|
+
{ id: '3', type: 'Message', text: 'updated message 3' },
|
|
54
|
+
{ id: '4', type: 'Message', text: 'updated message 4' },
|
|
55
|
+
],
|
|
56
|
+
included: [],
|
|
57
|
+
links: {},
|
|
58
|
+
meta: { count: 2, totalCount: 2 },
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('updateOrCreateRecordInPagesData', () => {
|
|
32
66
|
it('should update the record in the pages data', () => {
|
|
33
67
|
const id = '3'
|
|
34
68
|
const record = createRecord({ id })
|
|
35
|
-
const result =
|
|
69
|
+
const result = updateOrCreateRecordInPagesData({ data, record })
|
|
36
70
|
|
|
37
71
|
expect(result).toEqual({
|
|
38
72
|
pageParams: {},
|
|
@@ -63,7 +97,7 @@ describe('updateRecordInPagesData', () => {
|
|
|
63
97
|
const id = '2'
|
|
64
98
|
const record = createRecord({ id })
|
|
65
99
|
|
|
66
|
-
const result =
|
|
100
|
+
const result = updateOrCreateRecordInPagesData<typeof record>({
|
|
67
101
|
data,
|
|
68
102
|
record,
|
|
69
103
|
processRecord: r => ({ ...r, text: 'updated ' + r.text }),
|
|
@@ -93,20 +127,9 @@ describe('updateRecordInPagesData', () => {
|
|
|
93
127
|
],
|
|
94
128
|
})
|
|
95
129
|
})
|
|
96
|
-
|
|
97
|
-
it('should skip the record if it does not exist', () => {
|
|
98
|
-
const id = '5'
|
|
99
|
-
const record = createRecord({ id })
|
|
100
|
-
const result = updateRecordInPagesData<typeof record>({
|
|
101
|
-
data,
|
|
102
|
-
record,
|
|
103
|
-
processRecord: r => ({ ...r, text: 'updated ' + r.text }),
|
|
104
|
-
})
|
|
105
|
-
expect(result).toEqual(data)
|
|
106
|
-
})
|
|
107
130
|
})
|
|
108
131
|
|
|
109
|
-
describe('updateOrCreateRecordInPagesData', () => {
|
|
132
|
+
describe('updateOrCreateRecordInPagesData function', () => {
|
|
110
133
|
it('should update the record in the pages data', () => {
|
|
111
134
|
const id = '3'
|
|
112
135
|
const record = createRecord({ id })
|
|
@@ -207,6 +230,17 @@ describe('updateOrCreateRecordInPagesData', () => {
|
|
|
207
230
|
],
|
|
208
231
|
})
|
|
209
232
|
})
|
|
233
|
+
|
|
234
|
+
it('should skip the record if it does not exist', () => {
|
|
235
|
+
const id = '5'
|
|
236
|
+
const record = createRecord({ id })
|
|
237
|
+
const result = updateRecordInPagesData<typeof record>({
|
|
238
|
+
data,
|
|
239
|
+
record,
|
|
240
|
+
processRecord: r => ({ ...r, text: 'updated ' + r.text }),
|
|
241
|
+
})
|
|
242
|
+
expect(result).toEqual(data)
|
|
243
|
+
})
|
|
210
244
|
})
|
|
211
245
|
|
|
212
246
|
describe('deleteRecordInPagesData', () => {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
-
import { Platform,
|
|
2
|
+
import { Platform, Pressable, StyleSheet, View, ViewStyle } from 'react-native'
|
|
3
3
|
import ReanimatedSwipeable, {
|
|
4
4
|
SwipeableMethods,
|
|
5
5
|
} from 'react-native-gesture-handler/ReanimatedSwipeable'
|
|
6
|
+
import { useConversationsContext } from '../../contexts/conversations_context'
|
|
6
7
|
import { useTheme } from '../../hooks'
|
|
7
|
-
import {
|
|
8
|
-
|
|
8
|
+
import {
|
|
9
|
+
useConversationsMarkRead,
|
|
10
|
+
useConversationsMute,
|
|
11
|
+
} from '../../hooks/use_conversations_actions'
|
|
9
12
|
import { ConversationResource } from '../../types'
|
|
10
|
-
import {
|
|
13
|
+
import { tokens } from '../../vendor/tapestry/tokens'
|
|
14
|
+
import { ActionToggleButton } from './action_toggle_button'
|
|
11
15
|
|
|
12
16
|
export function ConversationActions({
|
|
13
17
|
children,
|
|
@@ -22,7 +26,7 @@ export function ConversationActions({
|
|
|
22
26
|
}) {
|
|
23
27
|
const swipeableRef = useRef<SwipeableMethods>(null)
|
|
24
28
|
const styles = useStyles()
|
|
25
|
-
const { activeConversationId, setActiveConversationId } =
|
|
29
|
+
const { activeConversationId, setActiveConversationId } = useConversationsContext()
|
|
26
30
|
const [disabled, setDisabled] = useState(false)
|
|
27
31
|
const overshootLeft = Platform.OS === 'ios'
|
|
28
32
|
|
|
@@ -75,44 +79,44 @@ interface LeftActionsProps {
|
|
|
75
79
|
|
|
76
80
|
function LeftActions({ conversation, onClose }: LeftActionsProps) {
|
|
77
81
|
const styles = useStyles()
|
|
78
|
-
const [muted, setMuted] = useState(conversation.muted)
|
|
79
|
-
const [latestMessageUnread, setLatestMessageUnread] = useState(conversation.unreadCount > 0)
|
|
80
82
|
const emptyConversation = conversation.lastMessageCreatedAt === null
|
|
81
83
|
|
|
82
84
|
const muteToggleContent = {
|
|
83
|
-
true: { iconName: 'general.
|
|
84
|
-
false: { iconName: 'general.
|
|
85
|
+
true: { iconName: 'general.bell', label: 'Unmute' },
|
|
86
|
+
false: { iconName: 'general.bellMuted', label: 'Mute' },
|
|
85
87
|
} as const
|
|
86
88
|
|
|
87
89
|
const latestMessageUnreadToggleContent = {
|
|
88
90
|
true: { iconName: 'general.outlinedTextMessage', label: 'Mark read' },
|
|
89
91
|
false: { iconName: 'general.textMessageNotifications', label: 'Mark unread' },
|
|
90
92
|
} as const
|
|
93
|
+
const { muted, setMuted, isPending } = useConversationsMute({ conversation })
|
|
94
|
+
const { read, markRead, isPending: markReadPending } = useConversationsMarkRead({ conversation })
|
|
91
95
|
|
|
92
96
|
const handleMute = useCallback(() => {
|
|
93
97
|
setMuted(!muted)
|
|
94
98
|
onClose()
|
|
95
|
-
}, [muted, onClose])
|
|
99
|
+
}, [muted, onClose, setMuted])
|
|
96
100
|
|
|
97
101
|
const handleLatestMessageUnread = useCallback(() => {
|
|
98
|
-
|
|
102
|
+
markRead(!read)
|
|
99
103
|
onClose()
|
|
100
|
-
}, [
|
|
104
|
+
}, [read, onClose, markRead])
|
|
101
105
|
|
|
102
106
|
return (
|
|
103
107
|
<View style={styles.actionButtonContainer}>
|
|
104
108
|
<ActionToggleButton
|
|
105
|
-
loading={
|
|
109
|
+
loading={markReadPending}
|
|
106
110
|
disabled={emptyConversation}
|
|
107
|
-
toggled={
|
|
108
|
-
onPress={
|
|
111
|
+
toggled={!read}
|
|
112
|
+
onPress={handleLatestMessageUnread}
|
|
109
113
|
toggleContent={latestMessageUnreadToggleContent}
|
|
110
114
|
backgroundColor={tokens.fillColorInteractionSwipeDefault}
|
|
111
115
|
/>
|
|
112
116
|
<ActionToggleButton
|
|
113
|
-
loading={
|
|
117
|
+
loading={isPending}
|
|
114
118
|
toggled={muted}
|
|
115
|
-
onPress={
|
|
119
|
+
onPress={handleMute}
|
|
116
120
|
toggleContent={muteToggleContent}
|
|
117
121
|
backgroundColor={tokens.fillColorInteractionSwipeSecondary}
|
|
118
122
|
/>
|
|
@@ -6,7 +6,6 @@ import { useConversationsContext } from '../../contexts/conversations_context'
|
|
|
6
6
|
import { useTheme } from '../../hooks'
|
|
7
7
|
import { Text } from '../display'
|
|
8
8
|
import { ConversationPreview } from './conversation_preview'
|
|
9
|
-
import { ConversationActionsProvider } from '../../contexts/swipeable_active_conversation'
|
|
10
9
|
|
|
11
10
|
interface ConversationsProps {
|
|
12
11
|
ListHeaderComponent?:
|
|
@@ -32,31 +31,29 @@ export const Conversations = ({ ListHeaderComponent }: ConversationsProps) => {
|
|
|
32
31
|
const showBadges = !chat_group_graph_id
|
|
33
32
|
|
|
34
33
|
return (
|
|
35
|
-
<
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
</View>
|
|
59
|
-
</ConversationActionsProvider>
|
|
34
|
+
<View style={styles.container}>
|
|
35
|
+
<FlashList
|
|
36
|
+
data={conversations}
|
|
37
|
+
estimatedItemSize={97}
|
|
38
|
+
contentContainerStyle={styles.contentContainer}
|
|
39
|
+
onRefresh={refetch}
|
|
40
|
+
refreshing={!isFetched && isRefetching}
|
|
41
|
+
ListHeaderComponent={ListHeaderComponent}
|
|
42
|
+
ListEmptyComponent={
|
|
43
|
+
<View style={styles.listEmpty}>
|
|
44
|
+
<Text variant="secondary">No conversations found</Text>
|
|
45
|
+
</View>
|
|
46
|
+
}
|
|
47
|
+
renderItem={({ item }) => (
|
|
48
|
+
<ConversationPreview
|
|
49
|
+
conversation={item}
|
|
50
|
+
onPress={() => navigation.navigate('Conversation', { conversation_id: item.id })}
|
|
51
|
+
showBadges={showBadges}
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
54
|
+
onEndReached={() => fetchNextPage()}
|
|
55
|
+
/>
|
|
56
|
+
</View>
|
|
60
57
|
)
|
|
61
58
|
}
|
|
62
59
|
|
|
@@ -49,6 +49,10 @@ export interface CreateChatThemeProps {
|
|
|
49
49
|
colorScheme?: ColorSchemeName
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export const useChatContext = () => {
|
|
53
|
+
return React.useContext(ChatContext)
|
|
54
|
+
}
|
|
55
|
+
|
|
52
56
|
export const useCreateChatTheme = ({
|
|
53
57
|
theme: customTheme = {},
|
|
54
58
|
colorScheme: appColorScheme,
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
InfiniteData,
|
|
8
8
|
} from '@tanstack/react-query'
|
|
9
9
|
import { ApiCollection, ConversationResource } from '../types'
|
|
10
|
-
import { useConversationsJoltEvents } from '../hooks/
|
|
10
|
+
import { useConversationsJoltEvents } from '../hooks/use_conversations_jolt_events'
|
|
11
11
|
|
|
12
12
|
interface ConversationsContextValue extends UseConversationsValue {
|
|
13
13
|
activeConversationId?: number
|
|
@@ -41,17 +41,7 @@ export const ConversationsContextProvider = ({
|
|
|
41
41
|
const [activeConversationId, setActiveConversationId] = useState<number | undefined>()
|
|
42
42
|
const { chat_group_graph_id, group_source_app_name } = args
|
|
43
43
|
|
|
44
|
-
const
|
|
45
|
-
if (chat_group_graph_id) {
|
|
46
|
-
return 'group'
|
|
47
|
-
} else if (group_source_app_name) {
|
|
48
|
-
return 'group_source_app_name'
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return 'mine_or_not_empty'
|
|
52
|
-
}, [chat_group_graph_id, group_source_app_name])
|
|
53
|
-
|
|
54
|
-
const query = useConversations({ group: chat_group_graph_id, group_source_app_name, filter })
|
|
44
|
+
const query = useConversations({ chat_group_graph_id, group_source_app_name })
|
|
55
45
|
|
|
56
46
|
const value = useMemo(
|
|
57
47
|
() => ({
|
|
@@ -63,7 +53,7 @@ export const ConversationsContextProvider = ({
|
|
|
63
53
|
[args, activeConversationId, query]
|
|
64
54
|
)
|
|
65
55
|
|
|
66
|
-
useConversationsJoltEvents()
|
|
56
|
+
useConversationsJoltEvents(args)
|
|
67
57
|
|
|
68
58
|
return <ConversationsContext.Provider value={value}>{children}</ConversationsContext.Provider>
|
|
69
59
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useMutation } from '@tanstack/react-query'
|
|
2
|
+
import { Alert } from 'react-native'
|
|
3
|
+
import { useConversationsContext } from '../contexts/conversations_context'
|
|
4
|
+
import { ApiResource, ConversationResource } from '../types'
|
|
5
|
+
import { useApiClient } from './use_api_client'
|
|
6
|
+
import { useConversationsCache } from './use_conversations_cache'
|
|
7
|
+
|
|
8
|
+
export const useConversationsMarkRead = ({
|
|
9
|
+
conversation,
|
|
10
|
+
}: {
|
|
11
|
+
conversation: ConversationResource
|
|
12
|
+
}) => {
|
|
13
|
+
const apiClient = useApiClient()
|
|
14
|
+
const { args } = useConversationsContext()
|
|
15
|
+
const { update, invalidate } = useConversationsCache(args)
|
|
16
|
+
|
|
17
|
+
const { mutate: handleMarkRead, ...mutation } = useMutation({
|
|
18
|
+
onMutate: async (read: boolean) => {
|
|
19
|
+
update({
|
|
20
|
+
...conversation,
|
|
21
|
+
unreadCount: read ? 0 : 1,
|
|
22
|
+
})
|
|
23
|
+
},
|
|
24
|
+
mutationKey: ['markRead', conversation.id],
|
|
25
|
+
mutationFn: async (read: boolean) => {
|
|
26
|
+
const action = read ? 'mark_read' : 'mark_unread'
|
|
27
|
+
|
|
28
|
+
return apiClient.chat.post<ApiResource<ConversationResource>>({
|
|
29
|
+
url: `/me/conversations/${conversation.id}/${action}`,
|
|
30
|
+
data: { data: { type: '', attributes: {} }, fields: { Conversation: 'unread_count' } },
|
|
31
|
+
})
|
|
32
|
+
},
|
|
33
|
+
onSuccess: (response: ApiResource<ConversationResource>) => {
|
|
34
|
+
update(response.data)
|
|
35
|
+
},
|
|
36
|
+
onError: () => {
|
|
37
|
+
Alert.alert('Oops', 'Something went wrong updating this conversation, please try again')
|
|
38
|
+
invalidate()
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
read: conversation.unreadCount < 1, // prefer cache
|
|
44
|
+
markRead: handleMarkRead,
|
|
45
|
+
...mutation,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const useConversationsMute = ({ conversation }: { conversation: ConversationResource }) => {
|
|
50
|
+
const apiClient = useApiClient()
|
|
51
|
+
const { args } = useConversationsContext()
|
|
52
|
+
const { update, invalidate } = useConversationsCache(args)
|
|
53
|
+
|
|
54
|
+
const { mutate: setMuted, ...mutation } = useMutation({
|
|
55
|
+
onMutate: async (muted: boolean) => {
|
|
56
|
+
update({
|
|
57
|
+
...conversation,
|
|
58
|
+
muted,
|
|
59
|
+
})
|
|
60
|
+
},
|
|
61
|
+
mutationKey: ['muteConversation'],
|
|
62
|
+
mutationFn: async (muted: boolean) => {
|
|
63
|
+
const action = muted ? 'mute' : 'unmute'
|
|
64
|
+
|
|
65
|
+
return apiClient.chat.post<ApiResource<ConversationResource>>({
|
|
66
|
+
url: `/me/conversations/${conversation.id}/${action}`,
|
|
67
|
+
data: { data: { type: '', attributes: {} }, fields: { Conversation: 'muted' } },
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
onSuccess: (response: ApiResource<ConversationResource>) => {
|
|
71
|
+
update(response.data)
|
|
72
|
+
},
|
|
73
|
+
onError: () => {
|
|
74
|
+
Alert.alert('Oops', 'Something went wrong muting this conversation, please try again')
|
|
75
|
+
invalidate()
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
muted: conversation.muted, // prefer cache
|
|
81
|
+
setMuted,
|
|
82
|
+
...mutation,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const useMarkAllRead = () => {
|
|
87
|
+
const apiClient = useApiClient()
|
|
88
|
+
const { args } = useConversationsContext()
|
|
89
|
+
const { invalidate, updateAll } = useConversationsCache(args)
|
|
90
|
+
const { mutate: markAllRead, ...query } = useMutation({
|
|
91
|
+
onMutate: () => {
|
|
92
|
+
updateAll({
|
|
93
|
+
unreadCount: 0,
|
|
94
|
+
})
|
|
95
|
+
},
|
|
96
|
+
mutationKey: ['markAllRead', args],
|
|
97
|
+
mutationFn: () =>
|
|
98
|
+
apiClient.chat.post({
|
|
99
|
+
url: '/me/mark_all_read',
|
|
100
|
+
}),
|
|
101
|
+
onSettled: invalidate,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
markAllRead,
|
|
106
|
+
...query,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
import { InfiniteData, useQueryClient } from '@tanstack/react-query'
|
|
2
2
|
import { ApiCollection, ApiResource, ConversationResource } from '../types'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
deleteRecordInPagesData,
|
|
5
|
+
updateAllRecordsInPagesData,
|
|
6
|
+
updateOrCreateRecordInPagesData,
|
|
7
|
+
} from '../utils'
|
|
8
|
+
import { ConversationRequestArgs, getConversationsRequestArgs } from '../utils/request/conversation'
|
|
4
9
|
import { useApiClient } from './use_api_client'
|
|
5
|
-
import { useCurrentPerson } from './use_current_person'
|
|
6
|
-
import { useJoltChannel, useJoltEvent } from './use_jolt'
|
|
7
10
|
import { getRequestQueryKey } from './use_suspense_api'
|
|
8
|
-
import { JoltConversationEvent } from '../types/jolt_events'
|
|
9
|
-
import { ConversationDeletedEvent } from '../types/jolt_events/conversation_events'
|
|
10
|
-
import { getConversationsRequestArgs } from '../utils/request/conversation'
|
|
11
11
|
|
|
12
12
|
type QueryData = InfiniteData<ApiCollection<ConversationResource>>
|
|
13
13
|
|
|
14
|
-
export function
|
|
14
|
+
export function useConversationsCache(args?: Partial<ConversationRequestArgs>) {
|
|
15
15
|
const apiClient = useApiClient()
|
|
16
16
|
const queryClient = useQueryClient()
|
|
17
|
-
const
|
|
18
|
-
const joltChannel = useJoltChannel(`chat.people.${currentPerson.id}`)
|
|
19
|
-
|
|
20
|
-
const conversationsRequestArgs = getConversationsRequestArgs()
|
|
17
|
+
const conversationsRequestArgs = getConversationsRequestArgs(args)
|
|
21
18
|
const conversationQueryKey = getRequestQueryKey(conversationsRequestArgs)
|
|
22
19
|
|
|
23
20
|
const fetchConversation = async (id: number) => {
|
|
@@ -33,10 +30,7 @@ export function useConversationsJoltEvents() {
|
|
|
33
30
|
return data
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
const
|
|
37
|
-
const { data } = e.data
|
|
38
|
-
const conversation: ConversationResource = await fetchConversation(data.id).catch(c => c)
|
|
39
|
-
|
|
33
|
+
const updateOrCreate = async (conversation: ConversationResource) => {
|
|
40
34
|
queryClient.setQueryData<QueryData>(conversationQueryKey, prev =>
|
|
41
35
|
updateOrCreateRecordInPagesData({
|
|
42
36
|
data: prev,
|
|
@@ -48,16 +42,37 @@ export function useConversationsJoltEvents() {
|
|
|
48
42
|
)
|
|
49
43
|
}
|
|
50
44
|
|
|
51
|
-
const
|
|
45
|
+
const updateAll = async (update: Partial<ConversationResource>) => {
|
|
46
|
+
queryClient.setQueryData<QueryData>(conversationQueryKey, prev =>
|
|
47
|
+
updateAllRecordsInPagesData({
|
|
48
|
+
data: prev,
|
|
49
|
+
processRecord: record => ({ ...record, ...update }),
|
|
50
|
+
})
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fetchAndUpdateOrCreate = async ({ id }: { id: number }) => {
|
|
55
|
+
const conversation: ConversationResource = await fetchConversation(id).catch(c => c)
|
|
56
|
+
|
|
57
|
+
updateOrCreate(conversation)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const handleConversationDestroy = ({ id }: { id: number }) => {
|
|
52
61
|
queryClient.setQueryData<QueryData>(conversationQueryKey, prev =>
|
|
53
62
|
deleteRecordInPagesData({
|
|
54
63
|
data: prev,
|
|
55
|
-
record: { id
|
|
64
|
+
record: { id },
|
|
56
65
|
})
|
|
57
66
|
)
|
|
58
67
|
}
|
|
59
68
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
return {
|
|
70
|
+
create: updateOrCreate,
|
|
71
|
+
destroy: handleConversationDestroy,
|
|
72
|
+
fetchCreate: fetchAndUpdateOrCreate,
|
|
73
|
+
fetchUpdate: fetchAndUpdateOrCreate,
|
|
74
|
+
invalidate: () => queryClient.invalidateQueries({ queryKey: conversationQueryKey }),
|
|
75
|
+
update: updateOrCreate,
|
|
76
|
+
updateAll,
|
|
77
|
+
}
|
|
63
78
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { JoltConversationEvent } from '../types/jolt_events'
|
|
2
|
+
import { ConversationRequestArgs } from '../utils/request/conversation'
|
|
3
|
+
import { useConversationsCache } from './use_conversations_cache'
|
|
4
|
+
import { useCurrentPerson } from './use_current_person'
|
|
5
|
+
import { useJoltChannel, useJoltEvent } from './use_jolt'
|
|
6
|
+
|
|
7
|
+
export function useConversationsJoltEvents(args?: Partial<ConversationRequestArgs>) {
|
|
8
|
+
const currentPerson = useCurrentPerson()
|
|
9
|
+
const joltChannel = useJoltChannel(`chat.people.${currentPerson.id}`)
|
|
10
|
+
const cache = useConversationsCache(args)
|
|
11
|
+
|
|
12
|
+
useJoltEvent(joltChannel, 'conversation.updated', (e: JoltConversationEvent) =>
|
|
13
|
+
cache.fetchUpdate({ id: e.data.data.id })
|
|
14
|
+
)
|
|
15
|
+
useJoltEvent(joltChannel, 'conversation.created', (e: JoltConversationEvent) =>
|
|
16
|
+
cache.fetchCreate({ id: e.data.data.id })
|
|
17
|
+
)
|
|
18
|
+
useJoltEvent(joltChannel, 'conversation.destroyed', (e: JoltConversationEvent) =>
|
|
19
|
+
cache.destroy({ id: e.data.data.id })
|
|
20
|
+
)
|
|
21
|
+
}
|
package/src/hooks/use_jolt.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import JoltClient from '@planningcenter/jolt-client'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
CustomMessage,
|
|
4
|
+
FetchAuthToken,
|
|
5
|
+
} from '@planningcenter/jolt-client/dist/types/JoltConnection'
|
|
3
6
|
import {
|
|
4
7
|
FetchSubscribeToken,
|
|
5
8
|
JoltSubscription,
|
|
6
9
|
} from '@planningcenter/jolt-client/dist/types/JoltSubscription'
|
|
7
|
-
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
|
|
8
|
-
import { useEffect, useState } from 'react'
|
|
10
|
+
import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
|
|
11
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
12
|
+
import { useChatContext } from '../contexts/chat_context'
|
|
9
13
|
import { ApiResource } from '../types'
|
|
10
|
-
import {
|
|
14
|
+
import { Client } from '../utils'
|
|
11
15
|
|
|
12
16
|
interface JoltResponse {
|
|
13
17
|
type: 'JoltToken'
|
|
@@ -16,11 +20,18 @@ interface JoltResponse {
|
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
export const useJoltClient = (): JoltClient | undefined => {
|
|
19
|
-
const
|
|
23
|
+
const { session } = useChatContext()
|
|
24
|
+
const queryClient = useQueryClient()
|
|
25
|
+
const apiClient = useMemo(
|
|
26
|
+
// Client that does not relay 401 errors
|
|
27
|
+
() => new Client({ app: 'chat', session, version: '2018-11-01' }),
|
|
28
|
+
[session]
|
|
29
|
+
)
|
|
30
|
+
|
|
20
31
|
const { data: joltToken } = useSuspenseQuery<ApiResource<JoltResponse>>({
|
|
21
32
|
queryKey: ['jolt-token'],
|
|
22
33
|
queryFn: () => {
|
|
23
|
-
return apiClient.
|
|
34
|
+
return apiClient.post({
|
|
24
35
|
url: '/me/jolt_authorize',
|
|
25
36
|
data: {
|
|
26
37
|
data: {
|
|
@@ -32,12 +43,27 @@ export const useJoltClient = (): JoltClient | undefined => {
|
|
|
32
43
|
},
|
|
33
44
|
})
|
|
34
45
|
|
|
35
|
-
const
|
|
36
|
-
return
|
|
46
|
+
const fetchJoltToken = async () => {
|
|
47
|
+
return apiClient.post<ApiResource<JoltResponse>>({
|
|
48
|
+
url: '/me/jolt_authorize',
|
|
49
|
+
data: {
|
|
50
|
+
data: {
|
|
51
|
+
type: 'JoltToken',
|
|
52
|
+
attributes: {},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
})
|
|
37
56
|
}
|
|
38
57
|
|
|
39
|
-
const
|
|
40
|
-
return
|
|
58
|
+
const fetchAuthTokenFn: FetchAuthToken = () => {
|
|
59
|
+
return queryClient.fetchQuery({
|
|
60
|
+
queryKey: ['jolt-token'],
|
|
61
|
+
queryFn: () => fetchJoltToken().then(res => res.data.id),
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const fetchSubscribeToken: FetchSubscribeToken = (channel: string, connectionId: string) => {
|
|
66
|
+
return apiClient
|
|
41
67
|
.post<ApiResource<JoltResponse>>({
|
|
42
68
|
url: '/me/jolt_subscribe',
|
|
43
69
|
data: {
|
|
@@ -48,6 +74,21 @@ export const useJoltClient = (): JoltClient | undefined => {
|
|
|
48
74
|
},
|
|
49
75
|
})
|
|
50
76
|
.then(res => res.data.id)
|
|
77
|
+
.catch((res: unknown) => {
|
|
78
|
+
console.error('failed to subscribe to Jolt channel', res)
|
|
79
|
+
return ''
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const fetchSubscribeTokenFn: FetchSubscribeToken = (
|
|
84
|
+
channel: string,
|
|
85
|
+
connectionId: string,
|
|
86
|
+
options
|
|
87
|
+
) => {
|
|
88
|
+
return queryClient.fetchQuery({
|
|
89
|
+
queryKey: ['jolt-subscribe-token', channel, connectionId],
|
|
90
|
+
queryFn: () => fetchSubscribeToken(channel, connectionId, options),
|
|
91
|
+
})
|
|
51
92
|
}
|
|
52
93
|
|
|
53
94
|
const { data: joltClient } = useQuery({
|
|
@@ -3,6 +3,7 @@ import React, { useMemo } from 'react'
|
|
|
3
3
|
import { StyleSheet, View } from 'react-native'
|
|
4
4
|
import { Heading, TextButton, ToggleButton } from '../../../components'
|
|
5
5
|
import { useTheme } from '../../../hooks'
|
|
6
|
+
import { useMarkAllRead } from '../../../hooks/use_conversations_actions'
|
|
6
7
|
import { useCanDisplayGroups } from '../../../hooks/use_groups'
|
|
7
8
|
import { ConversationScreenProps } from '../conversations_screen'
|
|
8
9
|
import { ChatGroupBadge } from './chat_group_badge'
|
|
@@ -22,6 +23,8 @@ export const ListHeaderComponent = () => {
|
|
|
22
23
|
const route = useRoute<RouteProp<ConversationScreenProps['route']>>()
|
|
23
24
|
const { chat_group_graph_id, group_source_app_name = '' } = route.params || {}
|
|
24
25
|
|
|
26
|
+
const { markAllRead, isPending } = useMarkAllRead()
|
|
27
|
+
|
|
25
28
|
const active: FilterTypes = useMemo(() => {
|
|
26
29
|
if (chat_group_graph_id) {
|
|
27
30
|
return FilterTypes.More
|
|
@@ -42,7 +45,9 @@ export const ListHeaderComponent = () => {
|
|
|
42
45
|
<Heading numberOfLines={1} variant="h2">
|
|
43
46
|
Conversations
|
|
44
47
|
</Heading>
|
|
45
|
-
<TextButton
|
|
48
|
+
<TextButton onPress={() => markAllRead()} disabled={isPending}>
|
|
49
|
+
Mark all read
|
|
50
|
+
</TextButton>
|
|
46
51
|
</View>
|
|
47
52
|
<View style={styles.filterRow}>
|
|
48
53
|
{showAppFilters && (
|
|
@@ -73,6 +73,28 @@ export function updateOrCreateRecordInPagesData<T extends ResourceObject>({
|
|
|
73
73
|
return { ...data, pages: newPages }
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* updateOrCreateRecordInPagesData
|
|
78
|
+
* Updates record if found in the cache, otherwise inserts.
|
|
79
|
+
*/
|
|
80
|
+
export function updateAllRecordsInPagesData<T extends ResourceObject>({
|
|
81
|
+
data,
|
|
82
|
+
processRecord = r => r,
|
|
83
|
+
}: {
|
|
84
|
+
data?: { pages: ApiCollection<T>[]; pageParams: any }
|
|
85
|
+
processRecord?: (_next: T) => T
|
|
86
|
+
}) {
|
|
87
|
+
if (!data) return data
|
|
88
|
+
|
|
89
|
+
const newPages = data.pages.map(page => {
|
|
90
|
+
const newData = page.data.map(processRecord)
|
|
91
|
+
|
|
92
|
+
return { ...page, data: newData }
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return { ...data, pages: newPages }
|
|
96
|
+
}
|
|
97
|
+
|
|
76
98
|
export function addRecordInPagesData<T extends ResourceObject>({
|
|
77
99
|
data,
|
|
78
100
|
record,
|