@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.
Files changed (60) hide show
  1. package/build/components/conversations/conversation_actions.d.ts.map +1 -1
  2. package/build/components/conversations/conversation_actions.js +14 -15
  3. package/build/components/conversations/conversation_actions.js.map +1 -1
  4. package/build/components/conversations/conversations.d.ts.map +1 -1
  5. package/build/components/conversations/conversations.js +5 -8
  6. package/build/components/conversations/conversations.js.map +1 -1
  7. package/build/contexts/chat_context.d.ts +1 -0
  8. package/build/contexts/chat_context.d.ts.map +1 -1
  9. package/build/contexts/chat_context.js +3 -0
  10. package/build/contexts/chat_context.js.map +1 -1
  11. package/build/contexts/conversations_context.d.ts.map +1 -1
  12. package/build/contexts/conversations_context.js +3 -12
  13. package/build/contexts/conversations_context.js.map +1 -1
  14. package/build/hooks/use_conversations_actions.d.ts +221 -0
  15. package/build/hooks/use_conversations_actions.d.ts.map +1 -0
  16. package/build/hooks/use_conversations_actions.js +93 -0
  17. package/build/hooks/use_conversations_actions.js.map +1 -0
  18. package/build/hooks/use_conversations_cache.d.ts +18 -0
  19. package/build/hooks/use_conversations_cache.d.ts.map +1 -0
  20. package/build/hooks/{use_conversation_jolt_events.js → use_conversations_cache.js} +27 -17
  21. package/build/hooks/use_conversations_cache.js.map +1 -0
  22. package/build/hooks/use_conversations_jolt_events.d.ts +3 -0
  23. package/build/hooks/use_conversations_jolt_events.d.ts.map +1 -0
  24. package/build/hooks/use_conversations_jolt_events.js +12 -0
  25. package/build/hooks/use_conversations_jolt_events.js.map +1 -0
  26. package/build/hooks/use_jolt.d.ts.map +1 -1
  27. package/build/hooks/use_jolt.js +39 -10
  28. package/build/hooks/use_jolt.js.map +1 -1
  29. package/build/screens/conversations/components/list_header_component.d.ts.map +1 -1
  30. package/build/screens/conversations/components/list_header_component.js +5 -1
  31. package/build/screens/conversations/components/list_header_component.js.map +1 -1
  32. package/build/utils/cache/page_mutations.d.ts +18 -0
  33. package/build/utils/cache/page_mutations.d.ts.map +1 -1
  34. package/build/utils/cache/page_mutations.js +13 -0
  35. package/build/utils/cache/page_mutations.js.map +1 -1
  36. package/build/utils/request/conversation.d.ts +1 -3
  37. package/build/utils/request/conversation.d.ts.map +1 -1
  38. package/build/utils/request/conversation.js +37 -30
  39. package/build/utils/request/conversation.js.map +1 -1
  40. package/package.json +2 -2
  41. package/src/__tests__/utils/cache/page_mutations.ts +49 -15
  42. package/src/components/conversations/conversation_actions.tsx +21 -17
  43. package/src/components/conversations/conversations.tsx +23 -26
  44. package/src/contexts/chat_context.tsx +4 -0
  45. package/src/contexts/conversations_context.tsx +3 -13
  46. package/src/hooks/use_conversations_actions.ts +108 -0
  47. package/src/hooks/{use_conversation_jolt_events.ts → use_conversations_cache.ts} +35 -20
  48. package/src/hooks/use_conversations_jolt_events.ts +21 -0
  49. package/src/hooks/use_jolt.ts +51 -10
  50. package/src/screens/conversations/components/list_header_component.tsx +6 -1
  51. package/src/utils/cache/page_mutations.ts +22 -0
  52. package/src/utils/request/conversation.ts +39 -34
  53. package/build/contexts/swipeable_active_conversation.d.ts +0 -11
  54. package/build/contexts/swipeable_active_conversation.d.ts.map +0 -1
  55. package/build/contexts/swipeable_active_conversation.js +0 -16
  56. package/build/contexts/swipeable_active_conversation.js.map +0 -1
  57. package/build/hooks/use_conversation_jolt_events.d.ts +0 -2
  58. package/build/hooks/use_conversation_jolt_events.d.ts.map +0 -1
  59. package/build/hooks/use_conversation_jolt_events.js.map +0 -1
  60. 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('updateRecordInPagesData', () => {
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 = updateRecordInPagesData({ data, record })
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 = updateRecordInPagesData<typeof record>({
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, View, StyleSheet, ViewStyle, Pressable } from 'react-native'
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 { ActionToggleButton } from './action_toggle_button'
8
- import { tokens } from '../../vendor/tapestry/tokens'
8
+ import {
9
+ useConversationsMarkRead,
10
+ useConversationsMute,
11
+ } from '../../hooks/use_conversations_actions'
9
12
  import { ConversationResource } from '../../types'
10
- import { useConversationActionsContext } from '../../contexts/swipeable_active_conversation'
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 } = useConversationActionsContext()
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.bellMuted', label: 'Mute' },
84
- false: { iconName: 'general.bell', label: 'Unmute' },
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
- setLatestMessageUnread(!latestMessageUnread)
102
+ markRead(!read)
99
103
  onClose()
100
- }, [latestMessageUnread, onClose])
104
+ }, [read, onClose, markRead])
101
105
 
