@planningcenter/chat-react-native 3.1.0 → 3.2.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 (48) hide show
  1. package/build/navigation/index.d.ts +1 -8
  2. package/build/navigation/index.d.ts.map +1 -1
  3. package/build/screens/conversation_filters/components/conversation_filters.d.ts +3 -0
  4. package/build/screens/conversation_filters/components/conversation_filters.d.ts.map +1 -0
  5. package/build/screens/conversation_filters/components/conversation_filters.js +173 -0
  6. package/build/screens/conversation_filters/components/conversation_filters.js.map +1 -0
  7. package/build/screens/conversation_filters/components/rows.d.ts +31 -0
  8. package/build/screens/conversation_filters/components/rows.d.ts.map +1 -0
  9. package/build/screens/conversation_filters/components/rows.js +111 -0
  10. package/build/screens/conversation_filters/components/rows.js.map +1 -0
  11. package/build/screens/conversation_filters/context/conversation_filter_context.d.ts +23 -0
  12. package/build/screens/conversation_filters/context/conversation_filter_context.d.ts.map +1 -0
  13. package/build/screens/conversation_filters/context/conversation_filter_context.js +51 -0
  14. package/build/screens/conversation_filters/context/conversation_filter_context.js.map +1 -0
  15. package/build/screens/conversation_filters/filter_types.d.ts +7 -0
  16. package/build/screens/conversation_filters/filter_types.d.ts.map +1 -0
  17. package/build/screens/conversation_filters/filter_types.js +8 -0
  18. package/build/screens/conversation_filters/filter_types.js.map +1 -0
  19. package/build/screens/conversation_filters/group_filters.d.ts +6 -0
  20. package/build/screens/conversation_filters/group_filters.d.ts.map +1 -0
  21. package/build/screens/conversation_filters/group_filters.js +15 -0
  22. package/build/screens/conversation_filters/group_filters.js.map +1 -0
  23. package/build/screens/conversation_filters/hooks/filters.d.ts +415 -0
  24. package/build/screens/conversation_filters/hooks/filters.d.ts.map +1 -0
  25. package/build/screens/conversation_filters/hooks/filters.js +41 -0
  26. package/build/screens/conversation_filters/hooks/filters.js.map +1 -0
  27. package/build/screens/conversation_filters/screen_props.d.ts +13 -0
  28. package/build/screens/conversation_filters/screen_props.d.ts.map +1 -0
  29. package/build/screens/conversation_filters/screen_props.js +2 -0
  30. package/build/screens/conversation_filters/screen_props.js.map +1 -0
  31. package/build/screens/conversation_filters/team_filters.d.ts +6 -0
  32. package/build/screens/conversation_filters/team_filters.d.ts.map +1 -0
  33. package/build/screens/conversation_filters/team_filters.js +15 -0
  34. package/build/screens/conversation_filters/team_filters.js.map +1 -0
  35. package/build/screens/conversation_filters_screen.d.ts +2 -9
  36. package/build/screens/conversation_filters_screen.d.ts.map +1 -1
  37. package/build/screens/conversation_filters_screen.js +81 -302
  38. package/build/screens/conversation_filters_screen.js.map +1 -1
  39. package/package.json +2 -2
  40. package/src/screens/conversation_filters/components/conversation_filters.tsx +233 -0
  41. package/src/screens/conversation_filters/components/rows.tsx +164 -0
  42. package/src/screens/conversation_filters/context/conversation_filter_context.tsx +65 -0
  43. package/src/screens/conversation_filters/filter_types.ts +6 -0
  44. package/src/screens/conversation_filters/group_filters.tsx +31 -0
  45. package/src/screens/conversation_filters/hooks/filters.ts +68 -0
  46. package/src/screens/conversation_filters/screen_props.ts +15 -0
  47. package/src/screens/conversation_filters/team_filters.tsx +31 -0
  48. package/src/screens/conversation_filters_screen.tsx +93 -429
