@planningcenter/chat-react-native 4.4.1-qa-staging.25 → 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 +6 -1
- package/build/components/display/filter_dropdown.d.ts.map +1 -1
- package/build/components/display/filter_dropdown.js +57 -35
- 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 +251 -185
- 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 +11 -3
- 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 +74 -7
- package/src/screens/conversations/components/filter_dropdown_list_header_component.tsx +55 -14
- 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
|
|
|
@@ -135,8 +135,8 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
135
135
|
setRouteParams({})
|
|
136
136
|
})
|
|
137
137
|
|
|
138
|
-
it('keeps the dropdown and actions on one line
|
|
139
|
-
mockUseWindowDimensions.mockReturnValue({ fontScale: 1
|
|
138
|
+
it('keeps the dropdown and actions on one line at the default font scale', () => {
|
|
139
|
+
mockUseWindowDimensions.mockReturnValue({ fontScale: 1 })
|
|
140
140
|
|
|
141
141
|
render(<FilterDropdownListHeaderComponent />)
|
|
142
142
|
|
|
@@ -145,8 +145,8 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
145
145
|
})
|
|
146
146
|
})
|
|
147
147
|
|
|
148
|
-
it('stacks the actions under the dropdown
|
|
149
|
-
mockUseWindowDimensions.mockReturnValue({ fontScale: 1.
|
|
148
|
+
it('stacks the actions under the dropdown at the first font size above the default', () => {
|
|
149
|
+
mockUseWindowDimensions.mockReturnValue({ fontScale: 1.12 })
|
|
150
150
|
|
|
151
151
|
render(<FilterDropdownListHeaderComponent />)
|
|
152
152
|
|
|
@@ -155,10 +155,25 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
155
155
|
})
|
|
156
156
|
})
|
|
157
157
|
|
|
158
|
+
it('caps the trigger to the room beside the actions so a long label truncates', () => {
|
|
159
|
+
render(<FilterDropdownListHeaderComponent />)
|
|
160
|
+
|
|
161
|
+
fireEvent(screen.getByTestId('conversations-filter-heading-row'), 'layout', {
|
|
162
|
+
nativeEvent: { layout: { width: 360 } },
|
|
163
|
+
})
|
|
164
|
+
fireEvent(screen.getByTestId('conversations-filter-actions'), 'layout', {
|
|
165
|
+
nativeEvent: { layout: { width: 160 } },
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
expect(screen.getByRole('button', { name: 'All conversations' })).toHaveStyle({
|
|
169
|
+
maxWidth: 156,
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
158
173
|
it('offers every filter the person can use, then More filters', () => {
|
|
159
174
|
render(<FilterDropdownListHeaderComponent />)
|
|
160
175
|
|
|
161
|
-
expect(menuItemNames()).toEqual(['All', 'Groups', 'Teams', 'DMs', 'More filters'])
|
|
176
|
+
expect(menuItemNames()).toEqual(['All', 'Groups', 'Teams', 'DMs', 'Unread', 'More filters'])
|
|
162
177
|
expect(screen.getByRole('button', { name: 'All conversations' })).toBeTruthy()
|
|
163
178
|
})
|
|
164
179
|
|
|
@@ -167,7 +182,7 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
167
182
|
|
|
168
183
|
render(<FilterDropdownListHeaderComponent />)
|
|
169
184
|
|
|
170
|
-
expect(menuItemNames()).toEqual(['All', 'DMs'])
|
|
185
|
+
expect(menuItemNames()).toEqual(['All', 'DMs', 'Unread'])
|
|
171
186
|
})
|
|
172
187
|
|
|
173
188
|
it('selecting Groups filters to group conversations', () => {
|
|
@@ -180,6 +195,45 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
180
195
|
)
|
|
181
196
|
})
|
|
182
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
|
+
|
|
183
237
|
it('labels the trigger with the active filter and checks it', () => {
|
|
184
238
|
setRouteParams({ group_source_app_name: 'Services' })
|
|
185
239
|
|
|
@@ -215,7 +269,7 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
215
269
|
|
|
216
270
|
render(<FilterDropdownListHeaderComponent />)
|
|
217
271
|
|
|
218
|
-
expect(menuItemNames()).toEqual(['All', 'DMs', 'More filters'])
|
|
272
|
+
expect(menuItemNames()).toEqual(['All', 'DMs', 'Unread', 'More filters'])
|
|
219
273
|
})
|
|
220
274
|
|
|
221
275
|
it('labels the archived list and carries it into More filters', () => {
|
|
@@ -264,6 +318,19 @@ describe('FilterDropdownListHeaderComponent', () => {
|
|
|
264
318
|
expect(mockNavigate).toHaveBeenCalledWith('ConversationFilters', expect.any(Object))
|
|
265
319
|
})
|
|
266
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
|
+
|
|
267
334
|
it('shows the search button only when search is enabled', () => {
|
|
268
335
|
mockSearchEnabled.mockReturnValue(false)
|
|
269
336
|
const view = render(<FilterDropdownListHeaderComponent />)
|