@planningcenter/chat-react-native 4.2.0-rc.1 → 4.2.0-rc.2
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/contexts/api_provider.d.ts.map +1 -1
- package/build/contexts/api_provider.js +12 -1
- package/build/contexts/api_provider.js.map +1 -1
- package/build/hooks/use_features.d.ts +16 -0
- package/build/hooks/use_features.d.ts.map +1 -1
- package/build/hooks/use_features.js +43 -47
- package/build/hooks/use_features.js.map +1 -1
- package/build/screens/conversations/components/search_list_header_component.d.ts +2 -0
- package/build/screens/conversations/components/search_list_header_component.d.ts.map +1 -0
- package/build/screens/conversations/components/search_list_header_component.js +496 -0
- package/build/screens/conversations/components/search_list_header_component.js.map +1 -0
- package/build/screens/conversations/conversations_screen.d.ts.map +1 -1
- package/build/screens/conversations/conversations_screen.js +28 -16
- package/build/screens/conversations/conversations_screen.js.map +1 -1
- package/build/utils/time.d.ts +1 -0
- package/build/utils/time.d.ts.map +1 -1
- package/build/utils/time.js +1 -0
- package/build/utils/time.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/hooks/use_features.test.ts +61 -0
- package/src/contexts/api_provider.tsx +4 -0
- package/src/hooks/use_features.ts +26 -14
- package/src/screens/conversations/components/search_list_header_component.tsx +291 -0
- package/src/screens/conversations/conversations_screen.tsx +6 -1
- package/src/utils/time.ts +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time.d.ts","sourceRoot":"","sources":["../../src/utils/time.ts"],"names":[],"mappings":"AAAA,KAAK,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAA;AAE/C,wBAAgB,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,CAS1D;AAED,eAAO,MAAM,YAAY,QAAqB,CAAA;AAC9C,eAAO,MAAM,YAAY,QAAqB,CAAA;AAC9C,eAAO,MAAM,cAAc,QAAsB,CAAA;AACjD,eAAO,MAAM,QAAQ,QAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"time.d.ts","sourceRoot":"","sources":["../../src/utils/time.ts"],"names":[],"mappings":"AAAA,KAAK,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAA;AAE/C,wBAAgB,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,CAS1D;AAED,eAAO,MAAM,YAAY,QAAqB,CAAA;AAC9C,eAAO,MAAM,UAAU,QAAqB,CAAA;AAC5C,eAAO,MAAM,YAAY,QAAqB,CAAA;AAC9C,eAAO,MAAM,cAAc,QAAsB,CAAA;AACjD,eAAO,MAAM,QAAQ,QAAmB,CAAA"}
|
package/build/utils/time.js
CHANGED
|
@@ -9,6 +9,7 @@ export function toMs(value, unit) {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
export const FIVE_SECONDS = toMs(5, 'seconds');
|
|
12
|
+
export const ONE_MINUTE = toMs(1, 'minutes');
|
|
12
13
|
export const FIVE_MINUTES = toMs(5, 'minutes');
|
|
13
14
|
export const THIRTY_MINUTES = toMs(30, 'minutes');
|
|
14
15
|
export const ONE_HOUR = toMs(1, 'hours');
|
package/build/utils/time.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time.js","names":["toMs","value","unit","FIVE_SECONDS","FIVE_MINUTES","THIRTY_MINUTES","ONE_HOUR"],"sources":["../../src/utils/time.ts"],"sourcesContent":["type TimeUnit = 'hours' | 'minutes' | 'seconds'\n\nexport function toMs(value: number, unit: TimeUnit): number {\n switch (unit) {\n case 'hours':\n return value * 60 * 60 * 1000\n case 'minutes':\n return value * 60 * 1000\n case 'seconds':\n return value * 1000\n }\n}\n\nexport const FIVE_SECONDS = toMs(5, 'seconds')\nexport const FIVE_MINUTES = toMs(5, 'minutes')\nexport const THIRTY_MINUTES = toMs(30, 'minutes')\nexport const ONE_HOUR = toMs(1, 'hours')\n"],"mappings":"AAEA,OAAO,SAASA,IAAIA,CAACC,KAAa,EAAEC,IAAc,EAAU;EAC1D,QAAQA,IAAI;IACV,KAAK,OAAO;MACV,OAAOD,KAAK,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI;IAC/B,KAAK,SAAS;MACZ,OAAOA,KAAK,GAAG,EAAE,GAAG,IAAI;IAC1B,KAAK,SAAS;MACZ,OAAOA,KAAK,GAAG,IAAI;EACvB;AACF;AAEA,OAAO,MAAME,YAAY,GAAGH,IAAI,CAAC,CAAC,EAAE,SAAS,CAAC;AAC9C,OAAO,MAAMI,
|
|
1
|
+
{"version":3,"file":"time.js","names":["toMs","value","unit","FIVE_SECONDS","ONE_MINUTE","FIVE_MINUTES","THIRTY_MINUTES","ONE_HOUR"],"sources":["../../src/utils/time.ts"],"sourcesContent":["type TimeUnit = 'hours' | 'minutes' | 'seconds'\n\nexport function toMs(value: number, unit: TimeUnit): number {\n switch (unit) {\n case 'hours':\n return value * 60 * 60 * 1000\n case 'minutes':\n return value * 60 * 1000\n case 'seconds':\n return value * 1000\n }\n}\n\nexport const FIVE_SECONDS = toMs(5, 'seconds')\nexport const ONE_MINUTE = toMs(1, 'minutes')\nexport const FIVE_MINUTES = toMs(5, 'minutes')\nexport const THIRTY_MINUTES = toMs(30, 'minutes')\nexport const ONE_HOUR = toMs(1, 'hours')\n"],"mappings":"AAEA,OAAO,SAASA,IAAIA,CAACC,KAAa,EAAEC,IAAc,EAAU;EAC1D,QAAQA,IAAI;IACV,KAAK,OAAO;MACV,OAAOD,KAAK,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI;IAC/B,KAAK,SAAS;MACZ,OAAOA,KAAK,GAAG,EAAE,GAAG,IAAI;IAC1B,KAAK,SAAS;MACZ,OAAOA,KAAK,GAAG,IAAI;EACvB;AACF;AAEA,OAAO,MAAME,YAAY,GAAGH,IAAI,CAAC,CAAC,EAAE,SAAS,CAAC;AAC9C,OAAO,MAAMI,UAAU,GAAGJ,IAAI,CAAC,CAAC,EAAE,SAAS,CAAC;AAC5C,OAAO,MAAMK,YAAY,GAAGL,IAAI,CAAC,CAAC,EAAE,SAAS,CAAC;AAC9C,OAAO,MAAMM,cAAc,GAAGN,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC;AACjD,OAAO,MAAMO,QAAQ,GAAGP,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/chat-react-native",
|
|
3
|
-
"version": "4.2.0-rc.
|
|
3
|
+
"version": "4.2.0-rc.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -84,5 +84,5 @@
|
|
|
84
84
|
"sf-symbols-typescript": "^2.2.0",
|
|
85
85
|
"typescript": "~6.0.3"
|
|
86
86
|
},
|
|
87
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "94508a030b77e376d9a99fd0883290d8a71e778f"
|
|
88
88
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { buildTestQueryClient } from '../../__utils__/query_client'
|
|
2
|
+
import type { ApiClient } from '../../hooks/use_api_client'
|
|
3
|
+
import { featuresQueryOptions } from '../../hooks/use_features'
|
|
4
|
+
|
|
5
|
+
const features = {
|
|
6
|
+
data: [{ type: 'Feature', id: '1', name: 'ROLLOUT_search', enabled: true }],
|
|
7
|
+
links: {},
|
|
8
|
+
meta: { count: 1, totalCount: 1 },
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const buildClient = (get: jest.Mock) => ({ chat: { get } }) as unknown as ApiClient
|
|
12
|
+
|
|
13
|
+
const fetchTwice = async (get: jest.Mock, secondsBetween: number) => {
|
|
14
|
+
const queryClient = buildTestQueryClient()
|
|
15
|
+
const options = featuresQueryOptions(buildClient(get))
|
|
16
|
+
|
|
17
|
+
await queryClient.fetchQuery(options)
|
|
18
|
+
jest.advanceTimersByTime(secondsBetween * 1000)
|
|
19
|
+
await queryClient.fetchQuery(options)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('featuresQueryOptions', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
jest.useFakeTimers()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
jest.useRealTimers()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('keeps successful flags fresh for five minutes', async () => {
|
|
32
|
+
const get = jest.fn().mockResolvedValue(features)
|
|
33
|
+
|
|
34
|
+
await fetchTwice(get, 4 * 60)
|
|
35
|
+
|
|
36
|
+
expect(get).toHaveBeenCalledTimes(1)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('refetches right away after a failed fetch', async () => {
|
|
40
|
+
const get = jest.fn().mockRejectedValue({ status: 503 })
|
|
41
|
+
|
|
42
|
+
await fetchTwice(get, 1)
|
|
43
|
+
|
|
44
|
+
expect(get).toHaveBeenCalledTimes(2)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('backs off for a minute after a 429', async () => {
|
|
48
|
+
const get = jest.fn().mockRejectedValue({ status: 429 })
|
|
49
|
+
const queryClient = buildTestQueryClient()
|
|
50
|
+
const options = featuresQueryOptions(buildClient(get))
|
|
51
|
+
|
|
52
|
+
await queryClient.fetchQuery(options)
|
|
53
|
+
jest.advanceTimersByTime(30 * 1000)
|
|
54
|
+
await queryClient.fetchQuery(options)
|
|
55
|
+
expect(get).toHaveBeenCalledTimes(1)
|
|
56
|
+
|
|
57
|
+
jest.advanceTimersByTime(31 * 1000)
|
|
58
|
+
await queryClient.fetchQuery(options)
|
|
59
|
+
expect(get).toHaveBeenCalledTimes(2)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -10,6 +10,7 @@ import { ViewProps } from 'react-native'
|
|
|
10
10
|
import { ApiClient, useApiClient } from '../hooks/use_api_client'
|
|
11
11
|
import { useAppState } from '../hooks/use_app_state'
|
|
12
12
|
import { appGrantsRequestArgs } from '../hooks/use_chat_permissions'
|
|
13
|
+
import { featuresQueryOptions } from '../hooks/use_features'
|
|
13
14
|
import { getRequestQueryKey, RequestQueryKey } from '../hooks/use_suspense_api'
|
|
14
15
|
import { emitAuthRefresh } from '../utils/auth_events'
|
|
15
16
|
import { ResponseError } from '../utils/response_error'
|
|
@@ -81,10 +82,13 @@ export function ApiProvider({ children }: ViewProps) {
|
|
|
81
82
|
// This needs to live in the provider so that it can access the api client
|
|
82
83
|
// and the chat query client
|
|
83
84
|
const PrefetchQueries = () => {
|
|
85
|
+
const client = useApiClient()
|
|
86
|
+
|
|
84
87
|
usePrefetchQuery({
|
|
85
88
|
queryKey: getRequestQueryKey(appGrantsRequestArgs),
|
|
86
89
|
queryFn: defaultQueryFn,
|
|
87
90
|
})
|
|
91
|
+
usePrefetchQuery(featuresQueryOptions(client))
|
|
88
92
|
|
|
89
93
|
return null
|
|
90
94
|
}
|
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
import { useSuspenseQuery } from '@tanstack/react-query'
|
|
1
|
+
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'
|
|
2
2
|
import { useCallback } from 'react'
|
|
3
3
|
import { ApiCollection } from '../types'
|
|
4
4
|
import type { FeatureResource } from '../types/resources/feature_resource'
|
|
5
5
|
import { getFeaturesRequestArgs, getFeaturesQueryKey } from '../utils/request/get_features'
|
|
6
|
-
import { FIVE_MINUTES } from '../utils/time'
|
|
7
|
-
import { useApiClient } from './use_api_client'
|
|
6
|
+
import { FIVE_MINUTES, ONE_MINUTE } from '../utils/time'
|
|
7
|
+
import { ApiClient, useApiClient } from './use_api_client'
|
|
8
8
|
|
|
9
9
|
type FeatureName = (typeof availableFeatures)[keyof typeof availableFeatures]
|
|
10
10
|
|
|
11
|
+
type FeaturesCollection = ApiCollection<FeatureResource> & {
|
|
12
|
+
failure?: { status?: number }
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
export function useFeatures() {
|
|
12
16
|
const apiClient = useApiClient()
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const { data } = useSuspenseQuery({
|
|
16
|
-
queryKey: getFeaturesQueryKey(),
|
|
17
|
-
queryFn: () => {
|
|
18
|
-
return apiClient.chat
|
|
19
|
-
.get<ApiCollection<FeatureResource>>(requestArgs)
|
|
20
|
-
.catch(() => stableEmptyFeatures)
|
|
21
|
-
},
|
|
22
|
-
staleTime: FIVE_MINUTES,
|
|
23
|
-
})
|
|
17
|
+
const { data } = useSuspenseQuery(featuresQueryOptions(apiClient))
|
|
24
18
|
|
|
25
19
|
const features = data.data
|
|
26
20
|
|
|
@@ -36,6 +30,23 @@ export function useFeatures() {
|
|
|
36
30
|
}
|
|
37
31
|
}
|
|
38
32
|
|
|
33
|
+
export const featuresQueryOptions = (apiClient: ApiClient) =>
|
|
34
|
+
queryOptions({
|
|
35
|
+
queryKey: getFeaturesQueryKey(),
|
|
36
|
+
queryFn: (): Promise<FeaturesCollection> => {
|
|
37
|
+
return apiClient.chat
|
|
38
|
+
.get<ApiCollection<FeatureResource>>(getFeaturesRequestArgs())
|
|
39
|
+
.catch(error => ({ ...stableEmptyFeatures, failure: { status: error?.status } }))
|
|
40
|
+
},
|
|
41
|
+
staleTime: query => {
|
|
42
|
+
const failure = query.state.data?.failure
|
|
43
|
+
if (!failure) return FIVE_MINUTES
|
|
44
|
+
|
|
45
|
+
return failure.status === 429 ? ONE_MINUTE : 0
|
|
46
|
+
},
|
|
47
|
+
gcTime: Infinity,
|
|
48
|
+
})
|
|
49
|
+
|
|
39
50
|
export const availableFeatures = {
|
|
40
51
|
gender_specific_conversations: 'ROLLOUT_gender_specific_conversations',
|
|
41
52
|
message_reporting: 'ROLLOUT_MOBILE_message_reporting',
|
|
@@ -44,6 +55,7 @@ export const availableFeatures = {
|
|
|
44
55
|
conversation_safety_lock: 'ROLLOUT_conversation_safety_lock',
|
|
45
56
|
video_moderation: 'ROLLOUT_MOBILE_video_moderation',
|
|
46
57
|
dm_search_clear_fix: 'ROLLOUT_MOBILE_dm_search_clear_fix',
|
|
58
|
+
search: 'ROLLOUT_search',
|
|
47
59
|
} as const satisfies Record<string, `ROLLOUT_${string}`>
|
|
48
60
|
|
|
49
61
|
const stableEmptyFeatures: ApiCollection<FeatureResource> = {
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'
|
|
2
|
+
import React, { useCallback, useMemo } from 'react'
|
|
3
|
+
import { Alert, ScrollView, StyleSheet, View } from 'react-native'
|
|
4
|
+
import { Heading, TextButton, ToggleButton } from '../../../components'
|
|
5
|
+
import {
|
|
6
|
+
useAtFontScaleBreakpoint,
|
|
7
|
+
useCanCreateConversations,
|
|
8
|
+
useCurrentPersonCache,
|
|
9
|
+
useDirectMessagesEligible,
|
|
10
|
+
useHasDirectMessages,
|
|
11
|
+
useNewConversationEntry,
|
|
12
|
+
useTheme,
|
|
13
|
+
} from '../../../hooks'
|
|
14
|
+
import { useMarkAllRead } from '../../../hooks/use_conversations_actions'
|
|
15
|
+
import { useCanDisplayGroups } from '../../../hooks/use_groups'
|
|
16
|
+
import { useNewConversationFromFilter } from '../../../hooks/use_new_conversation_from_filter'
|
|
17
|
+
import { GraphId } from '../../../types/resources/group_resource'
|
|
18
|
+
import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../../../utils'
|
|
19
|
+
import { Haptic } from '../../../utils/native_adapters'
|
|
20
|
+
import { ConversationFiltersParams } from '../../conversation_filters/screen_props'
|
|
21
|
+
import { ConversationsScreenProps } from '../conversations_screen'
|
|
22
|
+
import { ChatGroupBadge } from './chat_group_badge'
|
|
23
|
+
import { ListHeaderActionButton } from './list_header_action_button'
|
|
24
|
+
|
|
25
|
+
const ROW_PADDING_HORIZONTAL = 16
|
|
26
|
+
|
|
27
|
+
enum FilterTypes {
|
|
28
|
+
All = 'All',
|
|
29
|
+
Groups = 'Groups',
|
|
30
|
+
Teams = 'Teams',
|
|
31
|
+
DMs = 'DMs',
|
|
32
|
+
More = 'More',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const SearchListHeaderComponent = () => {
|
|
36
|
+
const navigation = useNavigation()
|
|
37
|
+
const canFilterByTeams = useCanDisplayGroups({ source_app_name: 'Services', source_type: 'Team' })
|
|
38
|
+
const canFilterByGroups = useCanDisplayGroups({ source_app_name: 'Groups', source_type: 'Group' })
|
|
39
|
+
const directMessagesEligible = useDirectMessagesEligible()
|
|
40
|
+
const hasDirectMessages = useHasDirectMessages()
|
|
41
|
+
const canFilterByDMs = directMessagesEligible && hasDirectMessages
|
|
42
|
+
const route = useRoute<RouteProp<ConversationsScreenProps['route']>>()
|
|
43
|
+
const {
|
|
44
|
+
chat_group_graph_id,
|
|
45
|
+
group_source_app_name = '',
|
|
46
|
+
show_direct_messages,
|
|
47
|
+
} = route.params || {}
|
|
48
|
+
const currentPersonCache = useCurrentPersonCache()
|
|
49
|
+
const { markAllRead, isPending } = useMarkAllRead()
|
|
50
|
+
const canCreateConversations = useCanCreateConversations()
|
|
51
|
+
const entryMode = useNewConversationEntry()
|
|
52
|
+
const newConversationFromFilter = useNewConversationFromFilter(
|
|
53
|
+
chat_group_graph_id as GraphId | undefined
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const active: FilterTypes = useMemo(() => {
|
|
57
|
+
if (chat_group_graph_id) {
|
|
58
|
+
return FilterTypes.More
|
|
59
|
+
} else if (/groups/i.test(group_source_app_name)) {
|
|
60
|
+
return FilterTypes.Groups
|
|
61
|
+
} else if (/services/i.test(group_source_app_name)) {
|
|
62
|
+
return FilterTypes.Teams
|
|
63
|
+
} else if (show_direct_messages) {
|
|
64
|
+
return FilterTypes.DMs
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return FilterTypes.All
|
|
68
|
+
}, [chat_group_graph_id, group_source_app_name, show_direct_messages])
|
|
69
|
+
|
|
70
|
+
const handleNewConversationNavigation = useCallback(() => {
|
|
71
|
+
if (newConversationFromFilter || entryMode === 'select_type') {
|
|
72
|
+
return navigation.navigate('ConversationSelectType', {
|
|
73
|
+
...route.params,
|
|
74
|
+
newConversationFromFilter,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (entryMode === 'groups') {
|
|
79
|
+
return navigation.navigate('New', {
|
|
80
|
+
screen: 'ConversationSelectGroupRecipients',
|
|
81
|
+
params: { ...route.params },
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (entryMode === 'teams') {
|
|
86
|
+
return navigation.navigate('New', {
|
|
87
|
+
screen: 'ConversationSelectTeamsILeadRecipients',
|
|
88
|
+
params: { ...route.params },
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (entryMode === 'direct_message') {
|
|
93
|
+
return navigation.navigate('New', {
|
|
94
|
+
screen: 'ConversationSelectDirectMessageRecipients',
|
|
95
|
+
params: { ...route.params },
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}, [navigation, route.params, entryMode, newConversationFromFilter])
|
|
99
|
+
|
|
100
|
+
const handleSetFilter = useCallback(
|
|
101
|
+
(params: ConversationFiltersParams) => {
|
|
102
|
+
navigation.setParams({
|
|
103
|
+
title: undefined,
|
|
104
|
+
chat_group_graph_id: undefined,
|
|
105
|
+
group_source_app_name: undefined,
|
|
106
|
+
show_direct_messages: undefined,
|
|
107
|
+
...params,
|
|
108
|
+
})
|
|
109
|
+
},
|
|
110
|
+
[navigation]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const handleChatSettings = useCallback(() => {
|
|
114
|
+
navigation.navigate('ChatSettings', {})
|
|
115
|
+
}, [navigation])
|
|
116
|
+
|
|
117
|
+
const handleOpenConversationFilters = useCallback(() => {
|
|
118
|
+
navigation.navigate('ConversationFilters', {
|
|
119
|
+
chat_group_graph_id,
|
|
120
|
+
group_source_app_name,
|
|
121
|
+
show_direct_messages,
|
|
122
|
+
})
|
|
123
|
+
}, [navigation, chat_group_graph_id, group_source_app_name, show_direct_messages])
|
|
124
|
+
|
|
125
|
+
const handleMarkAllRead = useCallback(() => {
|
|
126
|
+
currentPersonCache.update({ unreadCount: 0 })
|
|
127
|
+
Haptic.notificationSuccess()
|
|
128
|
+
markAllRead()
|
|
129
|
+
}, [currentPersonCache, markAllRead])
|
|
130
|
+
|
|
131
|
+
const alertMarkAllRead = useCallback(() => {
|
|
132
|
+
if (active === FilterTypes.All) return handleMarkAllRead()
|
|
133
|
+
|
|
134
|
+
Alert.alert('Mark all read', 'This includes conversations not shown by the current filter.', [
|
|
135
|
+
{ text: 'Cancel' },
|
|
136
|
+
{ text: 'OK', onPress: () => handleMarkAllRead() },
|
|
137
|
+
])
|
|
138
|
+
}, [active, handleMarkAllRead])
|
|
139
|
+
|
|
140
|
+
const hasSomeSourceFilterCapability = canFilterByTeams || canFilterByGroups
|
|
141
|
+
const hasMultiSourceFilterCapabilities = canFilterByTeams && canFilterByGroups
|
|
142
|
+
const showFilterRow = hasMultiSourceFilterCapabilities || canFilterByDMs
|
|
143
|
+
const shouldShowHeaderFiltersButton = hasSomeSourceFilterCapability && !showFilterRow
|
|
144
|
+
const moreFiltersButtonA11yLabel = shouldShowHeaderFiltersButton
|
|
145
|
+
? 'View filters'
|
|
146
|
+
: 'View more filters'
|
|
147
|
+
|
|
148
|
+
const moreFiltersButtonProps = {
|
|
149
|
+
onPress: handleOpenConversationFilters,
|
|
150
|
+
active: active === FilterTypes.More,
|
|
151
|
+
iconNameRight: 'general.threeReducingHorizontalBars' as const,
|
|
152
|
+
accessibilityLabel: moreFiltersButtonA11yLabel,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const styles = useStyles({ shouldShowHeaderFiltersButton })
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<View style={styles.listHeader}>
|
|
159
|
+
<View style={styles.headingRow}>
|
|
160
|
+
<View style={styles.titleContainer}>
|
|
161
|
+
<Heading numberOfLines={1} variant="h2">
|
|
162
|
+
Conversations
|
|
163
|
+
</Heading>
|
|
164
|
+
<TextButton
|
|
165
|
+
onPress={alertMarkAllRead}
|
|
166
|
+
disabled={isPending}
|
|
167
|
+
maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
|
|
168
|
+
accessibilityShowsLargeContentViewer
|
|
169
|
+
accessibilityLargeContentTitle="Mark all read"
|
|
170
|
+
>
|
|
171
|
+
Mark all read
|
|
172
|
+
</TextButton>
|
|
173
|
+
</View>
|
|
174
|
+
<View style={styles.actionsContainer}>
|
|
175
|
+
<ListHeaderActionButton
|
|
176
|
+
name="general.cog"
|
|
177
|
+
onPress={handleChatSettings}
|
|
178
|
+
accessibilityLabel="Chat settings"
|
|
179
|
+
testID="conversations-chat-settings-button"
|
|
180
|
+
maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
|
|
181
|
+
/>
|
|
182
|
+
{canCreateConversations && (
|
|
183
|
+
<ListHeaderActionButton
|
|
184
|
+
name="churchCenter.signups"
|
|
185
|
+
onPress={handleNewConversationNavigation}
|
|
186
|
+
accessibilityLabel="New conversation"
|
|
187
|
+
testID="conversations-new-conversation-button"
|
|
188
|
+
maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
|
|
189
|
+
/>
|
|
190
|
+
)}
|
|
191
|
+
{shouldShowHeaderFiltersButton && <ToggleButton {...moreFiltersButtonProps} />}
|
|
192
|
+
</View>
|
|
193
|
+
</View>
|
|
194
|
+
{showFilterRow && (
|
|
195
|
+
<ScrollView
|
|
196
|
+
horizontal
|
|
197
|
+
showsHorizontalScrollIndicator={false}
|
|
198
|
+
contentContainerStyle={styles.filterRow}
|
|
199
|
+
accessibilityRole="toolbar"
|
|
200
|
+
>
|
|
201
|
+
<ToggleButton
|
|
202
|
+
title={FilterTypes.All}
|
|
203
|
+
active={active === FilterTypes.All}
|
|
204
|
+
onPress={() => handleSetFilter({})}
|
|
205
|
+
accessibilityLabel="Show all conversations"
|
|
206
|
+
style={styles.filterButton}
|
|
207
|
+
/>
|
|
208
|
+
{canFilterByGroups && (
|
|
209
|
+
<ToggleButton
|
|
210
|
+
title={FilterTypes.Groups}
|
|
211
|
+
active={active === FilterTypes.Groups}
|
|
212
|
+
onPress={() => handleSetFilter({ group_source_app_name: 'Groups' })}
|
|
213
|
+
accessibilityLabel="Filter to group conversations"
|
|
214
|
+
style={styles.filterButton}
|
|
215
|
+
/>
|
|
216
|
+
)}
|
|
217
|
+
{canFilterByTeams && (
|
|
218
|
+
<ToggleButton
|
|
219
|
+
title={FilterTypes.Teams}
|
|
220
|
+
active={active === FilterTypes.Teams}
|
|
221
|
+
onPress={() => handleSetFilter({ group_source_app_name: 'Services' })}
|
|
222
|
+
accessibilityLabel="Filter to team conversations"
|
|
223
|
+
style={styles.filterButton}
|
|
224
|
+
/>
|
|
225
|
+
)}
|
|
226
|
+
{canFilterByDMs && (
|
|
227
|
+
<ToggleButton
|
|
228
|
+
title={FilterTypes.DMs}
|
|
229
|
+
active={active === FilterTypes.DMs}
|
|
230
|
+
onPress={() => handleSetFilter({ show_direct_messages: true })}
|
|
231
|
+
accessibilityLabel="Filter to direct message conversations"
|
|
232
|
+
style={styles.filterButton}
|
|
233
|
+
/>
|
|
234
|
+
)}
|
|
235
|
+
<ToggleButton {...moreFiltersButtonProps} />
|
|
236
|
+
</ScrollView>
|
|
237
|
+
)}
|
|
238
|
+
<ChatGroupBadge rowPaddingHorizontal={ROW_PADDING_HORIZONTAL} />
|
|
239
|
+
</View>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const useStyles = ({
|
|
244
|
+
shouldShowHeaderFiltersButton,
|
|
245
|
+
}: {
|
|
246
|
+
shouldShowHeaderFiltersButton: boolean
|
|
247
|
+
}) => {
|
|
248
|
+
const theme = useTheme()
|
|
249
|
+
const atFontScaleBreakpoint = useAtFontScaleBreakpoint(
|
|
250
|
+
shouldShowHeaderFiltersButton ? 1.2 : undefined
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return StyleSheet.create({
|
|
254
|
+
listHeader: {
|
|
255
|
+
borderBottomWidth: 1,
|
|
256
|
+
borderBottomColor: theme.colors.statusErrorBorder,
|
|
257
|
+
paddingBottom: 16,
|
|
258
|
+
gap: 16,
|
|
259
|
+
},
|
|
260
|
+
headingRow: {
|
|
261
|
+
gap: 12,
|
|
262
|
+
flexDirection: atFontScaleBreakpoint ? 'column' : 'row',
|
|
263
|
+
alignItems: atFontScaleBreakpoint ? 'flex-start' : 'center',
|
|
264
|
+
justifyContent: 'space-between',
|
|
265
|
+
paddingBottom: atFontScaleBreakpoint ? 4 : 0,
|
|
266
|
+
paddingHorizontal: ROW_PADDING_HORIZONTAL,
|
|
267
|
+
},
|
|
268
|
+
titleContainer: {
|
|
269
|
+
gap: 8,
|
|
270
|
+
},
|
|
271
|
+
actionsContainer: {
|
|
272
|
+
flexDirection: 'row',
|
|
273
|
+
gap: 8,
|
|
274
|
+
alignItems: 'center',
|
|
275
|
+
},
|
|
276
|
+
markAllReadRow: {
|
|
277
|
+
paddingBottom: 4,
|
|
278
|
+
},
|
|
279
|
+
filterRow: {
|
|
280
|
+
flexDirection: 'row',
|
|
281
|
+
justifyContent: 'flex-start',
|
|
282
|
+
gap: 8,
|
|
283
|
+
paddingHorizontal: ROW_PADDING_HORIZONTAL,
|
|
284
|
+
flexGrow: 1,
|
|
285
|
+
},
|
|
286
|
+
filterButton: {
|
|
287
|
+
flex: 1,
|
|
288
|
+
minWidth: 65,
|
|
289
|
+
},
|
|
290
|
+
})
|
|
291
|
+
}
|
|
@@ -3,10 +3,12 @@ import React from 'react'
|
|
|
3
3
|
import { StyleSheet, View } from 'react-native'
|
|
4
4
|
import { Conversations } from '../../components'
|
|
5
5
|
import { ConversationsContextProvider } from '../../contexts/conversations_context'
|
|
6
|
+
import { availableFeatures, useFeatures } from '../../hooks/use_features'
|
|
6
7
|
import { analyticsEvents, usePublishProductAnalyticsEvent } from '../../hooks/use_product_analytics'
|
|
7
8
|
import { AppName } from '../../types/resources/app_name'
|
|
8
9
|
import { GraphId } from '../../types/resources/group_resource'
|
|
9
10
|
import { ListHeaderComponent } from './components/list_header_component'
|
|
11
|
+
import { SearchListHeaderComponent } from './components/search_list_header_component'
|
|
10
12
|
|
|
11
13
|
export type ConversationsScreenProps = StaticScreenProps<{
|
|
12
14
|
title?: string
|
|
@@ -17,13 +19,16 @@ export type ConversationsScreenProps = StaticScreenProps<{
|
|
|
17
19
|
|
|
18
20
|
export function ConversationsScreen({ route }: ConversationsScreenProps) {
|
|
19
21
|
const styles = useStyles()
|
|
22
|
+
const { featureEnabled } = useFeatures()
|
|
23
|
+
const searchEnabled = featureEnabled(availableFeatures.search)
|
|
24
|
+
const Header = searchEnabled ? SearchListHeaderComponent : ListHeaderComponent
|
|
20
25
|
|
|
21
26
|
usePublishProductAnalyticsEvent(analyticsEvents.conversation_index_opened)
|
|
22
27
|
|
|
23
28
|
return (
|
|
24
29
|
<View style={styles.container}>
|
|
25
30
|
<ConversationsContextProvider args={route.params} opts={{ throwOnError: true }}>
|
|
26
|
-
<Conversations ListHeaderComponent={
|
|
31
|
+
<Conversations ListHeaderComponent={Header} />
|
|
27
32
|
</ConversationsContextProvider>
|
|
28
33
|
</View>
|
|
29
34
|
)
|
package/src/utils/time.ts
CHANGED
|
@@ -12,6 +12,7 @@ export function toMs(value: number, unit: TimeUnit): number {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export const FIVE_SECONDS = toMs(5, 'seconds')
|
|
15
|
+
export const ONE_MINUTE = toMs(1, 'minutes')
|
|
15
16
|
export const FIVE_MINUTES = toMs(5, 'minutes')
|
|
16
17
|
export const THIRTY_MINUTES = toMs(30, 'minutes')
|
|
17
18
|
export const ONE_HOUR = toMs(1, 'hours')
|