@planningcenter/chat-react-native 3.39.0-rc.0 → 3.39.0-rc.10
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/conversation/messages_disabled_banners.d.ts +6 -1
- package/build/components/conversation/messages_disabled_banners.d.ts.map +1 -1
- package/build/components/conversation/messages_disabled_banners.js +8 -2
- package/build/components/conversation/messages_disabled_banners.js.map +1 -1
- package/build/contexts/api_provider.js +2 -1
- package/build/contexts/api_provider.js.map +1 -1
- package/build/contexts/conversations_context.d.ts.map +1 -1
- package/build/contexts/conversations_context.js +6 -2
- package/build/contexts/conversations_context.js.map +1 -1
- package/build/hooks/direct_messages/use_direct_message_affiliations.d.ts +5 -0
- package/build/hooks/direct_messages/use_direct_message_affiliations.d.ts.map +1 -0
- package/build/hooks/direct_messages/use_direct_message_affiliations.js +20 -0
- package/build/hooks/direct_messages/use_direct_message_affiliations.js.map +1 -0
- package/build/hooks/direct_messages/use_direct_message_candidates.d.ts +4 -0
- package/build/hooks/direct_messages/use_direct_message_candidates.d.ts.map +1 -0
- package/build/hooks/direct_messages/use_direct_message_candidates.js +28 -0
- package/build/hooks/direct_messages/use_direct_message_candidates.js.map +1 -0
- package/build/hooks/direct_messages/use_direct_message_conversation_validate.d.ts +11 -0
- package/build/hooks/direct_messages/use_direct_message_conversation_validate.d.ts.map +1 -0
- package/build/hooks/direct_messages/use_direct_message_conversation_validate.js +25 -0
- package/build/hooks/direct_messages/use_direct_message_conversation_validate.js.map +1 -0
- package/build/hooks/direct_messages/use_find_or_create_direct_message_conversation.d.ts +13 -0
- package/build/hooks/direct_messages/use_find_or_create_direct_message_conversation.d.ts.map +1 -0
- package/build/hooks/direct_messages/use_find_or_create_direct_message_conversation.js +51 -0
- package/build/hooks/direct_messages/use_find_or_create_direct_message_conversation.js.map +1 -0
- package/build/hooks/groups/use_group_chat_conversation_payload.d.ts.map +1 -1
- package/build/hooks/groups/use_group_chat_conversation_payload.js +2 -1
- package/build/hooks/groups/use_group_chat_conversation_payload.js.map +1 -1
- package/build/hooks/index.d.ts +1 -0
- package/build/hooks/index.d.ts.map +1 -1
- package/build/hooks/index.js +1 -0
- package/build/hooks/index.js.map +1 -1
- package/build/hooks/services/use_services_chat_conversation_payload.d.ts.map +1 -1
- package/build/hooks/services/use_services_chat_conversation_payload.js +2 -1
- package/build/hooks/services/use_services_chat_conversation_payload.js.map +1 -1
- package/build/hooks/use_api.d.ts +5 -1
- package/build/hooks/use_api.d.ts.map +1 -1
- package/build/hooks/use_api.js +2 -1
- package/build/hooks/use_api.js.map +1 -1
- package/build/hooks/use_chat_configuration.d.ts.map +1 -1
- package/build/hooks/use_chat_configuration.js +2 -1
- package/build/hooks/use_chat_configuration.js.map +1 -1
- package/build/hooks/use_chat_permissions.d.ts.map +1 -1
- package/build/hooks/use_chat_permissions.js +4 -1
- package/build/hooks/use_chat_permissions.js.map +1 -1
- package/build/hooks/use_conversation.d.ts.map +1 -1
- package/build/hooks/use_conversation.js +1 -0
- package/build/hooks/use_conversation.js.map +1 -1
- package/build/hooks/use_conversation_jolt_events.d.ts.map +1 -1
- package/build/hooks/use_conversation_jolt_events.js +2 -2
- package/build/hooks/use_conversation_jolt_events.js.map +1 -1
- package/build/hooks/use_current_person.d.ts.map +1 -1
- package/build/hooks/use_current_person.js +4 -2
- package/build/hooks/use_current_person.js.map +1 -1
- package/build/hooks/use_features.d.ts.map +1 -1
- package/build/hooks/use_features.js +2 -1
- package/build/hooks/use_features.js.map +1 -1
- package/build/hooks/use_groups.d.ts.map +1 -1
- package/build/hooks/use_groups.js +2 -1
- package/build/hooks/use_groups.js.map +1 -1
- package/build/hooks/use_groups_groups.d.ts.map +1 -1
- package/build/hooks/use_groups_groups.js +2 -1
- package/build/hooks/use_groups_groups.js.map +1 -1
- package/build/hooks/use_has_direct_messages.d.ts +2 -0
- package/build/hooks/use_has_direct_messages.d.ts.map +1 -0
- package/build/hooks/use_has_direct_messages.js +17 -0
- package/build/hooks/use_has_direct_messages.js.map +1 -0
- package/build/hooks/use_jolt.d.ts.map +1 -1
- package/build/hooks/use_jolt.js +2 -1
- package/build/hooks/use_jolt.js.map +1 -1
- package/build/hooks/use_organization.d.ts.map +1 -1
- package/build/hooks/use_organization.js +4 -1
- package/build/hooks/use_organization.js.map +1 -1
- package/build/hooks/use_person_settings.d.ts +6 -0
- package/build/hooks/use_person_settings.d.ts.map +1 -0
- package/build/hooks/use_person_settings.js +26 -0
- package/build/hooks/use_person_settings.js.map +1 -0
- package/build/navigation/index.d.ts +13 -0
- package/build/navigation/index.d.ts.map +1 -1
- package/build/navigation/index.js +9 -0
- package/build/navigation/index.js.map +1 -1
- package/build/screens/conversation_details_screen.d.ts.map +1 -1
- package/build/screens/conversation_details_screen.js +2 -1
- package/build/screens/conversation_details_screen.js.map +1 -1
- package/build/screens/conversation_filters/components/conversation_filters.d.ts.map +1 -1
- package/build/screens/conversation_filters/components/conversation_filters.js +20 -5
- package/build/screens/conversation_filters/components/conversation_filters.js.map +1 -1
- package/build/screens/conversation_filters/components/rows.d.ts +2 -1
- package/build/screens/conversation_filters/components/rows.d.ts.map +1 -1
- package/build/screens/conversation_filters/components/rows.js +2 -2
- package/build/screens/conversation_filters/components/rows.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 +4 -0
- package/build/screens/conversation_filters/context/conversation_filter_context.js.map +1 -1
- package/build/screens/conversation_filters/filter_types.d.ts +1 -0
- package/build/screens/conversation_filters/filter_types.d.ts.map +1 -1
- package/build/screens/conversation_filters/filter_types.js +1 -0
- package/build/screens/conversation_filters/filter_types.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/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +1 -1
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.d.ts +1 -2
- package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.d.ts.map +1 -1
- package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.js +319 -10
- package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.js.map +1 -1
- package/build/screens/conversations/components/list_header_component.d.ts.map +1 -1
- package/build/screens/conversations/components/list_header_component.js +35 -17
- package/build/screens/conversations/components/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.map +1 -1
- package/build/screens/get_help_screen.d.ts.map +1 -1
- package/build/screens/get_help_screen.js +2 -18
- package/build/screens/get_help_screen.js.map +1 -1
- package/build/screens/index.d.ts +1 -0
- package/build/screens/index.d.ts.map +1 -1
- package/build/screens/index.js +1 -0
- package/build/screens/index.js.map +1 -1
- package/build/screens/settings_screen.d.ts +5 -0
- package/build/screens/settings_screen.d.ts.map +1 -0
- package/build/screens/settings_screen.js +56 -0
- package/build/screens/settings_screen.js.map +1 -0
- package/build/types/jolt_events/conversation_events.d.ts +2 -0
- package/build/types/jolt_events/conversation_events.d.ts.map +1 -1
- package/build/types/jolt_events/conversation_events.js.map +1 -1
- package/build/types/resources/conversation.d.ts +2 -0
- package/build/types/resources/conversation.d.ts.map +1 -1
- package/build/types/resources/conversation.js.map +1 -1
- package/build/types/resources/direct_message_affiliations.d.ts +8 -0
- package/build/types/resources/direct_message_affiliations.d.ts.map +1 -0
- package/build/types/resources/direct_message_affiliations.js +2 -0
- package/build/types/resources/direct_message_affiliations.js.map +1 -0
- package/build/types/resources/direct_message_candidate.d.ts +12 -0
- package/build/types/resources/direct_message_candidate.d.ts.map +1 -0
- package/build/types/resources/direct_message_candidate.js +2 -0
- package/build/types/resources/direct_message_candidate.js.map +1 -0
- package/build/types/resources/index.d.ts +2 -0
- package/build/types/resources/index.d.ts.map +1 -1
- package/build/types/resources/index.js +2 -0
- package/build/types/resources/index.js.map +1 -1
- package/build/types/resources/organization.d.ts +1 -0
- package/build/types/resources/organization.d.ts.map +1 -1
- package/build/types/resources/organization.js.map +1 -1
- package/build/types/resources/person.d.ts +1 -0
- package/build/types/resources/person.d.ts.map +1 -1
- package/build/types/resources/person.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 +15 -6
- package/build/utils/request/conversation.js.map +1 -1
- package/build/utils/time.d.ts +8 -0
- package/build/utils/time.d.ts.map +1 -0
- package/build/utils/time.js +15 -0
- package/build/utils/time.js.map +1 -0
- package/package.json +2 -2
- package/src/components/conversation/messages_disabled_banners.tsx +16 -2
- package/src/contexts/api_provider.tsx +4 -1
- package/src/contexts/conversations_context.tsx +6 -2
- package/src/hooks/direct_messages/use_direct_message_affiliations.ts +39 -0
- package/src/hooks/direct_messages/use_direct_message_candidates.ts +31 -0
- package/src/hooks/direct_messages/use_direct_message_conversation_validate.ts +35 -0
- package/src/hooks/direct_messages/use_find_or_create_direct_message_conversation.ts +66 -0
- package/src/hooks/groups/use_group_chat_conversation_payload.ts +2 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/services/use_services_chat_conversation_payload.ts +2 -1
- package/src/hooks/use_api.ts +10 -1
- package/src/hooks/use_chat_configuration.ts +2 -1
- package/src/hooks/use_chat_permissions.ts +4 -1
- package/src/hooks/use_conversation.ts +1 -0
- package/src/hooks/use_conversation_jolt_events.ts +3 -2
- package/src/hooks/use_current_person.ts +4 -2
- package/src/hooks/use_features.ts +2 -1
- package/src/hooks/use_groups.ts +16 -12
- package/src/hooks/use_groups_groups.ts +19 -15
- package/src/hooks/use_has_direct_messages.ts +21 -0
- package/src/hooks/use_jolt.ts +2 -1
- package/src/hooks/use_organization.ts +4 -1
- package/src/hooks/use_person_settings.ts +29 -0
- package/src/navigation/index.tsx +11 -0
- package/src/screens/conversation_details_screen.tsx +2 -1
- package/src/screens/conversation_filters/components/conversation_filters.tsx +25 -5
- package/src/screens/conversation_filters/components/rows.tsx +16 -3
- package/src/screens/conversation_filters/context/conversation_filter_context.tsx +4 -0
- package/src/screens/conversation_filters/filter_types.ts +1 -0
- package/src/screens/conversation_filters/screen_props.ts +1 -0
- package/src/screens/conversation_screen.tsx +2 -1
- package/src/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.tsx +513 -10
- package/src/screens/conversations/components/list_header_component.tsx +84 -34
- package/src/screens/conversations/conversations_screen.tsx +1 -0
- package/src/screens/get_help_screen.tsx +2 -29
- package/src/screens/index.ts +1 -0
- package/src/screens/settings_screen.tsx +67 -0
- package/src/types/jolt_events/conversation_events.ts +2 -0
- package/src/types/resources/conversation.ts +3 -0
- package/src/types/resources/direct_message_affiliations.ts +8 -0
- package/src/types/resources/direct_message_candidate.ts +12 -0
- package/src/types/resources/index.ts +2 -0
- package/src/types/resources/organization.ts +1 -0
- package/src/types/resources/person.ts +1 -0
- package/src/utils/__tests__/conversation.test.ts +14 -0
- package/src/utils/request/conversation.ts +13 -5
- package/src/utils/time.ts +17 -0
|
@@ -95,7 +95,10 @@ function useSessionChanged(value: Pick<ChatContextValue, 'token' | 'env'>): bool
|
|
|
95
95
|
const isInitialLogin = !prevToken && !!newToken
|
|
96
96
|
if (isInitialLogin) return false
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
const tokenChanged = Boolean(
|
|
99
|
+
prevToken?.access_token && newToken?.access_token !== prevToken?.access_token
|
|
100
|
+
)
|
|
101
|
+
return tokenChanged || Boolean(prevEnv && newEnv !== prevEnv)
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
function usePrevious<T>(value: T): T {
|
|
@@ -39,9 +39,13 @@ export const ConversationsContextProvider = ({
|
|
|
39
39
|
args = {},
|
|
40
40
|
}: PropsWithChildren<{ args: ConversationFiltersParams }>) => {
|
|
41
41
|
const [activeConversationId, setActiveConversationId] = useState<number | undefined>()
|
|
42
|
-
const { chat_group_graph_id, group_source_app_name } = args
|
|
42
|
+
const { chat_group_graph_id, group_source_app_name, show_direct_messages } = args
|
|
43
43
|
|
|
44
|
-
const query = useConversations({
|
|
44
|
+
const query = useConversations({
|
|
45
|
+
chat_group_graph_id,
|
|
46
|
+
group_source_app_name,
|
|
47
|
+
show_direct_messages,
|
|
48
|
+
})
|
|
45
49
|
|
|
46
50
|
const value = useMemo(
|
|
47
51
|
() => ({
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { useMemo } from 'react'
|
|
3
|
+
import { ApiResource } from '../../types'
|
|
4
|
+
import { DirectMessageAffiliationsResource } from '../../types/resources/direct_message_affiliations'
|
|
5
|
+
import { useApiClient } from '../use_api_client'
|
|
6
|
+
|
|
7
|
+
export function useDirectMessageAffiliations() {
|
|
8
|
+
const apiClient = useApiClient()
|
|
9
|
+
|
|
10
|
+
const { data } = useQuery({
|
|
11
|
+
queryKey: ['chat', '/me/direct_message_affiliations'],
|
|
12
|
+
queryFn: () =>
|
|
13
|
+
apiClient.chat.get({
|
|
14
|
+
url: '/me/direct_message_affiliations',
|
|
15
|
+
data: {
|
|
16
|
+
fields: { DirectMessageAffiliations: ['groups', 'teams'] },
|
|
17
|
+
},
|
|
18
|
+
}) as Promise<ApiResource<DirectMessageAffiliationsResource>>,
|
|
19
|
+
staleTime: 5 * 60 * 1000,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const groups = useMemo(
|
|
23
|
+
() =>
|
|
24
|
+
new Map<number, string>(
|
|
25
|
+
Object.entries(data?.data?.groups ?? {}).map(([id, name]) => [+id, name])
|
|
26
|
+
),
|
|
27
|
+
[data]
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const teams = useMemo(
|
|
31
|
+
() =>
|
|
32
|
+
new Map<number, string>(
|
|
33
|
+
Object.entries(data?.data?.teams ?? {}).map(([id, name]) => [+id, name])
|
|
34
|
+
),
|
|
35
|
+
[data]
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return { groups, teams }
|
|
39
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { ApiCollection } from '../../types'
|
|
3
|
+
import { DirectMessageCandidateResource } from '../../types/resources/direct_message_candidate'
|
|
4
|
+
import { useApiClient } from '../use_api_client'
|
|
5
|
+
|
|
6
|
+
export function useDirectMessageCandidates(term: string) {
|
|
7
|
+
const apiClient = useApiClient()
|
|
8
|
+
return useQuery({
|
|
9
|
+
queryKey: ['chat', '/me/direct_message_candidates', term],
|
|
10
|
+
queryFn: () =>
|
|
11
|
+
apiClient.chat.get({
|
|
12
|
+
url: '/me/direct_message_candidates',
|
|
13
|
+
data: {
|
|
14
|
+
q: term,
|
|
15
|
+
fields: {
|
|
16
|
+
DirectMessageCandidate: [
|
|
17
|
+
'name',
|
|
18
|
+
'avatar',
|
|
19
|
+
'groups',
|
|
20
|
+
'teams',
|
|
21
|
+
'under_18',
|
|
22
|
+
'direct_messages_disabled',
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
}) as Promise<ApiCollection<DirectMessageCandidateResource>>,
|
|
27
|
+
enabled: term.length >= 1,
|
|
28
|
+
staleTime: 1000 * 60,
|
|
29
|
+
throwOnError: true,
|
|
30
|
+
})
|
|
31
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { ApiResource, ConversationValidateResource, ConversationWarning } from '../../types'
|
|
3
|
+
import { useApiClient } from '../use_api_client'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
recipientIds: string[]
|
|
7
|
+
enabled?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useDirectMessageConversationValidate({ recipientIds, enabled = true }: Props) {
|
|
11
|
+
const apiClient = useApiClient()
|
|
12
|
+
|
|
13
|
+
const { data, isLoading } = useQuery({
|
|
14
|
+
queryKey: ['chat', '/me/conversation_validate', { recipient_ids: recipientIds }],
|
|
15
|
+
queryFn: () =>
|
|
16
|
+
apiClient.chat.post<ApiResource<ConversationValidateResource>>({
|
|
17
|
+
url: '/me/conversation_validate',
|
|
18
|
+
data: {
|
|
19
|
+
data: {
|
|
20
|
+
type: 'ConversationValidate',
|
|
21
|
+
attributes: { recipient_ids: recipientIds },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
enabled: enabled && recipientIds.length > 0,
|
|
26
|
+
retry: false,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const warnings: ConversationWarning[] = data?.data.warnings ?? []
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
warnings,
|
|
33
|
+
validationPending: enabled && recipientIds.length > 0 && isLoading,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { useState, useCallback } from 'react'
|
|
3
|
+
import { ApiCollection, ApiResource, ConversationResource } from '../../types'
|
|
4
|
+
import { ApiClient, useApiClient } from '../use_api_client'
|
|
5
|
+
|
|
6
|
+
const findDirectMessageConversation = (apiClient: ApiClient, participantIds: string[]) =>
|
|
7
|
+
apiClient.chat.get({
|
|
8
|
+
url: '/me/conversations',
|
|
9
|
+
data: {
|
|
10
|
+
fields: { Conversation: ['title'] },
|
|
11
|
+
filter: 'with_exact_direct_message_participants',
|
|
12
|
+
participant_ids: participantIds.join(','),
|
|
13
|
+
},
|
|
14
|
+
}) as Promise<ApiCollection<ConversationResource>>
|
|
15
|
+
|
|
16
|
+
const createDirectMessageConversation = (apiClient: ApiClient, ids: string[]) =>
|
|
17
|
+
apiClient.chat.post({
|
|
18
|
+
url: '/me/direct_messages',
|
|
19
|
+
data: {
|
|
20
|
+
data: {
|
|
21
|
+
type: 'DirectMessage',
|
|
22
|
+
attributes: { recipients: ids },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
}) as Promise<ApiResource<ConversationResource>>
|
|
26
|
+
|
|
27
|
+
interface Props {
|
|
28
|
+
recipientIds: string[]
|
|
29
|
+
onSuccess: (conversationId: ConversationResource['id']) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useFindOrCreateDirectMessageConversation({ recipientIds, onSuccess }: Props) {
|
|
33
|
+
const apiClient = useApiClient()
|
|
34
|
+
const [isFinding, setIsFinding] = useState(false)
|
|
35
|
+
const [isError, setIsError] = useState(false)
|
|
36
|
+
|
|
37
|
+
const { data } = useQuery({
|
|
38
|
+
queryKey: ['chat', '/me/conversations', 'with_exact_direct_message_participants', recipientIds],
|
|
39
|
+
queryFn: () => findDirectMessageConversation(apiClient, recipientIds),
|
|
40
|
+
enabled: recipientIds.length > 0,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const directMessageExists = (data?.data.length ?? 0) > 0
|
|
44
|
+
|
|
45
|
+
const findOrCreateConversation = useCallback(
|
|
46
|
+
async (participantIds: string[]) => {
|
|
47
|
+
setIsFinding(true)
|
|
48
|
+
setIsError(false)
|
|
49
|
+
try {
|
|
50
|
+
const findResponse = await findDirectMessageConversation(apiClient, participantIds)
|
|
51
|
+
if (findResponse.data.length > 0) {
|
|
52
|
+
onSuccess(findResponse.data[0].id)
|
|
53
|
+
} else {
|
|
54
|
+
const createResponse = await createDirectMessageConversation(apiClient, participantIds)
|
|
55
|
+
onSuccess(createResponse.data.id)
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
setIsError(true)
|
|
59
|
+
setIsFinding(false)
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
[apiClient, onSuccess]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return { findOrCreateConversation, directMessageExists, isFinding, isError }
|
|
66
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useQuery } from '@tanstack/react-query'
|
|
2
2
|
import { ApiResource, ResourceObject } from '../../types'
|
|
3
|
+
import { toMs } from '../../utils/time'
|
|
3
4
|
import { useApiClient } from '../use_api_client'
|
|
4
5
|
|
|
5
|
-
const STALE_TIME_MS = 25
|
|
6
|
+
const STALE_TIME_MS = toMs(25, 'minutes') // under Groups' 30-min server-side expiry
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
groupId: number
|
package/src/hooks/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './use_current_person'
|
|
|
10
10
|
export * from './use_deleting_ids'
|
|
11
11
|
export * from './use_direct_messages_enabled'
|
|
12
12
|
export * from './use_font_scale'
|
|
13
|
+
export * from './use_has_direct_messages'
|
|
13
14
|
export * from './use_groups_groups'
|
|
14
15
|
export * from './use_groups'
|
|
15
16
|
export * from './use_interaction_ghost_color'
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { toMs } from '../../utils/time'
|
|
2
3
|
import { useApiClient } from '../use_api_client'
|
|
3
4
|
import { getServicesChatConversationPayload } from './use_find_or_create_services_conversation'
|
|
4
5
|
|
|
5
|
-
const STALE_TIME_MS = 55
|
|
6
|
+
const STALE_TIME_MS = toMs(55, 'minutes') // under Services' 1-hour server-side expiry
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
teamIds: number[]
|
package/src/hooks/use_api.ts
CHANGED
|
@@ -17,7 +17,15 @@ interface ApiGetOptions extends GetRequest {
|
|
|
17
17
|
enabled?: boolean
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
type UseApiGetOptions = {
|
|
21
|
+
staleTime?: number
|
|
22
|
+
gcTime?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const useApiGet = <T extends ResourceObject | ResourceObject[]>(
|
|
26
|
+
args: ApiGetOptions,
|
|
27
|
+
opts?: UseApiGetOptions
|
|
28
|
+
) => {
|
|
21
29
|
type Resource = ApiResource<T>
|
|
22
30
|
const { session } = useContext(ChatContext)
|
|
23
31
|
const apiClient = useApiClient()
|
|
@@ -32,6 +40,7 @@ export const useApiGet = <T extends ResourceObject | ResourceObject[]>(args: Api
|
|
|
32
40
|
return apiClient[app].get({ url, data: d, headers }) as Promise<Resource>
|
|
33
41
|
},
|
|
34
42
|
enabled,
|
|
43
|
+
...opts,
|
|
35
44
|
})
|
|
36
45
|
|
|
37
46
|
return { ...data, ...query }
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getChatConfigurationRequestArgs,
|
|
6
6
|
getChatConfigurationQueryKey,
|
|
7
7
|
} from '../utils/request/get_chat_configuration'
|
|
8
|
+
import { ONE_HOUR } from '../utils/time'
|
|
8
9
|
import {
|
|
9
10
|
FALLBACK_ALLOWED_FILE_EXTENSIONS,
|
|
10
11
|
FALLBACK_ALLOWED_MIME_TYPES,
|
|
@@ -28,7 +29,7 @@ export function useChatConfiguration() {
|
|
|
28
29
|
.get<ApiResource<ChatConfigurationResource>>(requestArgs)
|
|
29
30
|
.catch(() => stableFallbackConfiguration)
|
|
30
31
|
},
|
|
31
|
-
staleTime:
|
|
32
|
+
staleTime: ONE_HOUR,
|
|
32
33
|
})
|
|
33
34
|
|
|
34
35
|
const attrs = data.data
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AppGrantsResource } from '../types'
|
|
2
|
+
import { FIVE_MINUTES } from '../utils/time'
|
|
2
3
|
import { useApiGet } from './use_api'
|
|
3
4
|
import { App } from './use_api_client'
|
|
4
5
|
|
|
@@ -13,7 +14,9 @@ export const appGrantsRequestArgs = {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export function useAppGrants() {
|
|
16
|
-
return useApiGet<AppGrantsResource[]>(appGrantsRequestArgs
|
|
17
|
+
return useApiGet<AppGrantsResource[]>(appGrantsRequestArgs, {
|
|
18
|
+
staleTime: FIVE_MINUTES,
|
|
19
|
+
})
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export function useCanCreateConversations(): boolean {
|
|
@@ -26,7 +26,8 @@ export function useConversationJoltEvents({ conversationId }: Props) {
|
|
|
26
26
|
|
|
27
27
|
const handleUpdatedConversation = useCallback(
|
|
28
28
|
(e: JoltConversationEvent) => {
|
|
29
|
-
const { last_message_idempotent_key, last_message_author_id, disabled } =
|
|
29
|
+
const { last_message_idempotent_key, last_message_author_id, disabled, disabled_reason } =
|
|
30
|
+
e.data.data
|
|
30
31
|
|
|
31
32
|
if (last_message_idempotent_key && last_message_author_id === currentPersonId) {
|
|
32
33
|
completeMessageCreationConversationTracking({
|
|
@@ -37,7 +38,7 @@ export function useConversationJoltEvents({ conversationId }: Props) {
|
|
|
37
38
|
|
|
38
39
|
queryClient.setQueryData<ApiResource<ConversationResource>>(queryKey, prev => {
|
|
39
40
|
if (!prev?.data) return prev
|
|
40
|
-
return { ...prev, data: { ...prev.data, disabled } }
|
|
41
|
+
return { ...prev, data: { ...prev.data, disabled, disabledReason: disabled_reason } }
|
|
41
42
|
})
|
|
42
43
|
|
|
43
44
|
queryClient.invalidateQueries({ queryKey })
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useQueryClient } from '@tanstack/react-query'
|
|
2
2
|
import { CurrentPersonResource } from '../types'
|
|
3
3
|
import { ApiResource } from '../types/api_primitives'
|
|
4
|
+
import { FIVE_MINUTES, THIRTY_MINUTES } from '../utils/time'
|
|
4
5
|
import { getRequestQueryKey, useSuspenseGet } from './use_suspense_api'
|
|
5
6
|
|
|
6
7
|
export const currentPersonRequestArgs = {
|
|
@@ -15,6 +16,7 @@ export const currentPersonRequestArgs = {
|
|
|
15
16
|
'unread_count',
|
|
16
17
|
'pco_chat_enabled',
|
|
17
18
|
'age_qualification_status',
|
|
19
|
+
'direct_messages_disabled',
|
|
18
20
|
],
|
|
19
21
|
},
|
|
20
22
|
},
|
|
@@ -24,8 +26,8 @@ export const currentPersonQueryKey = getRequestQueryKey(currentPersonRequestArgs
|
|
|
24
26
|
|
|
25
27
|
export const useCurrentPerson = () => {
|
|
26
28
|
const { data: person } = useSuspenseGet<CurrentPersonResource>(currentPersonRequestArgs, {
|
|
27
|
-
staleTime:
|
|
28
|
-
gcTime:
|
|
29
|
+
staleTime: FIVE_MINUTES,
|
|
30
|
+
gcTime: THIRTY_MINUTES,
|
|
29
31
|
})
|
|
30
32
|
|
|
31
33
|
return person
|
|
@@ -3,6 +3,7 @@ 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'
|
|
6
7
|
import { useApiClient } from './use_api_client'
|
|
7
8
|
|
|
8
9
|
type FeatureName = (typeof availableFeatures)[keyof typeof availableFeatures]
|
|
@@ -18,7 +19,7 @@ export function useFeatures() {
|
|
|
18
19
|
.get<ApiCollection<FeatureResource>>(requestArgs)
|
|
19
20
|
.catch(() => stableEmptyFeatures)
|
|
20
21
|
},
|
|
21
|
-
staleTime:
|
|
22
|
+
staleTime: FIVE_MINUTES,
|
|
22
23
|
})
|
|
23
24
|
|
|
24
25
|
const features = data.data
|
package/src/hooks/use_groups.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AppName } from '../types/resources/app_name'
|
|
2
2
|
import { GroupResource, SourceType } from '../types/resources/group_resource'
|
|
3
|
+
import { FIVE_MINUTES } from '../utils/time'
|
|
3
4
|
import { useApiPaginator } from './use_api'
|
|
4
5
|
|
|
5
6
|
type UseGroupsProps = {
|
|
@@ -8,20 +9,23 @@ type UseGroupsProps = {
|
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export const useGroups = ({ source_app_name, source_type }: UseGroupsProps) => {
|
|
11
|
-
return useApiPaginator<GroupResource>(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
return useApiPaginator<GroupResource>(
|
|
13
|
+
{
|
|
14
|
+
url: '/me/groups',
|
|
15
|
+
data: {
|
|
16
|
+
fields: {
|
|
17
|
+
Group: [],
|
|
18
|
+
},
|
|
19
|
+
where: {
|
|
20
|
+
source_app_name,
|
|
21
|
+
source_type,
|
|
22
|
+
},
|
|
23
|
+
perPage: 100,
|
|
16
24
|
},
|
|
17
|
-
|
|
18
|
-
source_app_name,
|
|
19
|
-
source_type,
|
|
20
|
-
},
|
|
21
|
-
perPage: 100,
|
|
25
|
+
app: 'chat',
|
|
22
26
|
},
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
{ staleTime: FIVE_MINUTES }
|
|
28
|
+
)
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
export const useCanDisplayGroups = (props: UseGroupsProps) => {
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import { GroupsGroupResource } from '../types'
|
|
2
|
+
import { FIVE_MINUTES } from '../utils/time'
|
|
2
3
|
import { useApiPaginator } from './use_api'
|
|
3
4
|
|
|
4
5
|
export const useGroupsGroups = () => {
|
|
5
|
-
return useApiPaginator<GroupsGroupResource>(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
return useApiPaginator<GroupsGroupResource>(
|
|
7
|
+
{
|
|
8
|
+
url: '/me/groups',
|
|
9
|
+
data: {
|
|
10
|
+
perPage: 100,
|
|
11
|
+
fields: {
|
|
12
|
+
Group: [
|
|
13
|
+
'can_create_conversation',
|
|
14
|
+
'can_create_conversation_reason_code',
|
|
15
|
+
'name',
|
|
16
|
+
'header_image',
|
|
17
|
+
'memberships_count',
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
order: 'name',
|
|
17
21
|
},
|
|
18
|
-
|
|
22
|
+
app: 'groups',
|
|
19
23
|
},
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
{ staleTime: FIVE_MINUTES }
|
|
25
|
+
)
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export const useCanDisplayGroupsGroups = () => {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { ApiCollection, ConversationResource } from '../types'
|
|
3
|
+
import { useApiClient } from './use_api_client'
|
|
4
|
+
|
|
5
|
+
export function useHasDirectMessages(): boolean {
|
|
6
|
+
const apiClient = useApiClient()
|
|
7
|
+
|
|
8
|
+
const { data, isFetched } = useQuery<ApiCollection<ConversationResource>>({
|
|
9
|
+
queryKey: ['chat', '/me/conversations', 'direct_message', 'has_any'],
|
|
10
|
+
queryFn: () =>
|
|
11
|
+
apiClient.chat.get({
|
|
12
|
+
url: '/me/conversations',
|
|
13
|
+
data: { filter: 'direct_message', perPage: 1, fields: { Conversation: ['id'] } },
|
|
14
|
+
}),
|
|
15
|
+
staleTime: 1000 * 60 * 5,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
if (!isFetched) return false
|
|
19
|
+
|
|
20
|
+
return (data?.data.length ?? 0) > 0
|
|
21
|
+
}
|
package/src/hooks/use_jolt.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { useChatContext } from '../contexts/chat_context'
|
|
|
13
13
|
import { ApiResource } from '../types'
|
|
14
14
|
import type { JoltSubscriptionPattern } from '../types/jolt_events'
|
|
15
15
|
import { Client, Uri } from '../utils'
|
|
16
|
+
import { FIVE_SECONDS } from '../utils/time'
|
|
16
17
|
|
|
17
18
|
interface JoltResponse {
|
|
18
19
|
type: 'JoltToken'
|
|
@@ -49,7 +50,7 @@ export const useJoltClient = (): JoltClient | undefined => {
|
|
|
49
50
|
},
|
|
50
51
|
})
|
|
51
52
|
},
|
|
52
|
-
staleTime:
|
|
53
|
+
staleTime: FIVE_SECONDS,
|
|
53
54
|
})
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { OrganizationResource } from '../types'
|
|
2
|
+
import { ONE_HOUR } from '../utils/time'
|
|
2
3
|
import { App } from './use_api_client'
|
|
3
4
|
import { useSuspenseGet } from './use_suspense_api'
|
|
4
5
|
|
|
@@ -13,5 +14,7 @@ export const organizationRequestArgs = {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export function useOrganization() {
|
|
16
|
-
return useSuspenseGet<OrganizationResource>(organizationRequestArgs
|
|
17
|
+
return useSuspenseGet<OrganizationResource>(organizationRequestArgs, {
|
|
18
|
+
staleTime: ONE_HOUR,
|
|
19
|
+
})
|
|
17
20
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useMutation } from '@tanstack/react-query'
|
|
2
|
+
import { throwResponseError } from '../utils/response_error'
|
|
3
|
+
import { useApiClient } from './use_api_client'
|
|
4
|
+
import { useCurrentPerson, useCurrentPersonCache } from './use_current_person'
|
|
5
|
+
|
|
6
|
+
export function usePersonSettings() {
|
|
7
|
+
const apiClient = useApiClient()
|
|
8
|
+
const person = useCurrentPerson()
|
|
9
|
+
const { update, invalidate } = useCurrentPersonCache()
|
|
10
|
+
|
|
11
|
+
const directMessagesDisabled = person?.directMessagesDisabled ?? false
|
|
12
|
+
|
|
13
|
+
const { mutate: setDirectMessagesDisabled, isPending: isMutating } = useMutation({
|
|
14
|
+
mutationFn: (disabled: boolean) => {
|
|
15
|
+
const action = disabled ? 'disable_direct_messages' : 'enable_direct_messages'
|
|
16
|
+
return apiClient.chat
|
|
17
|
+
.post({ url: `/me/${action}`, data: { data: { type: 'Person', attributes: {} } } })
|
|
18
|
+
.catch(throwResponseError)
|
|
19
|
+
},
|
|
20
|
+
onMutate: disabled => {
|
|
21
|
+
update({ directMessagesDisabled: disabled })
|
|
22
|
+
},
|
|
23
|
+
onError: () => {
|
|
24
|
+
invalidate()
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
return { directMessagesDisabled, setDirectMessagesDisabled, isMutating }
|
|
29
|
+
}
|
package/src/navigation/index.tsx
CHANGED
|
@@ -77,6 +77,7 @@ import { NotificationSettingsScreen } from '../screens/notification_settings_scr
|
|
|
77
77
|
import { PreferredAppSelectionScreen } from '../screens/preferred_app_selection_screen'
|
|
78
78
|
import { ReactionsScreen, ReactionsScreenOptions } from '../screens/reactions_screen'
|
|
79
79
|
import { SendGiphyScreen, SendGiphyScreenOptions } from '../screens/send_giphy_screen'
|
|
80
|
+
import { SettingsScreen } from '../screens/settings_screen'
|
|
80
81
|
import {
|
|
81
82
|
SystemMessagePeopleScreen,
|
|
82
83
|
SystemMessagePeopleScreenOptions,
|
|
@@ -280,6 +281,16 @@ export const ChatStack = createNativeStackNavigator({
|
|
|
280
281
|
screen: AvatarPickerScreen,
|
|
281
282
|
options: AvatarPickerScreenOptions,
|
|
282
283
|
},
|
|
284
|
+
ChatSettings: {
|
|
285
|
+
screen: SettingsScreen,
|
|
286
|
+
options: ({ navigation }) => ({
|
|
287
|
+
title: 'Chat settings',
|
|
288
|
+
headerBackVisible: false,
|
|
289
|
+
headerRight: (props: NativeStackHeaderRightProps) => (
|
|
290
|
+
<HeaderTextButton {...props} onPress={navigation.goBack} title="Done" />
|
|
291
|
+
),
|
|
292
|
+
}),
|
|
293
|
+
},
|
|
283
294
|
NotificationSettings: {
|
|
284
295
|
screen: NotificationSettingsScreen,
|
|
285
296
|
options: ({ navigation }) => ({
|
|
@@ -122,6 +122,7 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
|
|
|
122
122
|
const canDelete = conversation.memberAbility?.canDelete || false
|
|
123
123
|
const isLeader = conversation.memberAbility?.leader || false
|
|
124
124
|
const isDisabled = conversation.disabled ?? false
|
|
125
|
+
const showAdultRequirementNotice = isDisabled && conversation.disabledReason === 'safety_check'
|
|
125
126
|
|
|
126
127
|
const {
|
|
127
128
|
data: members,
|
|
@@ -350,7 +351,7 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
|
|
|
350
351
|
sectionInnerStyle: styles.sectionInnerGenderNotice,
|
|
351
352
|
},
|
|
352
353
|
{
|
|
353
|
-
type:
|
|
354
|
+
type: showAdultRequirementNotice ? SectionTypes.view : SectionTypes.hidden,
|
|
354
355
|
data: {
|
|
355
356
|
children: <AdultRequirementNotice />,
|
|
356
357
|
},
|
|
@@ -2,7 +2,12 @@ import React, { useContext, useMemo } from 'react'
|
|
|
2
2
|
import { StyleSheet, View, ViewStyle } from 'react-native'
|
|
3
3
|
import { FlatList } from 'react-native-gesture-handler'
|
|
4
4
|
import { Heading } from '../../../components'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
useAtFontScaleBreakpoint,
|
|
7
|
+
useDirectMessagesEnabled,
|
|
8
|
+
useHasDirectMessages,
|
|
9
|
+
useTheme,
|
|
10
|
+
} from '../../../hooks'
|
|
6
11
|
import { FilterContext, useFilterContext } from '../context/conversation_filter_context'
|
|
7
12
|
import { FilterTypes } from '../filter_types'
|
|
8
13
|
import { useGroupsToFilter, useTeamsToFilter } from '../hooks/filters'
|
|
@@ -52,7 +57,11 @@ interface DataItem<T, TName extends SectionTypes> {
|
|
|
52
57
|
export const ConversationFilters = () => {
|
|
53
58
|
const styles = useStyles()
|
|
54
59
|
const { setScrollOffset, params } = useContext(FilterContext)
|
|
55
|
-
const { chat_group_graph_id, group_source_app_name = '' } = params
|
|
60
|
+
const { chat_group_graph_id, group_source_app_name = '', show_direct_messages } = params
|
|
61
|
+
|
|
62
|
+
const directMessagesEnabled = useDirectMessagesEnabled()
|
|
63
|
+
const hasDirectMessages = useHasDirectMessages()
|
|
64
|
+
const canFilterByDMs = directMessagesEnabled && hasDirectMessages
|
|
56
65
|
|
|
57
66
|
const activeFilter: FilterTypes = useMemo(() => {
|
|
58
67
|
if (chat_group_graph_id) {
|
|
@@ -61,10 +70,12 @@ export const ConversationFilters = () => {
|
|
|
61
70
|
return FilterTypes.Groups
|
|
62
71
|
} else if (/services/i.test(group_source_app_name)) {
|
|
63
72
|
return FilterTypes.Teams
|
|
73
|
+
} else if (show_direct_messages) {
|
|
74
|
+
return FilterTypes.DirectMessages
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
return FilterTypes.All
|
|
67
|
-
}, [chat_group_graph_id, group_source_app_name])
|
|
78
|
+
}, [chat_group_graph_id, group_source_app_name, show_direct_messages])
|
|
68
79
|
|
|
69
80
|
const { groups = [] } = useGroupsToFilter()
|
|
70
81
|
const { teams = [] } = useTeamsToFilter()
|
|
@@ -91,14 +102,15 @@ export const ConversationFilters = () => {
|
|
|
91
102
|
const teamItemData = selectActiveWithFirst(5)(teamItems)
|
|
92
103
|
|
|
93
104
|
const hideAppFilters = groupItems.length < 1 || teamItems.length < 1
|
|
105
|
+
const showGeneralFilters = !hideAppFilters || canFilterByDMs
|
|
94
106
|
|
|
95
107
|
const listData: SectionListData = [
|
|
96
108
|
{
|
|
97
|
-
type:
|
|
109
|
+
type: showGeneralFilters ? SectionTypes.header : SectionTypes.hidden,
|
|
98
110
|
data: { title: 'General Filters' },
|
|
99
111
|
},
|
|
100
112
|
{
|
|
101
|
-
type:
|
|
113
|
+
type: showGeneralFilters ? SectionTypes.filter : SectionTypes.hidden,
|
|
102
114
|
data: { filter: FilterTypes.All, isActive: activeFilter === FilterTypes.All },
|
|
103
115
|
},
|
|
104
116
|
{
|
|
@@ -117,6 +129,14 @@ export const ConversationFilters = () => {
|
|
|
117
129
|
isActive: activeFilter === FilterTypes.Teams,
|
|
118
130
|
},
|
|
119
131
|
},
|
|
132
|
+
{
|
|
133
|
+
type: canFilterByDMs ? SectionTypes.filter : SectionTypes.hidden,
|
|
134
|
+
data: {
|
|
135
|
+
filter: FilterTypes.DirectMessages,
|
|
136
|
+
show_direct_messages: true,
|
|
137
|
+
isActive: activeFilter === FilterTypes.DirectMessages,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
120
140
|
{
|
|
121
141
|
type: groupItems.length ? SectionTypes.header : SectionTypes.hidden,
|
|
122
142
|
data: { title: 'Groups', style: styles.extraHeaderScalableSpace },
|