@planningcenter/chat-react-native 4.4.1-qa-staging.26 → 4.4.1-qa-staging.28
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 +199 -206
- 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 +57 -38
- package/src/screens/conversations/components/filter_dropdown_list_header_component.tsx +25 -27
- 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
|
|
|
@@ -21,15 +21,6 @@ const mockSearchEnabled = jest.fn()
|
|
|
21
21
|
const mockCanBrowseArchived = jest.fn()
|
|
22
22
|
const mockGroupsByGraphId: Record<string, { name: string }> = {}
|
|
23
23
|
|
|
24
|
-
jest.mock('react-native', () => {
|
|
25
|
-
const actual = jest.requireActual('react-native')
|
|
26
|
-
return Object.defineProperty(actual, 'useWindowDimensions', {
|
|
27
|
-
value: jest.fn(() => ({ fontScale: 1 })),
|
|
28
|
-
configurable: true,
|
|
29
|
-
})
|
|
30
|
-
})
|
|
31
|
-
const mockUseWindowDimensions = jest.requireMock('react-native').useWindowDimensions
|
|
32
|
-
|
|
33
24
|
jest.mock('@react-navigation/native', () => ({
|
|
34
25
|
...jest.requireActual('@react-navigation/native'),
|
|
35
26
|
useNavigation: () => ({ setParams: mockSetParams, navigate: mockNavigate }),
|
|
@@ -130,50 +121,26 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
130
121
|
mockSetParams.mockClear()
|
|
131
122
|
mockSearchEnabled.mockReturnValue(true)
|
|
132
123
|
mockCanBrowseArchived.mockReturnValue(false)
|
|
133
|
-
mockUseWindowDimensions.mockReturnValue({ fontScale: 1 })
|
|
134
124
|
setCapabilities({ groups: true, teams: true })
|
|
135
125
|
setRouteParams({})
|
|
136
126
|
})
|
|
137
127
|
|
|
138
|
-
it('
|
|
139
|
-
mockUseWindowDimensions.mockReturnValue({ fontScale: 1 })
|
|
140
|
-
|
|
141
|
-
render(<FilterDropdownListHeaderComponent />)
|
|
142
|
-
|
|
143
|
-
expect(screen.getByTestId('conversations-filter-heading-row')).toHaveStyle({
|
|
144
|
-
flexDirection: 'row',
|
|
145
|
-
})
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('stacks the actions under the dropdown at the first font size above the default', () => {
|
|
149
|
-
mockUseWindowDimensions.mockReturnValue({ fontScale: 1.12 })
|
|
150
|
-
|
|
151
|
-
render(<FilterDropdownListHeaderComponent />)
|
|
152
|
-
|
|
153
|
-
expect(screen.getByTestId('conversations-filter-heading-row')).toHaveStyle({
|
|
154
|
-
flexDirection: 'column',
|
|
155
|
-
})
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it('caps the trigger to the room beside the actions so a long label truncates', () => {
|
|
128
|
+
it('caps the trigger to the row so a long label truncates instead of overflowing', () => {
|
|
159
129
|
render(<FilterDropdownListHeaderComponent />)
|
|
160
130
|
|
|
161
131
|
fireEvent(screen.getByTestId('conversations-filter-heading-row'), 'layout', {
|
|
162
132
|
nativeEvent: { layout: { width: 360 } },
|
|
163
133
|
})
|
|
164
|
-
fireEvent(screen.getByTestId('conversations-filter-actions'), 'layout', {
|
|
165
|
-
nativeEvent: { layout: { width: 160 } },
|
|
166
|
-
})
|
|
167
134
|
|
|
168
135
|
expect(screen.getByRole('button', { name: 'All conversations' })).toHaveStyle({
|
|
169
|
-
maxWidth:
|
|
136
|
+
maxWidth: 328,
|
|
170
137
|
})
|
|
171
138
|
})
|
|
172
139
|
|
|
173
140
|
it('offers every filter the person can use, then More filters', () => {
|
|
174
141
|
render(<FilterDropdownListHeaderComponent />)
|
|
175
142
|
|
|
176
|
-
expect(menuItemNames()).toEqual(['All', 'Groups', 'Teams', 'DMs', 'More filters'])
|
|
143
|
+
expect(menuItemNames()).toEqual(['All', 'Groups', 'Teams', 'DMs', 'Unread', 'More filters'])
|
|
177
144
|
expect(screen.getByRole('button', { name: 'All conversations' })).toBeTruthy()
|
|
178
145
|
})
|
|
179
146
|
|
|
@@ -182,7 +149,7 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
182
149
|
|
|
183
150
|
render(<FilterDropdownListHeaderComponent />)
|
|
184
151
|
|
|
185
|
-
expect(menuItemNames()).toEqual(['All', 'DMs'])
|
|
152
|
+
expect(menuItemNames()).toEqual(['All', 'DMs', 'Unread'])
|
|
186
153
|
})
|
|
187
154
|
|
|
188
155
|
it('selecting Groups filters to group conversations', () => {
|
|
@@ -195,6 +162,45 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
195
162
|
)
|
|
196
163
|
})
|
|
197
164
|
|
|
165
|
+
it('selecting Unread filters to unread conversations', () => {
|
|
166
|
+
setRouteParams({ group_source_app_name: 'Groups' })
|
|
167
|
+
|
|
168
|
+
render(<FilterDropdownListHeaderComponent />)
|
|
169
|
+
|
|
170
|
+
fireEvent.press(screen.getByRole('menuitem', { name: 'Unread' }))
|
|
171
|
+
|
|
172
|
+
expect(mockSetParams).toHaveBeenCalledWith({
|
|
173
|
+
title: undefined,
|
|
174
|
+
chat_group_graph_id: undefined,
|
|
175
|
+
group_source_app_name: undefined,
|
|
176
|
+
show_direct_messages: undefined,
|
|
177
|
+
show_unread: true,
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('selecting another filter clears Unread', () => {
|
|
182
|
+
setRouteParams({ show_unread: true })
|
|
183
|
+
|
|
184
|
+
render(<FilterDropdownListHeaderComponent />)
|
|
185
|
+
|
|
186
|
+
fireEvent.press(screen.getByRole('menuitem', { name: 'DMs' }))
|
|
187
|
+
|
|
188
|
+
expect(mockSetParams).toHaveBeenCalledWith(
|
|
189
|
+
expect.objectContaining({ show_direct_messages: true, show_unread: undefined })
|
|
190
|
+
)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('labels the trigger Unread and checks it when filtered to unread', () => {
|
|
194
|
+
setRouteParams({ show_unread: true })
|
|
195
|
+
|
|
196
|
+
render(<FilterDropdownListHeaderComponent />)
|
|
197
|
+
|
|
198
|
+
expect(screen.getByRole('button', { name: 'Unread' })).toBeTruthy()
|
|
199
|
+
expect(screen.getByRole('menuitem', { name: 'Unread' }).props.accessibilityState.selected).toBe(
|
|
200
|
+
true
|
|
201
|
+
)
|
|
202
|
+
})
|
|
203
|
+
|
|
198
204
|
it('labels the trigger with the active filter and checks it', () => {
|
|
199
205
|
setRouteParams({ group_source_app_name: 'Services' })
|
|
200
206
|
|
|
@@ -230,7 +236,7 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
230
236
|
|
|
231
237
|
render(<FilterDropdownListHeaderComponent />)
|
|
232
238
|
|
|
233
|
-
expect(menuItemNames()).toEqual(['All', 'DMs', 'More filters'])
|
|
239
|
+
expect(menuItemNames()).toEqual(['All', 'DMs', 'Unread', 'More filters'])
|
|
234
240
|
})
|
|
235
241
|
|
|
236
242
|
it('labels the archived list and carries it into More filters', () => {
|
|
@@ -279,6 +285,19 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
279
285
|
expect(mockNavigate).toHaveBeenCalledWith('ConversationFilters', expect.any(Object))
|
|
280
286
|
})
|
|
281
287
|
|
|
288
|
+
it('More filters carries the Unread filter into the sheet', () => {
|
|
289
|
+
setRouteParams({ show_unread: true })
|
|
290
|
+
|
|
291
|
+
render(<FilterDropdownListHeaderComponent />)
|
|
292
|
+
|
|
293
|
+
fireEvent.press(screen.getByRole('menuitem', { name: 'More filters' }))
|
|
294
|
+
|
|
295
|
+
expect(mockNavigate).toHaveBeenCalledWith(
|
|
296
|
+
'ConversationFilters',
|
|
297
|
+
expect.objectContaining({ show_unread: true })
|
|
298
|
+
)
|
|
299
|
+
})
|
|
300
|
+
|
|
282
301
|
it('shows the search button only when search is enabled', () => {
|
|
283
302
|
mockSearchEnabled.mockReturnValue(false)
|
|
284
303
|
const view = render(<FilterDropdownListHeaderComponent />)
|