@planningcenter/chat-react-native 4.4.1-qa-staging.26 → 4.4.1-qa-staging.27
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/conversations.d.ts.map +1 -1
- package/build/components/conversations/conversations.js +3 -2
- package/build/components/conversations/conversations.js.map +1 -1
- package/build/components/display/filter_dropdown.d.ts.map +1 -1
- package/build/components/display/filter_dropdown.js +3 -1
- package/build/components/display/filter_dropdown.js.map +1 -1
- package/build/components/display/index.d.ts +1 -0
- package/build/components/display/index.d.ts.map +1 -1
- package/build/components/display/index.js +1 -0
- package/build/components/display/index.js.map +1 -1
- package/build/components/display/popover_menu_view.d.ts +8 -0
- package/build/components/display/popover_menu_view.d.ts.map +1 -0
- package/build/components/display/popover_menu_view.js +482 -0
- package/build/components/display/popover_menu_view.js.map +1 -0
- package/build/contexts/conversations_context.d.ts.map +1 -1
- package/build/contexts/conversations_context.js +20 -17
- package/build/contexts/conversations_context.js.map +1 -1
- package/build/hooks/use_clear_conversation_filters.d.ts.map +1 -1
- package/build/hooks/use_clear_conversation_filters.js +2 -1
- package/build/hooks/use_clear_conversation_filters.js.map +1 -1
- package/build/hooks/use_conversations_cache.d.ts +1 -1
- package/build/hooks/use_conversations_cache.d.ts.map +1 -1
- package/build/hooks/use_conversations_cache.js +64 -55
- package/build/hooks/use_conversations_cache.js.map +1 -1
- package/build/hooks/use_conversations_jolt_events.js +2 -2
- package/build/hooks/use_conversations_jolt_events.js.map +1 -1
- package/build/hooks/use_jolt.d.ts.map +1 -1
- package/build/hooks/use_jolt.js +37 -9
- package/build/hooks/use_jolt.js.map +1 -1
- package/build/screens/conversation_filters/context/conversation_filter_context.d.ts.map +1 -1
- package/build/screens/conversation_filters/context/conversation_filter_context.js +8 -4
- package/build/screens/conversation_filters/context/conversation_filter_context.js.map +1 -1
- package/build/screens/conversation_filters/screen_props.d.ts +1 -0
- package/build/screens/conversation_filters/screen_props.d.ts.map +1 -1
- package/build/screens/conversation_filters/screen_props.js.map +1 -1
- package/build/screens/conversations/components/filter_dropdown_list_header_component.d.ts.map +1 -1
- package/build/screens/conversations/components/filter_dropdown_list_header_component.js +211 -188
- package/build/screens/conversations/components/filter_dropdown_list_header_component.js.map +1 -1
- package/build/screens/conversations/conversations_screen.d.ts +1 -0
- package/build/screens/conversations/conversations_screen.d.ts.map +1 -1
- package/build/screens/conversations/conversations_screen.js +1 -2
- package/build/screens/conversations/conversations_screen.js.map +1 -1
- package/build/screens/design_system_screen.d.ts.map +1 -1
- package/build/screens/design_system_screen.js +289 -201
- package/build/screens/design_system_screen.js.map +1 -1
- package/build/utils/native_adapters/menu.d.ts +1 -2
- package/build/utils/native_adapters/menu.d.ts.map +1 -1
- package/build/utils/native_adapters/menu.js +2 -41
- package/build/utils/native_adapters/menu.js.map +1 -1
- package/build/utils/request/conversation.d.ts +2 -1
- package/build/utils/request/conversation.d.ts.map +1 -1
- package/build/utils/request/conversation.js +4 -1
- package/build/utils/request/conversation.js.map +1 -1
- package/package.json +3 -3
- package/src/components/conversations/__tests__/conversations_blank_state.test.tsx +1 -0
- package/src/components/conversations/conversations.tsx +12 -2
- package/src/components/display/__tests__/filter_dropdown.test.tsx +18 -2
- package/src/components/display/__tests__/popover_menu_view.test.tsx +63 -0
- package/src/components/display/filter_dropdown.tsx +4 -2
- package/src/components/display/index.ts +1 -0
- package/src/components/display/popover_menu_view.tsx +239 -0
- package/src/contexts/conversations_context.tsx +8 -1
- package/src/hooks/__tests__/use_conversations_jolt_events.test.tsx +135 -0
- package/src/hooks/__tests__/use_jolt.test.tsx +76 -1
- package/src/hooks/use_clear_conversation_filters.ts +1 -0
- package/src/hooks/use_conversations_cache.ts +29 -24
- package/src/hooks/use_conversations_jolt_events.ts +2 -2
- package/src/hooks/use_jolt.ts +26 -2
- package/src/screens/conversation_filters/context/conversation_filter_context.tsx +4 -0
- package/src/screens/conversation_filters/screen_props.ts +1 -0
- package/src/screens/conversations/__tests__/conversations_screen.test.tsx +2 -12
- package/src/screens/conversations/components/__tests__/filter_dropdown_list_header_component.test.tsx +55 -3
- package/src/screens/conversations/components/filter_dropdown_list_header_component.tsx +17 -2
- package/src/screens/conversations/conversations_screen.tsx +2 -2
- package/src/screens/design_system_screen.tsx +34 -1
- package/src/utils/__tests__/conversation.test.ts +1 -0
- package/src/utils/native_adapters/menu.tsx +4 -18
- package/src/utils/request/conversation.ts +4 -0
- package/src/utils/native_adapters/__tests__/menu.test.tsx +0 -46
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { InfiniteData, QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
+
import { renderHook, waitFor } from '@testing-library/react-native'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { buildTestQueryClient } from '../../__utils__/query_client'
|
|
5
|
+
import { ConversationFiltersParams } from '../../screens/conversation_filters/screen_props'
|
|
6
|
+
import type { ApiCollection, ConversationResource } from '../../types'
|
|
7
|
+
import { getConversationsRequestArgs } from '../../utils/request/conversation'
|
|
8
|
+
import { useConversationsJoltEvents } from '../use_conversations_jolt_events'
|
|
9
|
+
import { getRequestQueryKey } from '../use_suspense_api'
|
|
10
|
+
|
|
11
|
+
const handlers: Record<string, (event: unknown) => void> = {}
|
|
12
|
+
jest.mock('../use_jolt', () => ({
|
|
13
|
+
useJoltChannel: () => 'channel',
|
|
14
|
+
useJoltEvent: (_channel: unknown, name: string, handler: (event: unknown) => void) => {
|
|
15
|
+
handlers[name] = handler
|
|
16
|
+
},
|
|
17
|
+
}))
|
|
18
|
+
jest.mock('../use_api', () => ({ useApiGet: () => ({ data: { id: 1 } }) }))
|
|
19
|
+
jest.mock('../use_current_person', () => ({
|
|
20
|
+
currentPersonRequestArgs: {},
|
|
21
|
+
useCurrentPersonCache: () => ({ invalidate: jest.fn() }),
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
const mockGet = jest.fn()
|
|
25
|
+
jest.mock('../use_api_client', () => ({ useApiClient: () => ({ chat: { get: mockGet } }) }))
|
|
26
|
+
|
|
27
|
+
let mockContextArgs: ConversationFiltersParams = {}
|
|
28
|
+
jest.mock('../../contexts/conversations_context', () => ({
|
|
29
|
+
useConversationsContext: () => ({ args: mockContextArgs }),
|
|
30
|
+
}))
|
|
31
|
+
|
|
32
|
+
type QueryData = InfiniteData<ApiCollection<ConversationResource>>
|
|
33
|
+
|
|
34
|
+
const conversationId = 7
|
|
35
|
+
let queryClient: QueryClient
|
|
36
|
+
|
|
37
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
38
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const queryKey = () => getRequestQueryKey(getConversationsRequestArgs(mockContextArgs))
|
|
42
|
+
|
|
43
|
+
const seedList = (conversations: Partial<ConversationResource>[] = []) =>
|
|
44
|
+
queryClient.setQueryData<QueryData>(queryKey(), {
|
|
45
|
+
pages: [
|
|
46
|
+
{
|
|
47
|
+
data: conversations,
|
|
48
|
+
links: {},
|
|
49
|
+
meta: {},
|
|
50
|
+
} as unknown as ApiCollection<ConversationResource>,
|
|
51
|
+
],
|
|
52
|
+
pageParams: [undefined],
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const cachedIds = () =>
|
|
56
|
+
queryClient
|
|
57
|
+
.getQueryData<QueryData>(queryKey())!
|
|
58
|
+
.pages.flatMap(page => page.data.map(conversation => conversation.id))
|
|
59
|
+
|
|
60
|
+
const serverReturns = (conversations: Partial<ConversationResource>[]) =>
|
|
61
|
+
mockGet.mockResolvedValue({
|
|
62
|
+
data: conversations.map(conversation => ({
|
|
63
|
+
type: 'Conversation',
|
|
64
|
+
id: conversationId,
|
|
65
|
+
title: 'Youth',
|
|
66
|
+
...conversation,
|
|
67
|
+
})),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const conversationUpdated = () =>
|
|
71
|
+
handlers['conversation.updated']({
|
|
72
|
+
event: 'conversation.updated',
|
|
73
|
+
data: { data: { id: conversationId, last_message_author_id: 2 } },
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const renderWithFilter = (args: ConversationFiltersParams) => {
|
|
77
|
+
mockContextArgs = args
|
|
78
|
+
queryClient = buildTestQueryClient({ defaultOptions: { queries: { gcTime: Infinity } } })
|
|
79
|
+
renderHook(() => useConversationsJoltEvents(), { wrapper })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
beforeEach(() => mockGet.mockReset())
|
|
83
|
+
|
|
84
|
+
describe('useConversationsJoltEvents on conversation.updated', () => {
|
|
85
|
+
it('asks the server for the conversation under the active filter', async () => {
|
|
86
|
+
renderWithFilter({ show_unread: true })
|
|
87
|
+
seedList()
|
|
88
|
+
serverReturns([])
|
|
89
|
+
|
|
90
|
+
conversationUpdated()
|
|
91
|
+
|
|
92
|
+
await waitFor(() => expect(mockGet).toHaveBeenCalled())
|
|
93
|
+
expect(mockGet).toHaveBeenCalledWith(
|
|
94
|
+
expect.objectContaining({
|
|
95
|
+
url: '/me/conversations',
|
|
96
|
+
data: expect.objectContaining({
|
|
97
|
+
filter: 'mine_or_not_empty,unread',
|
|
98
|
+
where: { id: conversationId },
|
|
99
|
+
perPage: 1,
|
|
100
|
+
}),
|
|
101
|
+
})
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('adds a conversation the server returns for the list', async () => {
|
|
106
|
+
renderWithFilter({ show_unread: true })
|
|
107
|
+
seedList()
|
|
108
|
+
serverReturns([{ unreadCount: 1 }])
|
|
109
|
+
|
|
110
|
+
conversationUpdated()
|
|
111
|
+
|
|
112
|
+
await waitFor(() => expect(cachedIds()).toEqual([conversationId]))
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('removes a conversation the server leaves out of the list', async () => {
|
|
116
|
+
renderWithFilter({ show_unread: true })
|
|
117
|
+
seedList([{ id: conversationId, unreadCount: 1 }])
|
|
118
|
+
serverReturns([])
|
|
119
|
+
|
|
120
|
+
conversationUpdated()
|
|
121
|
+
|
|
122
|
+
await waitFor(() => expect(cachedIds()).toEqual([]))
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('keeps the list as it was when the fetch fails', async () => {
|
|
126
|
+
renderWithFilter({ show_unread: true })
|
|
127
|
+
seedList([{ id: conversationId, unreadCount: 1 }])
|
|
128
|
+
mockGet.mockRejectedValue(new Error('offline'))
|
|
129
|
+
|
|
130
|
+
conversationUpdated()
|
|
131
|
+
|
|
132
|
+
await waitFor(() => expect(mockGet).toHaveBeenCalled())
|
|
133
|
+
expect(cachedIds()).toEqual([conversationId])
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -9,7 +9,7 @@ import { ChatContext, ChatContextValue } from '../../contexts/chat_context'
|
|
|
9
9
|
import { Session } from '../../utils'
|
|
10
10
|
import { Client } from '../../utils/client'
|
|
11
11
|
import { ResponseError } from '../../utils/response_error'
|
|
12
|
-
import { useJoltClient, useJoltForegroundPing } from '../use_jolt'
|
|
12
|
+
import { useJoltChannel, useJoltClient, useJoltEvent, useJoltForegroundPing } from '../use_jolt'
|
|
13
13
|
|
|
14
14
|
let caught: unknown
|
|
15
15
|
|
|
@@ -167,3 +167,78 @@ describe('useJoltForegroundPing', () => {
|
|
|
167
167
|
expect(ping).toHaveBeenCalledTimes(1)
|
|
168
168
|
})
|
|
169
169
|
})
|
|
170
|
+
|
|
171
|
+
describe('useJoltChannel', () => {
|
|
172
|
+
type Listener = (_event: unknown) => void
|
|
173
|
+
const channelName = 'chat.people.1'
|
|
174
|
+
|
|
175
|
+
const buildJolt = () => {
|
|
176
|
+
const listeners: Listener[] = []
|
|
177
|
+
const subscription = {
|
|
178
|
+
channel: channelName,
|
|
179
|
+
bind: jest.fn((_event: string, listener: Listener) => {
|
|
180
|
+
listeners.push(listener)
|
|
181
|
+
return () => listeners.splice(listeners.indexOf(listener), 1)
|
|
182
|
+
}),
|
|
183
|
+
}
|
|
184
|
+
const jolt = {
|
|
185
|
+
subscriptions: [] as (typeof subscription)[],
|
|
186
|
+
subscribe: jest.fn(() => {
|
|
187
|
+
jolt.subscriptions.push(subscription)
|
|
188
|
+
return subscription
|
|
189
|
+
}),
|
|
190
|
+
// The real client unbinds every listener on the subscription when it unsubscribes.
|
|
191
|
+
unsubscribe: jest.fn(() => {
|
|
192
|
+
listeners.length = 0
|
|
193
|
+
jolt.subscriptions = []
|
|
194
|
+
}),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { jolt, emit: (event: unknown) => listeners.forEach(listener => listener(event)) }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const ChannelProbe = ({ onEvent }: { onEvent: Listener }) => {
|
|
201
|
+
const channel = useJoltChannel(channelName)
|
|
202
|
+
useJoltEvent(channel, 'conversation.updated', onEvent)
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const renderConsumers = (jolt: unknown, onEvent: Listener) => {
|
|
207
|
+
const session = new Session({ token: { access_token: 'a-real-token' } })
|
|
208
|
+
const queryClient = buildTestQueryClient({ defaultOptions: { queries: { gcTime: Infinity } } })
|
|
209
|
+
queryClient.setQueryData(['wss-url'], 'wss://jolt.test')
|
|
210
|
+
queryClient.setQueryData(['jolt-client', 'wss://jolt.test', 'a-real-token'], jolt)
|
|
211
|
+
|
|
212
|
+
const tree = (count: number) => (
|
|
213
|
+
<QueryClientProvider client={queryClient}>
|
|
214
|
+
<ChatContext.Provider value={{ session } as unknown as ChatContextValue}>
|
|
215
|
+
<Suspense fallback={null}>
|
|
216
|
+
{Array.from({ length: count }, (_, index) => (
|
|
217
|
+
<ChannelProbe key={index} onEvent={onEvent} />
|
|
218
|
+
))}
|
|
219
|
+
</Suspense>
|
|
220
|
+
</ChatContext.Provider>
|
|
221
|
+
</QueryClientProvider>
|
|
222
|
+
)
|
|
223
|
+
const result = render(tree(2))
|
|
224
|
+
|
|
225
|
+
return { setConsumers: (count: number) => result.rerender(tree(count)) }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
it('keeps delivering events to the remaining consumer after another unmounts', async () => {
|
|
229
|
+
const { jolt, emit } = buildJolt()
|
|
230
|
+
const onEvent = jest.fn()
|
|
231
|
+
const { setConsumers } = renderConsumers(jolt, onEvent)
|
|
232
|
+
await waitFor(() => expect(jolt.subscribe).toHaveBeenCalledTimes(1))
|
|
233
|
+
|
|
234
|
+
setConsumers(1)
|
|
235
|
+
emit({ event: 'conversation.updated' })
|
|
236
|
+
|
|
237
|
+
expect(jolt.unsubscribe).not.toHaveBeenCalled()
|
|
238
|
+
expect(onEvent).toHaveBeenCalledTimes(1)
|
|
239
|
+
|
|
240
|
+
setConsumers(0)
|
|
241
|
+
|
|
242
|
+
expect(jolt.unsubscribe).toHaveBeenCalledWith(channelName)
|
|
243
|
+
})
|
|
244
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { InfiniteData, useQueryClient } from '@tanstack/react-query'
|
|
2
|
-
import { ApiCollection,
|
|
2
|
+
import { ApiCollection, ConversationResource } from '../types'
|
|
3
3
|
import {
|
|
4
4
|
deleteRecordInPagesData,
|
|
5
5
|
updateRecordInPagesData,
|
|
@@ -21,16 +21,13 @@ export function useConversationsCache(args?: Partial<ConversationRequestArgs>) {
|
|
|
21
21
|
const fetchConversation = async (id: number) => {
|
|
22
22
|
if (!id) return
|
|
23
23
|
|
|
24
|
-
const { data
|
|
25
|
-
const { data } = await apiClient.chat.get<
|
|
26
|
-
url
|
|
27
|
-
data: {
|
|
28
|
-
fields: argsData.fields,
|
|
29
|
-
include: argsData.include,
|
|
30
|
-
},
|
|
24
|
+
const { url, data } = conversationsRequestArgs
|
|
25
|
+
const { data: conversations } = await apiClient.chat.get<ApiCollection<ConversationResource>>({
|
|
26
|
+
url,
|
|
27
|
+
data: { ...data, where: { ...data.where, id }, perPage: 1 },
|
|
31
28
|
})
|
|
32
29
|
|
|
33
|
-
return
|
|
30
|
+
return conversations[0]
|
|
34
31
|
}
|
|
35
32
|
|
|
36
33
|
const updateOrCreate = async (conversation: ConversationResource) => {
|
|
@@ -70,20 +67,6 @@ export function useConversationsCache(args?: Partial<ConversationRequestArgs>) {
|
|
|
70
67
|
)
|
|
71
68
|
}
|
|
72
69
|
|
|
73
|
-
const fetchAndUpdate = async ({ id }: { id: number }) => {
|
|
74
|
-
const conversation: ConversationResource = await fetchConversation(id).catch(c => c)
|
|
75
|
-
if (!conversation.id) return
|
|
76
|
-
|
|
77
|
-
updateOnly(conversation)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const fetchAndUpdateOrCreate = async ({ id }: { id: number }) => {
|
|
81
|
-
const conversation: ConversationResource = await fetchConversation(id).catch(c => c)
|
|
82
|
-
if (!conversation.id) return
|
|
83
|
-
|
|
84
|
-
updateOrCreate(conversation)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
70
|
const handleConversationDestroy = ({ id }: { id: number }) => {
|
|
88
71
|
if (!id) return
|
|
89
72
|
|
|
@@ -95,10 +78,32 @@ export function useConversationsCache(args?: Partial<ConversationRequestArgs>) {
|
|
|
95
78
|
)
|
|
96
79
|
}
|
|
97
80
|
|
|
81
|
+
const fetchAndUpdate = async ({ id }: { id: number }) => {
|
|
82
|
+
const conversation = await fetchConversation(id).catch(() => undefined)
|
|
83
|
+
if (!conversation) return
|
|
84
|
+
|
|
85
|
+
updateOnly(conversation)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const fetchAndSync = async ({ id }: { id: number }) => {
|
|
89
|
+
let conversation: ConversationResource | undefined
|
|
90
|
+
try {
|
|
91
|
+
conversation = await fetchConversation(id)
|
|
92
|
+
} catch {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (conversation) {
|
|
97
|
+
updateOrCreate(conversation)
|
|
98
|
+
} else {
|
|
99
|
+
handleConversationDestroy({ id })
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
98
103
|
return {
|
|
99
104
|
create: updateOrCreate,
|
|
100
105
|
destroy: handleConversationDestroy,
|
|
101
|
-
|
|
106
|
+
fetchSync: fetchAndSync,
|
|
102
107
|
fetchUpdate: fetchAndUpdate,
|
|
103
108
|
invalidate: () => queryClient.invalidateQueries({ queryKey: conversationQueryKey }),
|
|
104
109
|
update: updateOrCreate,
|
|
@@ -31,10 +31,10 @@ export function useConversationsJoltEvents() {
|
|
|
31
31
|
idempotentKey: e.data.data.last_message_idempotent_key,
|
|
32
32
|
})
|
|
33
33
|
}
|
|
34
|
-
cache.
|
|
34
|
+
cache.fetchSync({ id: e.data.data.id })
|
|
35
35
|
})
|
|
36
36
|
useJoltEvent(joltChannel, 'conversation.created', (e: JoltConversationEvent) =>
|
|
37
|
-
cache.
|
|
37
|
+
cache.fetchSync({ id: e.data.data.id })
|
|
38
38
|
)
|
|
39
39
|
useJoltEvent(joltChannel, 'conversation.destroyed', (e: JoltConversationEvent) =>
|
|
40
40
|
cache.destroy({ id: e.data.data.id })
|
package/src/hooks/use_jolt.ts
CHANGED
|
@@ -181,6 +181,25 @@ export const useJoltForegroundPing = () => {
|
|
|
181
181
|
}, [queryClient])
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
const channelConsumers = new WeakMap<JoltClient, Map<string, number>>()
|
|
185
|
+
|
|
186
|
+
const retainChannel = (jolt: JoltClient, channelName: string) => {
|
|
187
|
+
const counts = channelConsumers.get(jolt) ?? new Map<string, number>()
|
|
188
|
+
channelConsumers.set(jolt, counts)
|
|
189
|
+
counts.set(channelName, (counts.get(channelName) ?? 0) + 1)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const releaseChannel = (jolt: JoltClient, channelName: string) => {
|
|
193
|
+
const counts = channelConsumers.get(jolt)
|
|
194
|
+
const remaining = (counts?.get(channelName) ?? 1) - 1
|
|
195
|
+
if (remaining > 0) {
|
|
196
|
+
counts?.set(channelName, remaining)
|
|
197
|
+
} else {
|
|
198
|
+
counts?.delete(channelName)
|
|
199
|
+
}
|
|
200
|
+
return remaining
|
|
201
|
+
}
|
|
202
|
+
|
|
184
203
|
export function useJoltChannel(
|
|
185
204
|
channelName: string,
|
|
186
205
|
enabled: boolean = true
|
|
@@ -216,8 +235,13 @@ export function useJoltChannel(
|
|
|
216
235
|
}, [queryClient, channelName, jolt])
|
|
217
236
|
|
|
218
237
|
useEffect(() => {
|
|
219
|
-
return
|
|
220
|
-
|
|
238
|
+
if (!jolt) return
|
|
239
|
+
|
|
240
|
+
retainChannel(jolt, channelName)
|
|
241
|
+
return () => {
|
|
242
|
+
if (releaseChannel(jolt, channelName) === 0) handleUnsubscribe()
|
|
243
|
+
}
|
|
244
|
+
}, [jolt, channelName, handleUnsubscribe])
|
|
221
245
|
|
|
222
246
|
return subscription
|
|
223
247
|
}
|
|
@@ -23,6 +23,7 @@ export const FilterContext = createContext<{
|
|
|
23
23
|
group_source_app_name: undefined,
|
|
24
24
|
show_direct_messages: undefined,
|
|
25
25
|
show_archived: undefined,
|
|
26
|
+
show_unread: undefined,
|
|
26
27
|
},
|
|
27
28
|
scrollOffset: 0,
|
|
28
29
|
setScrollOffset: () => {},
|
|
@@ -59,6 +60,7 @@ export const FilterProvider = ({ children }: PropsWithChildren) => {
|
|
|
59
60
|
group_source_app_name: undefined,
|
|
60
61
|
show_direct_messages: undefined,
|
|
61
62
|
show_archived: undefined,
|
|
63
|
+
show_unread: undefined,
|
|
62
64
|
})
|
|
63
65
|
)
|
|
64
66
|
},
|
|
@@ -68,6 +70,7 @@ export const FilterProvider = ({ children }: PropsWithChildren) => {
|
|
|
68
70
|
group_source_app_name: undefined,
|
|
69
71
|
show_direct_messages: undefined,
|
|
70
72
|
show_archived: undefined,
|
|
73
|
+
show_unread: undefined,
|
|
71
74
|
})
|
|
72
75
|
},
|
|
73
76
|
setAppFilter: (params: Omit<ConversationFiltersParams, 'chat_group_graph_id'>) => {
|
|
@@ -76,6 +79,7 @@ export const FilterProvider = ({ children }: PropsWithChildren) => {
|
|
|
76
79
|
group_source_app_name: params.group_source_app_name,
|
|
77
80
|
show_direct_messages: params.show_direct_messages,
|
|
78
81
|
show_archived: params.show_archived,
|
|
82
|
+
show_unread: undefined,
|
|
79
83
|
})
|
|
80
84
|
},
|
|
81
85
|
applyFilters: (params: ConversationFiltersParams) => {
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
ChatAdapters,
|
|
8
8
|
ClipboardAdapter,
|
|
9
9
|
ImagePickerAdapter,
|
|
10
|
-
MenuAdapter,
|
|
11
10
|
VideoAdapter,
|
|
12
11
|
} from '../../../utils/native_adapters'
|
|
13
12
|
import { ConversationsScreen } from '../conversations_screen'
|
|
@@ -41,7 +40,7 @@ jest.mock('../components/filter_dropdown_list_header_component', () => {
|
|
|
41
40
|
return { FilterDropdownListHeaderComponent: () => <Text>Filter dropdown header</Text> }
|
|
42
41
|
})
|
|
43
42
|
|
|
44
|
-
const configureAdapters = (
|
|
43
|
+
const configureAdapters = () => {
|
|
45
44
|
ChatAdapters.configure({
|
|
46
45
|
clipboard: new ClipboardAdapter({
|
|
47
46
|
getStringAsync: async () => '',
|
|
@@ -53,7 +52,6 @@ const configureAdapters = ({ menu }: { menu: boolean }) => {
|
|
|
53
52
|
openCameraAsync: async () => ({ canceled: true, assets: null }),
|
|
54
53
|
openImageLibraryAsync: async () => ({ canceled: true, assets: null }),
|
|
55
54
|
}),
|
|
56
|
-
...(menu ? { menu: new MenuAdapter({ View: () => null }) } : {}),
|
|
57
55
|
})
|
|
58
56
|
}
|
|
59
57
|
|
|
@@ -80,7 +78,7 @@ const renderWithFeatures = (enabledFeatures: string[]) => {
|
|
|
80
78
|
|
|
81
79
|
describe('ConversationsScreen header gating', () => {
|
|
82
80
|
beforeEach(() => {
|
|
83
|
-
configureAdapters(
|
|
81
|
+
configureAdapters()
|
|
84
82
|
})
|
|
85
83
|
|
|
86
84
|
it('shows the search header with only the QA flag on', async () => {
|
|
@@ -107,14 +105,6 @@ describe('ConversationsScreen header gating', () => {
|
|
|
107
105
|
expect(await view.findByText('Filter dropdown header')).toBeTruthy()
|
|
108
106
|
})
|
|
109
107
|
|
|
110
|
-
it('falls back to the search header when the host has no menu adapter', async () => {
|
|
111
|
-
configureAdapters({ menu: false })
|
|
112
|
-
|
|
113
|
-
const view = renderWithFeatures(['QA_MOBILE_filter_dropdown', 'ROLLOUT_MOBILE_search'])
|
|
114
|
-
|
|
115
|
-
expect(await view.findByText('Search header')).toBeTruthy()
|
|
116
|
-
})
|
|
117
|
-
|
|
118
108
|
it('shows the default header with neither flag on', async () => {
|
|
119
109
|
const view = renderWithFeatures(['ROLLOUT_WEB_search'])
|
|
120
110
|
|
|
@@ -173,7 +173,7 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
173
173
|
it('offers every filter the person can use, then More filters', () => {
|
|
174
174
|
render(<FilterDropdownListHeaderComponent />)
|
|
175
175
|
|
|
176
|
-
expect(menuItemNames()).toEqual(['All', 'Groups', 'Teams', 'DMs', 'More filters'])
|
|
176
|
+
expect(menuItemNames()).toEqual(['All', 'Groups', 'Teams', 'DMs', 'Unread', 'More filters'])
|
|
177
177
|
expect(screen.getByRole('button', { name: 'All conversations' })).toBeTruthy()
|
|
178
178
|
})
|
|
179
179
|
|
|
@@ -182,7 +182,7 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
182
182
|
|
|
183
183
|
render(<FilterDropdownListHeaderComponent />)
|
|
184
184
|
|
|
185
|
-
expect(menuItemNames()).toEqual(['All', 'DMs'])
|
|
185
|
+
expect(menuItemNames()).toEqual(['All', 'DMs', 'Unread'])
|
|
186
186
|
})
|
|
187
187
|
|
|
188
188
|
it('selecting Groups filters to group conversations', () => {
|
|
@@ -195,6 +195,45 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
195
195
|
)
|
|
196
196
|
})
|
|
197
197
|
|
|
198
|
+
it('selecting Unread filters to unread conversations', () => {
|
|
199
|
+
setRouteParams({ group_source_app_name: 'Groups' })
|
|
200
|
+
|
|
201
|
+
render(<FilterDropdownListHeaderComponent />)
|
|
202
|
+
|
|
203
|
+
fireEvent.press(screen.getByRole('menuitem', { name: 'Unread' }))
|
|
204
|
+
|
|
205
|
+
expect(mockSetParams).toHaveBeenCalledWith({
|
|
206
|
+
title: undefined,
|
|
207
|
+
chat_group_graph_id: undefined,
|
|
208
|
+
group_source_app_name: undefined,
|
|
209
|
+
show_direct_messages: undefined,
|
|
210
|
+
show_unread: true,
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('selecting another filter clears Unread', () => {
|
|
215
|
+
setRouteParams({ show_unread: true })
|
|
216
|
+
|
|
217
|
+
render(<FilterDropdownListHeaderComponent />)
|
|
218
|
+
|
|
219
|
+
fireEvent.press(screen.getByRole('menuitem', { name: 'DMs' }))
|
|
220
|
+
|
|
221
|
+
expect(mockSetParams).toHaveBeenCalledWith(
|
|
222
|
+
expect.objectContaining({ show_direct_messages: true, show_unread: undefined })
|
|
223
|
+
)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('labels the trigger Unread and checks it when filtered to unread', () => {
|
|
227
|
+
setRouteParams({ show_unread: true })
|
|
228
|
+
|
|
229
|
+
render(<FilterDropdownListHeaderComponent />)
|
|
230
|
+
|
|
231
|
+
expect(screen.getByRole('button', { name: 'Unread' })).toBeTruthy()
|
|
232
|
+
expect(screen.getByRole('menuitem', { name: 'Unread' }).props.accessibilityState.selected).toBe(
|
|
233
|
+
true
|
|
234
|
+
)
|
|
235
|
+
})
|
|
236
|
+
|
|
198
237
|
it('labels the trigger with the active filter and checks it', () => {
|
|
199
238
|
setRouteParams({ group_source_app_name: 'Services' })
|
|
200
239
|
|
|
@@ -230,7 +269,7 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
230
269
|
|
|
231
270
|
render(<FilterDropdownListHeaderComponent />)
|
|
232
271
|
|
|
233
|
-
expect(menuItemNames()).toEqual(['All', 'DMs', 'More filters'])
|
|
272
|
+
expect(menuItemNames()).toEqual(['All', 'DMs', 'Unread', 'More filters'])
|
|
234
273
|
})
|
|
235
274
|
|
|
236
275
|
it('labels the archived list and carries it into More filters', () => {
|
|
@@ -279,6 +318,19 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
279
318
|
expect(mockNavigate).toHaveBeenCalledWith('ConversationFilters', expect.any(Object))
|
|
280
319
|
})
|
|
281
320
|
|
|
321
|
+
it('More filters carries the Unread filter into the sheet', () => {
|
|
322
|
+
setRouteParams({ show_unread: true })
|
|
323
|
+
|
|
324
|
+
render(<FilterDropdownListHeaderComponent />)
|
|
325
|
+
|
|
326
|
+
fireEvent.press(screen.getByRole('menuitem', { name: 'More filters' }))
|
|
327
|
+
|
|
328
|
+
expect(mockNavigate).toHaveBeenCalledWith(
|
|
329
|
+
'ConversationFilters',
|
|
330
|
+
expect.objectContaining({ show_unread: true })
|
|
331
|
+
)
|
|
332
|
+
})
|
|
333
|
+
|
|
282
334
|
it('shows the search button only when search is enabled', () => {
|
|
283
335
|
mockSearchEnabled.mockReturnValue(false)
|
|
284
336
|
const view = render(<FilterDropdownListHeaderComponent />)
|
|
@@ -35,6 +35,7 @@ enum FilterTypes {
|
|
|
35
35
|
Groups = 'Groups',
|
|
36
36
|
Teams = 'Teams',
|
|
37
37
|
DMs = 'DMs',
|
|
38
|
+
Unread = 'Unread',
|
|
38
39
|
More = 'More',
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -43,6 +44,7 @@ const FILTER_PARAMS: Record<Exclude<FilterTypes, FilterTypes.More>, Conversation
|
|
|
43
44
|
[FilterTypes.Groups]: { group_source_app_name: 'Groups' },
|
|
44
45
|
[FilterTypes.Teams]: { group_source_app_name: 'Services' },
|
|
45
46
|
[FilterTypes.DMs]: { show_direct_messages: true },
|
|
47
|
+
[FilterTypes.Unread]: { show_unread: true },
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
export const FilterDropdownListHeaderComponent = () => {
|
|
@@ -61,6 +63,7 @@ export const FilterDropdownListHeaderComponent = () => {
|
|
|
61
63
|
group_source_app_name = '',
|
|
62
64
|
show_direct_messages,
|
|
63
65
|
show_archived,
|
|
66
|
+
show_unread,
|
|
64
67
|
} = route.params || {}
|
|
65
68
|
const currentPersonCache = useCurrentPersonCache()
|
|
66
69
|
const { markAllRead, isPending } = useMarkAllRead()
|
|
@@ -83,10 +86,12 @@ export const FilterDropdownListHeaderComponent = () => {
|
|
|
83
86
|
return FilterTypes.Teams
|
|
84
87
|
} else if (show_direct_messages) {
|
|
85
88
|
return FilterTypes.DMs
|
|
89
|
+
} else if (show_unread) {
|
|
90
|
+
return FilterTypes.Unread
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
return FilterTypes.All
|
|
89
|
-
}, [chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived])
|
|
94
|
+
}, [chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived, show_unread])
|
|
90
95
|
|
|
91
96
|
const options: FilterDropdownOption[] = useMemo(
|
|
92
97
|
() => [
|
|
@@ -94,6 +99,7 @@ export const FilterDropdownListHeaderComponent = () => {
|
|
|
94
99
|
...(canFilterByGroups ? [{ id: FilterTypes.Groups, label: FilterTypes.Groups }] : []),
|
|
95
100
|
...(canFilterByTeams ? [{ id: FilterTypes.Teams, label: FilterTypes.Teams }] : []),
|
|
96
101
|
...(canFilterByDMs ? [{ id: FilterTypes.DMs, label: FilterTypes.DMs }] : []),
|
|
102
|
+
{ id: FilterTypes.Unread, label: FilterTypes.Unread },
|
|
97
103
|
],
|
|
98
104
|
[canFilterByGroups, canFilterByTeams, canFilterByDMs]
|
|
99
105
|
)
|
|
@@ -144,6 +150,7 @@ export const FilterDropdownListHeaderComponent = () => {
|
|
|
144
150
|
group_source_app_name: undefined,
|
|
145
151
|
show_direct_messages: undefined,
|
|
146
152
|
show_archived: undefined,
|
|
153
|
+
show_unread: undefined,
|
|
147
154
|
...params,
|
|
148
155
|
})
|
|
149
156
|
},
|
|
@@ -172,8 +179,16 @@ export const FilterDropdownListHeaderComponent = () => {
|
|
|
172
179
|
group_source_app_name,
|
|
173
180
|
show_direct_messages,
|
|
174
181
|
show_archived,
|
|
182
|
+
show_unread,
|
|
175
183
|
})
|
|
176
|
-
}, [
|
|
184
|
+
}, [
|
|
185
|
+
navigation,
|
|
186
|
+
chat_group_graph_id,
|
|
187
|
+
group_source_app_name,
|
|
188
|
+
show_direct_messages,
|
|
189
|
+
show_archived,
|
|
190
|
+
show_unread,
|
|
191
|
+
])
|
|
177
192
|
|
|
178
193
|
const handleMarkAllRead = useCallback(() => {
|
|
179
194
|
currentPersonCache.update({ unreadCount: 0 })
|
|
@@ -8,7 +8,6 @@ import { analyticsEvents, usePublishProductAnalyticsEvent } from '../../hooks/us
|
|
|
8
8
|
import { useSearchEnabled } from '../../hooks/use_search_enabled'
|
|
9
9
|
import { AppName } from '../../types/resources/app_name'
|
|
10
10
|
import { GraphId } from '../../types/resources/group_resource'
|
|
11
|
-
import { Menu } from '../../utils/native_adapters'
|
|
12
11
|
import { FilterDropdownListHeaderComponent } from './components/filter_dropdown_list_header_component'
|
|
13
12
|
import { ListHeaderComponent } from './components/list_header_component'
|
|
14
13
|
import { SearchListHeaderComponent } from './components/search_list_header_component'
|
|
@@ -19,6 +18,7 @@ export type ConversationsScreenProps = StaticScreenProps<{
|
|
|
19
18
|
group_source_app_name?: AppName
|
|
20
19
|
show_direct_messages?: boolean
|
|
21
20
|
show_archived?: boolean
|
|
21
|
+
show_unread?: boolean
|
|
22
22
|
}>
|
|
23
23
|
|
|
24
24
|
export function ConversationsScreen({ route }: ConversationsScreenProps) {
|
|
@@ -40,7 +40,7 @@ const useListHeaderComponent = () => {
|
|
|
40
40
|
const filterDropdownEnabled = useFilterDropdownEnabled()
|
|
41
41
|
const searchEnabled = useSearchEnabled()
|
|
42
42
|
|
|
43
|
-
if (filterDropdownEnabled
|
|
43
|
+
if (filterDropdownEnabled) return FilterDropdownListHeaderComponent
|
|
44
44
|
if (searchEnabled) return SearchListHeaderComponent
|
|
45
45
|
|
|
46
46
|
return ListHeaderComponent
|