@planningcenter/chat-react-native 4.4.1-qa-staging.25 → 4.5.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/build/components/conversation/message.d.ts.map +1 -1
  2. package/build/components/conversation/message.js +145 -193
  3. package/build/components/conversation/message.js.map +1 -1
  4. package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
  5. package/build/components/conversation/reply_shadow_message.js +107 -135
  6. package/build/components/conversation/reply_shadow_message.js.map +1 -1
  7. package/build/components/conversations/conversations.d.ts.map +1 -1
  8. package/build/components/conversations/conversations.js +3 -2
  9. package/build/components/conversations/conversations.js.map +1 -1
  10. package/build/contexts/conversations_context.d.ts.map +1 -1
  11. package/build/contexts/conversations_context.js +20 -17
  12. package/build/contexts/conversations_context.js.map +1 -1
  13. package/build/hooks/use_clear_conversation_filters.d.ts.map +1 -1
  14. package/build/hooks/use_clear_conversation_filters.js +2 -1
  15. package/build/hooks/use_clear_conversation_filters.js.map +1 -1
  16. package/build/hooks/use_conversations_cache.d.ts +1 -1
  17. package/build/hooks/use_conversations_cache.d.ts.map +1 -1
  18. package/build/hooks/use_conversations_cache.js +64 -55
  19. package/build/hooks/use_conversations_cache.js.map +1 -1
  20. package/build/hooks/use_conversations_jolt_events.js +2 -2
  21. package/build/hooks/use_conversations_jolt_events.js.map +1 -1
  22. package/build/hooks/use_jolt.d.ts.map +1 -1
  23. package/build/hooks/use_jolt.js +37 -9
  24. package/build/hooks/use_jolt.js.map +1 -1
  25. package/build/screens/conversation_filters/context/conversation_filter_context.d.ts.map +1 -1
  26. package/build/screens/conversation_filters/context/conversation_filter_context.js +8 -4
  27. package/build/screens/conversation_filters/context/conversation_filter_context.js.map +1 -1
  28. package/build/screens/conversation_filters/screen_props.d.ts +1 -0
  29. package/build/screens/conversation_filters/screen_props.d.ts.map +1 -1
  30. package/build/screens/conversation_filters/screen_props.js.map +1 -1
  31. package/build/screens/conversations/components/filter_dropdown_list_header_component.d.ts.map +1 -1
  32. package/build/screens/conversations/components/filter_dropdown_list_header_component.js +192 -170
  33. package/build/screens/conversations/components/filter_dropdown_list_header_component.js.map +1 -1
  34. package/build/screens/conversations/conversations_screen.d.ts +1 -0
  35. package/build/screens/conversations/conversations_screen.d.ts.map +1 -1
  36. package/build/screens/conversations/conversations_screen.js.map +1 -1
  37. package/build/utils/request/conversation.d.ts +2 -1
  38. package/build/utils/request/conversation.d.ts.map +1 -1
  39. package/build/utils/request/conversation.js +4 -1
  40. package/build/utils/request/conversation.js.map +1 -1
  41. package/build/utils/styles.d.ts +0 -1
  42. package/build/utils/styles.d.ts.map +1 -1
  43. package/build/utils/styles.js +1 -1
  44. package/build/utils/styles.js.map +1 -1
  45. package/package.json +3 -3
  46. package/src/components/conversation/message.tsx +2 -10
  47. package/src/components/conversation/reply_shadow_message.tsx +3 -14
  48. package/src/components/conversations/__tests__/conversations_blank_state.test.tsx +1 -0
  49. package/src/components/conversations/conversations.tsx +12 -2
  50. package/src/contexts/conversations_context.tsx +8 -1
  51. package/src/hooks/__tests__/use_conversations_jolt_events.test.tsx +135 -0
  52. package/src/hooks/__tests__/use_jolt.test.tsx +76 -1
  53. package/src/hooks/use_clear_conversation_filters.ts +1 -0
  54. package/src/hooks/use_conversations_cache.ts +29 -24
  55. package/src/hooks/use_conversations_jolt_events.ts +2 -2
  56. package/src/hooks/use_jolt.ts +26 -2
  57. package/src/screens/conversation_filters/context/conversation_filter_context.tsx +4 -0
  58. package/src/screens/conversation_filters/screen_props.ts +1 -0
  59. package/src/screens/conversations/components/__tests__/filter_dropdown_list_header_component.test.tsx +53 -31
  60. package/src/screens/conversations/components/filter_dropdown_list_header_component.tsx +19 -4
  61. package/src/screens/conversations/conversations_screen.tsx +1 -0
  62. package/src/utils/__tests__/conversation.test.ts +1 -0
  63. package/src/utils/request/conversation.ts +4 -0
  64. package/src/utils/styles.ts +0 -1