@@ -1,309 +1,130 @@
1
- import { PlatformPressable } from '@react-navigation/elements'
1
+ import { HeaderTitle, HeaderTitleProps, useHeaderHeight } from '@react-navigation/elements'
2
+ import { createComponentForStaticNavigation, RouteProp, useRoute } from '@react-navigation/native'
2
3
  import {
3
- RouteProp,
4
- StackActions,
5
- StaticScreenProps,
6
- useNavigation,
7
- useRoute,
8
- } from '@react-navigation/native'
9
- import { NativeStackNavigationOptions } from '@react-navigation/native-stack'
10
- import React, { createContext, PropsWithChildren, useContext, useMemo, useState } from 'react'
11
- import {
12
- Animated,
13
- FlatList,
14
- Platform,
15
- StyleSheet,
16
- useWindowDimensions,
17
- View,
18
- ViewStyle,
19
- } from 'react-native'
4
+ createNativeStackNavigator,
5
+ NativeStackNavigationOptions,
6
+ } from '@react-navigation/native-stack'
7
+ import React from 'react'
8
+ import { Platform, StyleSheet, useWindowDimensions, View } from 'react-native'
20
9
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
21
- import { Button, Heading, Icon, Image, Text, TextButton } from '../components'
10
+ import { Button, TextButton } from '../components'
11
+ import {
12
+ FilterContext,
13
+ FilterProvider,
14
+ } from './conversation_filters/context/conversation_filter_context'
15
+ import { ConversationFilters } from './conversation_filters/components/conversation_filters'
16
+ import { GroupFilters } from './conversation_filters/group_filters'
17
+ import {
18
+ ConversationFiltersScreenProps,
19
+ ConversationFilterStackParamList,
20
+ } from './conversation_filters/screen_props'
22
21
  import { useTheme } from '../hooks'
23
- import { useGroups } from '../hooks/use_groups'
24
- import { useGroupsGroups } from '../hooks/use_groups_groups'
25
- import { useServicesTeamsMap } from '../hooks/use_services_team'
26
- import { useTeams } from '../hooks/use_teams'
27
- import { GroupsGroupResource, TeamOptionResponseItem } from '../types'
28
- import { GraphId, GroupResource } from '../types/resources/group_resource'
29
-
30
- // =========================================
31
- // ====== Factory Constants & Types ========
32
- // =========================================
22
+ import { TeamFilters } from './conversation_filters/team_filters'
33
23
 