102
106
  return (
103
107
  <View style={styles.actionButtonContainer}>
104
108
  <ActionToggleButton
105
- loading={false} // TODO: Might need this when there is data fetching
109
+ loading={markReadPending}
106
110
  disabled={emptyConversation}
107
- toggled={latestMessageUnread}
108
- onPress={() => handleLatestMessageUnread()}
111
+ toggled={!read}
112
+ onPress={handleLatestMessageUnread}
109
113
  toggleContent={latestMessageUnreadToggleContent}
110
114
  backgroundColor={tokens.fillColorInteractionSwipeDefault}
111
115
  />
112
116
  <ActionToggleButton
113
- loading={false} // TODO: Might need this when there is data fetching
117
+ loading={isPending}
114
118
  toggled={muted}
115
- onPress={() => handleMute()}
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
- <ConversationActionsProvider>
36
- <View style={styles.container}>
37
- <FlashList
38
- data={conversations}
39
- estimatedItemSize={97}
40
- contentContainerStyle={styles.contentContainer}
41
- onRefresh={refetch}
42
- refreshing={!isFetched && isRefetching}
43
- ListHeaderComponent={ListHeaderComponent}
44
- ListEmptyComponent={
45
- <View style={styles.listEmpty}>
46
- <Text variant="secondary">No conversations found</Text>
47
- </View>
48
- }
49
- renderItem={({ item }) => (
50
- <ConversationPreview
51
- conversation={item}
52
- onPress={() => navigation.navigate('Conversation', { conversation_id: item.id })}
53
- showBadges={showBadges}
54
- />
55
- )}
56
- onEndReached={() => fetchNextPage()}
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/use_conversation_jolt_events'
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 filter = useMemo(() => {
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 { deleteRecordInPagesData, updateOrCreateRecordInPagesData } from '../utils'
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 useConversationsJoltEvents() {
14
+ export function useConversationsCache(args?: Partial<ConversationRequestArgs>) {
15
15
  const apiClient = useApiClient()
16
16
  const queryClient = useQueryClient()
17
- const currentPerson = useCurrentPerson()
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 handleConversationUpdateOrCreate = async (e: JoltConversationEvent) => {
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 handleConversationDestroy = (e: ConversationDeletedEvent) => {
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: e.data.data.id },
64
+ record: { id },
56
65
  })
57
66
  )
58
67
  }
59
68
 
60
- useJoltEvent(joltChannel, 'conversation.updated', handleConversationUpdateOrCreate)
61
- useJoltEvent(joltChannel, 'conversation.created', handleConversationUpdateOrCreate)
62
- useJoltEvent(joltChannel, 'conversation.destroyed', handleConversationDestroy)
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
+ }
@@ -1,13 +1,17 @@
1
1
  import JoltClient from '@planningcenter/jolt-client'
2
- import { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'
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 { useApiClient } from './use_api_client'
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 apiClient = useApiClient()
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.chat.post({
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 fetchAuthTokenFn = async () => {
36
- return joltToken.data.id || ''
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 fetchSubscribeTokenFn: FetchSubscribeToken = (channel: string, connectionId: string) => {
40
- return apiClient.chat
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>Mark all read</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,