@@ -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
+ })
@@ -11,6 +11,7 @@ export function useClearConversationFilters() {
11
11
  group_source_app_name: undefined,
12
12
  show_direct_messages: undefined,
13
13
  show_archived: undefined,
14
+ show_unread: undefined,
14
15
  })
15
16
  }, [navigation])
16
17
  }
@@ -1,5 +1,5 @@
1
1
  import { InfiniteData, useQueryClient } from '@tanstack/react-query'
2
- import { ApiCollection, ApiResource, ConversationResource } from '../types'
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: argsData } = conversationsRequestArgs
25
- const { data } = await apiClient.chat.get<ApiResource<ConversationResource>>({
26
- url: `/me/conversations/${id}`,
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 data
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
- fetchCreate: fetchAndUpdateOrCreate,
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.fetchUpdate({ id: e.data.data.id })
34
+ cache.fetchSync({ id: e.data.data.id })
35
35
  })
36
36
  useJoltEvent(joltChannel, 'conversation.created', (e: JoltConversationEvent) =>
37
- cache.fetchCreate({ id: e.data.data.id })
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 })
@@ -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 handleUnsubscribe
220
- }, [handleUnsubscribe])
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) => {
@@ -6,6 +6,7 @@ export type ConversationFiltersParams = {
6
6
  group_source_app_name?: string
7
7
  show_direct_messages?: boolean
8
8
  show_archived?: boolean
9
+ show_unread?: boolean
9
10
  }
10
11
 
11
12
  export type ConversationFiltersScreenProps = StaticScreenProps<ConversationFiltersParams>
@@ -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,53 +121,71 @@ 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('keeps the dropdown and actions on one line below the header font cap', () => {
139
- mockUseWindowDimensions.mockReturnValue({ fontScale: 1.15 })
140
-
128
+ it('offers every filter the person can use, then More filters', () => {
141
129
  render(<FilterDropdownListHeaderComponent />)
142
130
 
143
- expect(screen.getByTestId('conversations-filter-heading-row')).toHaveStyle({
144
- flexDirection: 'row',
145
- })
131
+ expect(menuItemNames()).toEqual(['All', 'Groups', 'Teams', 'DMs', 'Unread', 'More filters'])
132
+ expect(screen.getByRole('button', { name: 'All conversations' })).toBeTruthy()
146
133
  })
147
134
 
148
- it('stacks the actions under the dropdown once header text stops scaling', () => {
149
- mockUseWindowDimensions.mockReturnValue({ fontScale: 1.2 })
135
+ it('leaves out Teams and More filters when only DMs are filterable', () => {
136
+ setCapabilities({ groups: false, teams: false })
150
137
 
151
138
  render(<FilterDropdownListHeaderComponent />)
152
139
 
153
- expect(screen.getByTestId('conversations-filter-heading-row')).toHaveStyle({
154
- flexDirection: 'column',
155
- })
140
+ expect(menuItemNames()).toEqual(['All', 'DMs', 'Unread'])
156
141
  })
157
142
 
158
- it('offers every filter the person can use, then More filters', () => {
143
+ it('selecting Groups filters to group conversations', () => {
159
144
  render(<FilterDropdownListHeaderComponent />)
160
145
 
161
- expect(menuItemNames()).toEqual(['All', 'Groups', 'Teams', 'DMs', 'More filters'])
162
- expect(screen.getByRole('button', { name: 'All conversations' })).toBeTruthy()
146
+ fireEvent.press(screen.getByRole('menuitem', { name: 'Groups' }))
147
+
148
+ expect(mockSetParams).toHaveBeenCalledWith(
149
+ expect.objectContaining({ group_source_app_name: 'Groups', show_direct_messages: undefined })
150
+ )
163
151
  })
164
152
 
165
- it('leaves out Teams and More filters when only DMs are filterable', () => {
166
- setCapabilities({ groups: false, teams: false })
153
+ it('selecting Unread filters to unread conversations', () => {
154
+ setRouteParams({ group_source_app_name: 'Groups' })
167
155
 
168
156
  render(<FilterDropdownListHeaderComponent />)
169
157
 
170
- expect(menuItemNames()).toEqual(['All', 'DMs'])
158
+ fireEvent.press(screen.getByRole('menuitem', { name: 'Unread' }))
159
+
160
+ expect(mockSetParams).toHaveBeenCalledWith({
161
+ title: undefined,
162
+ chat_group_graph_id: undefined,
163
+ group_source_app_name: undefined,
164
+ show_direct_messages: undefined,
165
+ show_unread: true,
166
+ })
171
167
  })
172
168
 
173
- it('selecting Groups filters to group conversations', () => {
169
+ it('selecting another filter clears Unread', () => {
170
+ setRouteParams({ show_unread: true })
171
+
174
172
  render(<FilterDropdownListHeaderComponent />)
175
173
 
176
- fireEvent.press(screen.getByRole('menuitem', { name: 'Groups' }))
174
+ fireEvent.press(screen.getByRole('menuitem', { name: 'DMs' }))
177
175
 
178
176
  expect(mockSetParams).toHaveBeenCalledWith(
179
- expect.objectContaining({ group_source_app_name: 'Groups', show_direct_messages: undefined })
177
+ expect.objectContaining({ show_direct_messages: true, show_unread: undefined })
178
+ )
179
+ })
180
+
181
+ it('labels the trigger Unread and checks it when filtered to unread', () => {
182
+ setRouteParams({ show_unread: true })
183
+
184
+ render(<FilterDropdownListHeaderComponent />)
185
+
186
+ expect(screen.getByRole('button', { name: 'Unread' })).toBeTruthy()
187
+ expect(screen.getByRole('menuitem', { name: 'Unread' }).props.accessibilityState.selected).toBe(
188
+ true
180
189
  )
181
190
  })
182
191
 
@@ -215,7 +224,7 @@ describe('FilterDropdownListHeaderComponent', () => {
215
224
 
216
225
  render(<FilterDropdownListHeaderComponent />)
217
226
 
218
- expect(menuItemNames()).toEqual(['All', 'DMs', 'More filters'])
227
+ expect(menuItemNames()).toEqual(['All', 'DMs', 'Unread', 'More filters'])
219
228
  })
220
229
 
221
230
  it('labels the archived list and carries it into More filters', () => {
@@ -264,6 +273,19 @@ describe('FilterDropdownListHeaderComponent', () => {
264
273
  expect(mockNavigate).toHaveBeenCalledWith('ConversationFilters', expect.any(Object))
265
274
  })
266
275
 
276
+ it('More filters carries the Unread filter into the sheet', () => {
277
+ setRouteParams({ show_unread: true })
278
+
279
+ render(<FilterDropdownListHeaderComponent />)
280
+
281
+ fireEvent.press(screen.getByRole('menuitem', { name: 'More filters' }))
282
+
283
+ expect(mockNavigate).toHaveBeenCalledWith(
284
+ 'ConversationFilters',
285
+ expect.objectContaining({ show_unread: true })
286
+ )
287
+ })
288
+
267
289
  it('shows the search button only when search is enabled', () => {
268
290
  mockSearchEnabled.mockReturnValue(false)
269
291
  const view = render(<FilterDropdownListHeaderComponent />)
@@ -33,6 +33,7 @@ enum FilterTypes {
33
33
  Groups = 'Groups',
34
34
  Teams = 'Teams',
35
35
  DMs = 'DMs',
36
+ Unread = 'Unread',
36
37
  More = 'More',
37
38
  }
38
39
 
@@ -41,6 +42,7 @@ const FILTER_PARAMS: Record<Exclude<FilterTypes, FilterTypes.More>, Conversation
41
42
  [FilterTypes.Groups]: { group_source_app_name: 'Groups' },
42
43
  [FilterTypes.Teams]: { group_source_app_name: 'Services' },
43
44
  [FilterTypes.DMs]: { show_direct_messages: true },
45
+ [FilterTypes.Unread]: { show_unread: true },
44
46
  }
45
47
 
46
48
  export const FilterDropdownListHeaderComponent = () => {
@@ -59,6 +61,7 @@ export const FilterDropdownListHeaderComponent = () => {
59
61
  group_source_app_name = '',
60
62
  show_direct_messages,
61
63
  show_archived,
64
+ show_unread,
62
65
  } = route.params || {}
63
66
  const currentPersonCache = useCurrentPersonCache()
64
67
  const { markAllRead, isPending } = useMarkAllRead()
@@ -81,10 +84,12 @@ export const FilterDropdownListHeaderComponent = () => {
81
84
  return FilterTypes.Teams
82
85
  } else if (show_direct_messages) {
83
86
  return FilterTypes.DMs
87
+ } else if (show_unread) {
88
+ return FilterTypes.Unread
84
89
  }
85
90
 
86
91
  return FilterTypes.All
87
- }, [chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived])
92
+ }, [chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived, show_unread])
88
93
 
89
94
  const options: FilterDropdownOption[] = useMemo(
90
95
  () => [
@@ -92,6 +97,7 @@ export const FilterDropdownListHeaderComponent = () => {
92
97
  ...(canFilterByGroups ? [{ id: FilterTypes.Groups, label: FilterTypes.Groups }] : []),
93
98
  ...(canFilterByTeams ? [{ id: FilterTypes.Teams, label: FilterTypes.Teams }] : []),
94
99
  ...(canFilterByDMs ? [{ id: FilterTypes.DMs, label: FilterTypes.DMs }] : []),
100
+ { id: FilterTypes.Unread, label: FilterTypes.Unread },
95
101
  ],
96
102
  [canFilterByGroups, canFilterByTeams, canFilterByDMs]
97
103
  )
@@ -142,6 +148,7 @@ export const FilterDropdownListHeaderComponent = () => {
142
148
  group_source_app_name: undefined,
143
149
  show_direct_messages: undefined,
144
150
  show_archived: undefined,
151
+ show_unread: undefined,
145
152
  ...params,
146
153
  })
147
154
  },
@@ -170,8 +177,16 @@ export const FilterDropdownListHeaderComponent = () => {
170
177
  group_source_app_name,
171
178
  show_direct_messages,
172
179
  show_archived,
180
+ show_unread,
173
181
  })
174
- }, [navigation, chat_group_graph_id, group_source_app_name, show_direct_messages, show_archived])
182
+ }, [
183
+ navigation,
184
+ chat_group_graph_id,
185
+ group_source_app_name,
186
+ show_direct_messages,
187
+ show_archived,
188
+ show_unread,
189
+ ])
175
190
 
176
191
  const handleMarkAllRead = useCallback(() => {
177
192
  currentPersonCache.update({ unreadCount: 0 })
@@ -194,7 +209,7 @@ export const FilterDropdownListHeaderComponent = () => {
194
209
 
195
210
  return (
196
211
  <View style={styles.listHeader}>
197
- <View style={styles.headingRow} testID="conversations-filter-heading-row">
212
+ <View style={styles.headingRow}>
198
213
  <View style={styles.titleContainer}>
199
214
  <FilterDropdown
200
215
  options={options}
@@ -252,7 +267,7 @@ export const FilterDropdownListHeaderComponent = () => {
252
267
 
253
268
  const useStyles = () => {
254
269
  const theme = useTheme()
255
- const atFontScaleBreakpoint = useAtFontScaleBreakpoint(MAX_FONT_SIZE_MULTIPLIER_LANDMARK)
270
+ const atFontScaleBreakpoint = useAtFontScaleBreakpoint()
256
271
 
257
272
  return StyleSheet.create({
258
273
  listHeader: {
@@ -19,6 +19,7 @@ export type ConversationsScreenProps = StaticScreenProps<{
19
19
  group_source_app_name?: AppName
20
20
  show_direct_messages?: boolean
21
21
  show_archived?: boolean
22
+ show_unread?: boolean
22
23
  }>
23
24
 
24
25
  export function ConversationsScreen({ route }: ConversationsScreenProps) {
@@ -13,6 +13,7 @@ describe('getConversationsRequestArgs', () => {
13
13
  ['chat_group_graph_id', { chat_group_graph_id: groupId }, 'mine_or_not_empty,group'],
14
14
  ['show_direct_messages', { show_direct_messages: true }, 'mine_or_not_empty,direct_message'],
15
15
  ['show_archived', { show_archived: true }, 'mine_or_not_empty,archived'],
16
+ ['show_unread', { show_unread: true }, 'mine_or_not_empty,unread'],
16
17
  ])('%s uses filter: %s', (_label, args, expectedFilter) => {
17
18
  expect(getConversationsRequestArgs(args).data.filter).toBe(expectedFilter)
18
19
  })
@@ -7,6 +7,7 @@ export interface ConversationRequestArgs {
7
7
  group_source_app_name?: string
8
8
  show_direct_messages?: boolean
9
9
  show_archived?: boolean
10
+ show_unread?: boolean
10
11
  }
11
12
 
12
13
  export const getConversationsRequestArgs = ({
@@ -15,12 +16,15 @@ export const getConversationsRequestArgs = ({
15
16
  group_source_app_name,
16
17
  show_direct_messages,
17
18
  show_archived,
19
+ show_unread,
18
20
  }: Partial<ConversationRequestArgs> = {}): GetRequest => {
19
21
  let filter: string
20
22
  if (chat_group_graph_id) {
21
23
  filter = 'mine_or_not_empty,group'
22
24
  } else if (show_archived) {
23
25
  filter = 'mine_or_not_empty,archived'
26
+ } else if (show_unread) {
27
+ filter = 'mine_or_not_empty,unread'
24
28
  } else if (show_direct_messages) {
25
29
  filter = 'mine_or_not_empty,direct_message'
26
30
  } else if (group_source_app_name) {
@@ -6,7 +6,6 @@ export const MAX_FONT_SIZE_MULTIPLIER_LANDMARK = 1.2 // Use in headers, footers,
6
6
 
7
7
  export const CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL = 16 // TODO: move to `screens/conversation/utils/styles` when `/screens/conversation` is created
8
8
  export const MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH = 32 // TODO: move to `screens/conversation/utils/styles` when `/screens/conversation` is created
9
- export const MESSAGE_BACKGROUND_OUTSET = 2
10
9
 
11
10
  export const platformFontWeightMedium = Platform.select({
12
11
  ios: tokens.fontWeightMedium,