34
- enum SectionTypes {
35
- filterBar,
36
- filter,
37
- groups,
38
- header,
39
- teams,
40
- hidden,
24
+ const FilterHeaderTitle = ({ tintColor }: HeaderTitleProps) => {
25
+ const styles = useStyles()
26
+ return <HeaderTitle style={[styles.headerTitle, { color: tintColor }]}>Filters</HeaderTitle>
41
27
  }
42
28
 
43
- type SectionListData = Array<
44
- | DataItem<FilterProps, SectionTypes.filter>
45
- | DataItem<GroupRowProps, SectionTypes.groups>
46
- | DataItem<HeaderProps, SectionTypes.header>
47
- | DataItem<TeamRowProps, SectionTypes.teams>
48
- | DataItem<{}, SectionTypes.filterBar>
49
- | DataItem<any, SectionTypes.hidden>
50
- >
29
+ const HeaderRight = () => {
30
+ const { resetFilter, applyFilters } = React.useContext(FilterContext)
31
+ const route = useRoute<RouteProp<ConversationFiltersScreenProps['route']>>()
32
+ const styles = useStyles()
51
33
 
52
- interface DataItem<T, TName extends SectionTypes> {
53
- type: TName
54
- data: T
55
- sectionStyle?: ViewStyle
34
+ return (
35
+ <View style={styles.headerRight}>
36
+ <TextButton onPress={resetFilter}>Reset</TextButton>
37
+ <Button title="Apply" onPress={() => applyFilters(route.params)} />
38
+ </View>
39
+ )
56
40
  }
57
41
 
58
- const ASPECT_RATIO = 16 / 9
59
- const THUMBNAIL_WIDTH = 80
60
- const THUMBNAIL_HEIGHT = THUMBNAIL_WIDTH / ASPECT_RATIO
61
-
62
- enum FilterTypes {
63
- All = 'All conversations',
64
- Groups = 'All my groups',
65
- Teams = 'All my teams and plans',
66
- More = 'More',
42
+ const HeaderRightWithContext = () => {
43
+ return (
44
+ <FilterProvider>
45
+ <HeaderRight />
46
+ </FilterProvider>
47
+ )
67
48
  }
68
49
 
69
- // =================================
70
- // ====== Components ===============
71
- // =================================
72
-
73
50
  export const ConversationFiltersScreenOptions: NativeStackNavigationOptions = {
74
- presentation: 'formSheet',
75
- headerShown: false,
76
- sheetAllowedDetents: [0.75],
51
+ presentation: Platform.select({ android: 'modal', ios: 'formSheet' }),
52
+ sheetAllowedDetents: Platform.select({
53
+ android: [0.75, 0.94],
54
+ default: [0.75, 1],
55
+ }),
77
56
  sheetGrabberVisible: true,
78
- }
79
-
80
- type ConversationFilters = {
81
- chat_group_graph_id?: GraphId
82
- group_source_app_name?: string
83
- }
84
- type ConversationFiltersScreenProps = StaticScreenProps<ConversationFilters>
85
-
86
- const FilterContext = createContext<{
87
- scrollOffset: number
88
- resetFilter: () => void
89
- setGroupFilter: (params: ConversationFilters) => void
90
- setAppFilter: (params: ConversationFilters) => void
91
- applyFilters: (params: ConversationFilters) => void
92
- }>({
93
- scrollOffset: 0,
94
- resetFilter: () => {},
95
- setGroupFilter: () => {},
96
- setAppFilter: () => {},
97
- applyFilters: () => {},
98
- })
99
-
100
- export const ConversationFiltersScreen = ({}: ConversationFiltersScreenProps) => {
101
- const styles = useStyles()
102
- const navigation = useNavigation()
103
- const route = useRoute<RouteProp<ConversationFiltersScreenProps['route']>>()
104
- const { chat_group_graph_id, group_source_app_name = '' } = route.params
105
-
106
- const [scrollOffset, setScrollOffset] = useState(0)
107
-
108
- const activeFilter: FilterTypes = useMemo(() => {
109
- if (chat_group_graph_id) {
110
- return FilterTypes.More
111
- } else if (/groups/i.test(group_source_app_name)) {
112
- return FilterTypes.Groups
113
- } else if (/services/i.test(group_source_app_name)) {
114
- return FilterTypes.Teams
115
- }
116
-
117
- return FilterTypes.All
118
- }, [chat_group_graph_id, group_source_app_name])
119
-
120
- const { groups = [], fetchNextPage: fetchNextGroupsPage } = useGroupsToFilter()
121
- const { teams = [], fetchNextPage: fetchNextTeamsPage } = useTeamsToFilter()
122
-
123
- const activeGroupId = chat_group_graph_id
124
- const isExactGroupFilter = activeFilter === FilterTypes.More
125
-
126
- // @ts-expect-error
127
- const teamItems: DataItem<TeamRowProps, SectionTypes.teams>[] = teams.map(team => ({
128
- type: SectionTypes.teams,
129
- data: {
130
- team,
131
- isActive: isExactGroupFilter && team.id.toString() === activeGroupId,
132
- },
133
- }))
134
-
135
- const groupItems: DataItem<GroupRowProps, SectionTypes.groups>[] = groups.map(group => ({
136
- type: SectionTypes.groups,
137
- data: {
138
- group,
139
- isActive: isExactGroupFilter && group?.id.toString() === activeGroupId,
140
- },
141
- }))
142
-
143
- const hideAppFilters = groupItems.length < 1 || teamItems.length < 1
144
-
145
- const listData: SectionListData = [
146
- {
147
- type: SectionTypes.filterBar,
148
- data: {},
149
- },
150
- {
151
- type: hideAppFilters ? SectionTypes.hidden : SectionTypes.header,
152
- data: { title: 'General Filters' },
153
- },
154
- {
155
- type: hideAppFilters ? SectionTypes.hidden : SectionTypes.filter,
156
- data: { filter: FilterTypes.All, isActive: activeFilter === FilterTypes.All },
157
- },
158
- {
159
- type: hideAppFilters ? SectionTypes.hidden : SectionTypes.filter,
160
- data: {
161
- filter: FilterTypes.Groups,
162
- group_source_app_name: 'groups',
163
- isActive: activeFilter === FilterTypes.Groups,
57
+ headerBackVisible: false,
58
+ headerRight: HeaderRightWithContext,
59
+ headerTitle: FilterHeaderTitle,
60
+ headerTitleAlign: 'left',
61
+ }
62
+
63
+ const FilterNavigator = createNativeStackNavigator<ConversationFilterStackParamList>({
64
+ initialRouteName: 'Filters',
65
+ screens: {
66
+ Filters: {
67
+ screen: ConversationFilters,
68
+ options: {
69
+ headerShown: false,
164
70
  },
165
71
  },
166
- {
167
- type: hideAppFilters ? SectionTypes.hidden : SectionTypes.filter,
168
- data: {
169
- filter: FilterTypes.Teams,
170
- group_source_app_name: 'services',
171
- isActive: activeFilter === FilterTypes.Teams,
72
+ GroupFilters: {
73
+ screen: GroupFilters,
74
+ options: {
75
+ headerTitle: Platform.select({
76
+ android: 'Groups',
77
+ ios: '',
78
+ }),
79
+ headerBackTitle: 'Groups',
172
80
  },
173
81
  },
174
- {
175
- type: groupItems.length ? SectionTypes.header : SectionTypes.hidden,
176
- data: { title: 'Groups' },
177
- },
178
- ...groupItems,
179
- {
180
- type: teamItems.length ? SectionTypes.header : SectionTypes.hidden,
181
- data: { title: 'Teams' },
182
- },
183
- ...teamItems,
184
- ]
185
-
186
- function handleOnEndReached() {
187
- fetchNextTeamsPage()
188
- fetchNextGroupsPage()
189
- }
190
-
191
- const filterContextValue = {
192
- scrollOffset,
193
- resetFilter: () => {
194
- navigation.dispatch(
195
- StackActions.popTo('Conversations', {
196
- chat_group_graph_id: undefined,
197
- group_source_app_name: undefined,
198
- })
199
- )
200
- },
201
- setGroupFilter: (params: Omit<ConversationFilters, 'group_source_app_name'>) => {
202
- navigation.setParams({
203
- chat_group_graph_id: params.chat_group_graph_id,
204
- group_source_app_name: undefined,
205
- })
206
- },
207
- setAppFilter: (params: Omit<ConversationFilters, 'chat_group_graph_id'>) => {
208
- navigation.setParams({
209
- chat_group_graph_id: undefined,
210
- group_source_app_name: params.group_source_app_name,
211
- })
212
- },
213
- applyFilters: (params: ConversationFilters) => {
214
- navigation.dispatch(StackActions.popTo('Conversations', params))
82
+ TeamFilters: {
83
+ screen: TeamFilters,
84
+ options: {
85
+ headerTitle: Platform.select({
86
+ android: 'Teams',
87
+ ios: '',
88
+ }),
89
+ headerBackTitle: 'Teams',
90
+ },
215
91
  },
216
- }
92
+ },
93
+ })
217
94
 
218
- return (
219
- <View style={styles.container}>
220
- <FilterContext.Provider value={filterContextValue}>
221
- <FlatList
222
- data={listData}
223
- contentContainerStyle={styles.flatlistContainer}
224
- onEndReached={handleOnEndReached}
225
- stickyHeaderIndices={[0]}
226
- nestedScrollEnabled={true}
227
- onScroll={e => setScrollOffset(e.nativeEvent.contentOffset.y)}
228
- renderItem={({ item }) => {
229
- switch (item.type) {
230
- case SectionTypes.filterBar:
231
- return <FilterBar {...item.data} />
232
- case SectionTypes.header:
233
- return <Header {...item.data} />
234
- case SectionTypes.filter:
235
- return <FilterRow {...item.data} />
236
- case SectionTypes.groups:
237
- return <GroupRow {...item.data} />
238
- case SectionTypes.teams:
239
- return <TeamRow {...item.data} />
240
- default:
241
- return null
242
- }
243
- }}
244
- />
245
- </FilterContext.Provider>
246
- </View>
247
- )
248
- }
95
+ const Filters = createComponentForStaticNavigation(FilterNavigator, 'Filters')
249
96
 
250
- function FilterBar({ style }: { style?: ViewStyle }) {
97
+ export const ConversationFiltersScreen = (_props: ConversationFiltersScreenProps) => {
251
98
  const styles = useStyles()
252
- const { scrollOffset, resetFilter, applyFilters } = React.useContext(FilterContext)
253
- const route = useRoute<RouteProp<ConversationFiltersScreenProps['route']>>()
254
99
 
255
100
  return (
256
- <Animated.View style={[styles.filterBar, scrollOffset > 0 && styles.filterBarScroll, style]}>
257
- <Heading variant="h3">Filters</Heading>
258
- <View style={styles.filterBarActions}>
259
- <TextButton onPress={resetFilter}>Reset</TextButton>
260
- <Button title="Apply" onPress={() => applyFilters(route.params)} />
101
+ <FilterProvider>
102
+ <View style={styles.container}>
103
+ <Filters />
261
104
  </View>
262
- </Animated.View>
105
+ </FilterProvider>
263
106
  )
264
107
  }
265
108
 
266
109
  const useStyles = () => {
110
+ const { top } = useSafeAreaInsets()
267
111
  const { height } = useWindowDimensions()
268
- const { bottom } = useSafeAreaInsets()
112
+ const headerHeight = useHeaderHeight()
269
113
  const theme = useTheme()
270
114
 
115
+ const containerHeight = Platform.select({
116
+ android: height,
117
+ ios: height - top - headerHeight,
118
+ })
119
+
271
120
  return StyleSheet.create({
272
121
  container: {
273
- gap: 8,
274
- paddingTop: 12,
275
- },
276
- flatlistContainer: {
277
- paddingBottom: bottom,
278
- height: Platform.select({
279
- ios: undefined,
280
- android: height,
281
- }),
122
+ height: containerHeight,
282
123
  },
283
- section: {
124
+ headerTitle: {
125
+ textAlign: 'left',
284
126
  flex: 1,
285
127
  },
286
- sectionHeader: {
287
- flexDirection: 'row',
288
- justifyContent: 'space-between',
289
- paddingTop: 24,
290
- paddingBottom: 8,
291
- paddingHorizontal: 16,
292
- },
293
- selectTeamsButton: {},
294
- row: {},
295
- rowImage: {
296
- width: THUMBNAIL_WIDTH,
297
- height: THUMBNAIL_HEIGHT,
298
- borderRadius: 4,
299
- },
300
- rowIconRight: {
301
- marginLeft: 'auto',
302
- color: theme.colors.statusSuccessIcon,
303
- },
304
- rowTitle: {
305
- fontSize: 16,
306
- },
307
128
  filterBar: {
308
129
  backgroundColor: theme.colors.fillColorNeutral100Inverted,
309
130
  flexDirection: 'row',
@@ -322,167 +143,10 @@ const useStyles = () => {
322
143
  gap: 8,
323
144
  alignItems: 'center',
324
145
  },
325
- })
326
- }
327
-
328
- type HeaderProps = {
329
- title: string
330
- }
331
-
332
- const Header = ({ title }: HeaderProps) => {
333
- const styles = useStyles()
334
-
335
- return (
336
- <View style={styles.sectionHeader}>
337
- <Heading variant="h3">{title}</Heading>
338
- </View>
339
- )
340
- }
341
-
342
- type FilterProps = { group_source_app_name?: string; isActive: boolean; filter: FilterTypes }
343
-
344
- const FilterRow = ({ group_source_app_name, isActive, filter }: FilterProps) => {
345
- const styles = useStyles()
346
- const { setAppFilter } = useContext(FilterContext)
347
-
348
- return (
349
- <PressableRow style={styles.row} onPress={() => setAppFilter({ group_source_app_name })}>
350
- <Text>{filter}</Text>
351
- {isActive ? <Icon name="general.check" size={16} style={styles.rowIconRight} /> : null}
352
- </PressableRow>
353
- )
354
- }
355
-
356
- type GroupRowProps = {
357
- group: FilteredGroup
358
- isActive: boolean
359
- }
360
-
361
- const GroupRow = ({ group, isActive }: GroupRowProps) => {
362
- const styles = useStyles()
363
- const { setGroupFilter } = useContext(FilterContext)
364
-
365
- const handleFilterByGroup = () => {
366
- setGroupFilter({ chat_group_graph_id: group.id })
367
- }
368
-
369
- const { headerImage, membershipsCount } = group
370
-
371
- return (
372
- <PressableRow style={styles.row} onPress={handleFilterByGroup}>
373
- {headerImage?.thumbnail && (
374
- <Image
375
- source={{ uri: headerImage?.thumbnail }}
376
- resizeMode="cover"
377
- style={styles.rowImage}
378
- alt={`Image for ${group.name}`}
379
- />
380
- )}
381
- <View>
382
- <Heading variant="h3" style={styles.rowTitle}>
383
- {group.name}
384
- </Heading>
385
- {membershipsCount && <Text>{group.membershipsCount} members</Text>}
386
- </View>
387
- {isActive ? <Icon name="general.check" size={16} style={styles.rowIconRight} /> : null}
388
- </PressableRow>
389
- )
390
- }
391
-
392
- type TeamRowProps = {
393
- team: Partial<TeamOptionResponseItem> & Pick<GroupResource, 'id'>
394
- isActive: boolean
395
- }
396
-
397
- const TeamRow = ({ team }: TeamRowProps) => {
398
- const styles = useStyles()
399
- const { setGroupFilter } = useContext(FilterContext)
400
- const servicesTeams = useServicesTeamsMap()
401
- const route = useRoute<RouteProp<ConversationFiltersScreenProps['route']>>()
402
- const { chat_group_graph_id } = route.params
403
- const isActive = chat_group_graph_id === team.id.toString()
404
-
405
- const handleFilterByGroup = () => {
406
- setGroupFilter({ chat_group_graph_id: team.id })
407
- }
408
-
409
- const servicesTeam = servicesTeams[team.id]
410
- const serviceTypeName = servicesTeam?.serviceTypeName || servicesTeam?.group
411
-
412
- return (
413
- <PressableRow style={styles.row} onPress={handleFilterByGroup}>
414
- <View>
415
- <Heading variant="h3" style={styles.rowTitle}>
416
- {team.name}
417
- </Heading>
418
- <Text>{serviceTypeName}</Text>
419
- </View>
420
- {isActive ? <Icon name="general.check" size={16} style={styles.rowIconRight} /> : null}
421
- </PressableRow>
422
- )
423
- }
424
-
425
- const PressableRow = ({
426
- children,
427
- onPress,
428
- style,
429
- }: PropsWithChildren<{ onPress: () => void; style?: ViewStyle }>) => {
430
- const styles = useRowStyles()
431
- return (
432
- <PlatformPressable style={styles.container} onPress={onPress}>
433
- <View style={[styles.innerContainer, style]}>{children}</View>
434
- </PlatformPressable>
435
- )
436
- }
437
-
438
- const useRowStyles = () => {
439
- const theme = useTheme()
440
- return StyleSheet.create({
441
- container: {
442
- paddingLeft: 16,
443
- },
444
- innerContainer: {
146
+ headerRight: {
445
147
  flexDirection: 'row',
148
+ gap: 8,
446
149
  alignItems: 'center',
447
- gap: 12,
448
- borderBottomWidth: 1,
449
- borderBottomColor: theme.colors.fillColorNeutral050Base,
450
- paddingVertical: 12,
451
- paddingRight: 16,
452
150
  },
453
151
  })
454
152
  }
455
-
456
- const useTeamsToFilter = () => {
457
- const { data: teams = [], fetchNextPage } = useTeams()
458
-
459
- return { teams, fetchNextPage }
460
- }
461
-
462
- interface FilteredGroup
463
- extends GroupResource,
464
- Partial<Pick<GroupsGroupResource, 'headerImage' | 'membershipsCount'>> {}
465
-
466
- const useGroupsToFilter = () => {
467
- const { data: groups = [] } = useGroups({ source_app_name: 'Groups', source_type: 'Group' })
468
- const { data: groupsData = [], fetchNextPage } = useGroupsGroups()
469
-
470
- const filteredGroups: FilteredGroup[] = useMemo(
471
- () =>
472
- groups
473
- .map(group => {
474
- const groupsGroup = groupsData.find(g => group.id.includes(g.id))
475
- const { membershipsCount, headerImage } = groupsGroup || {}
476
-
477
- return {
478
- ...group,
479
- membershipsCount,
480
- headerImage,
481
- }
482
- })
483
- .filter(g => typeof g !== 'undefined'),
484
- [groups, groupsData]
485
- )
486
-
487
- return { groups: filteredGroups, fetchNextPage }
488
- }