@planningcenter/chat-react-native 2.2.2-rc.1 → 2.3.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.
- package/build/components/conversations.d.ts.map +1 -1
- package/build/components/conversations.js +4 -0
- package/build/components/conversations.js.map +1 -1
- package/build/components/display/action_button.d.ts +8 -0
- package/build/components/display/action_button.d.ts.map +1 -0
- package/build/components/display/action_button.js +44 -0
- package/build/components/display/action_button.js.map +1 -0
- package/build/contexts/api_provider.js +2 -2
- package/build/contexts/api_provider.js.map +1 -1
- package/build/hooks/index.d.ts +2 -0
- package/build/hooks/index.d.ts.map +1 -1
- package/build/hooks/index.js +2 -0
- package/build/hooks/index.js.map +1 -1
- package/build/hooks/use_api.d.ts +383 -0
- package/build/hooks/use_api.d.ts.map +1 -0
- package/build/hooks/use_api.js +40 -0
- package/build/hooks/use_api.js.map +1 -0
- package/build/hooks/use_api_client.d.ts +5 -3
- package/build/hooks/use_api_client.d.ts.map +1 -1
- package/build/hooks/use_api_client.js +1 -1
- package/build/hooks/use_api_client.js.map +1 -1
- package/build/hooks/use_chat_permissions.d.ts +172 -0
- package/build/hooks/use_chat_permissions.d.ts.map +1 -0
- package/build/hooks/use_chat_permissions.js +17 -0
- package/build/hooks/use_chat_permissions.js.map +1 -0
- package/build/hooks/use_conversation.d.ts.map +1 -1
- package/build/hooks/use_conversation.js +6 -5
- package/build/hooks/use_conversation.js.map +1 -1
- package/build/hooks/use_suspense_api.d.ts +13 -4
- package/build/hooks/use_suspense_api.d.ts.map +1 -1
- package/build/hooks/use_suspense_api.js +1 -0
- package/build/hooks/use_suspense_api.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/navigation/index.d.ts +123 -2
- package/build/navigation/index.d.ts.map +1 -1
- package/build/navigation/index.js +40 -4
- package/build/navigation/index.js.map +1 -1
- package/build/navigation/screenLayout.js +1 -1
- package/build/navigation/screenLayout.js.map +1 -1
- package/build/screens/create/conversation_create_screen.d.ts +9 -0
- package/build/screens/create/conversation_create_screen.d.ts.map +1 -0
- package/build/screens/create/conversation_create_screen.js +123 -0
- package/build/screens/create/conversation_create_screen.js.map +1 -0
- package/build/screens/create/conversation_filter_recipients_screen.d.ts +8 -0
- package/build/screens/create/conversation_filter_recipients_screen.d.ts.map +1 -0
- package/build/screens/create/conversation_filter_recipients_screen.js +52 -0
- package/build/screens/create/conversation_filter_recipients_screen.js.map +1 -0
- package/build/screens/create/conversation_select_recipients_screen.d.ts +8 -0
- package/build/screens/create/conversation_select_recipients_screen.d.ts.map +1 -0
- package/build/screens/create/conversation_select_recipients_screen.js +105 -0
- package/build/screens/create/conversation_select_recipients_screen.js.map +1 -0
- package/build/types/resources/app_grant.d.ts +6 -0
- package/build/types/resources/app_grant.d.ts.map +1 -0
- package/build/types/resources/app_grant.js +2 -0
- package/build/types/resources/app_grant.js.map +1 -0
- package/build/types/resources/groups/groups_group_resource.d.ts +12 -0
- package/build/types/resources/groups/groups_group_resource.d.ts.map +1 -0
- package/build/types/resources/groups/groups_group_resource.js +2 -0
- package/build/types/resources/groups/groups_group_resource.js.map +1 -0
- package/build/types/resources/groups/index.d.ts +2 -0
- package/build/types/resources/groups/index.d.ts.map +1 -0
- package/build/types/resources/groups/index.js +2 -0
- package/build/types/resources/groups/index.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/package.json +2 -2
- package/src/components/conversations.tsx +8 -0
- package/src/components/display/action_button.tsx +62 -0
- package/src/contexts/api_provider.tsx +2 -2
- package/src/hooks/index.ts +2 -0
- package/src/hooks/use_api.ts +80 -0
- package/src/hooks/use_api_client.ts +13 -15
- package/src/hooks/use_chat_permissions.ts +20 -0
- package/src/hooks/use_conversation.ts +6 -5
- package/src/hooks/use_suspense_api.ts +16 -4
- package/src/index.tsx +1 -1
- package/src/navigation/index.tsx +46 -7
- package/src/navigation/screenLayout.tsx +1 -1
- package/src/screens/create/conversation_create_screen.tsx +148 -0
- package/src/screens/create/conversation_filter_recipients_screen.tsx +79 -0
- package/src/screens/create/conversation_select_recipients_screen.tsx +136 -0
- package/src/types/resources/app_grant.ts +6 -0
- package/src/types/resources/groups/groups_group_resource.ts +12 -0
- package/src/types/resources/groups/index.ts +1 -0
- package/src/types/resources/index.ts +2 -0
- package/build/contexts/index.d.ts +0 -3
- package/build/contexts/index.d.ts.map +0 -1
- package/build/contexts/index.js +0 -3
- package/build/contexts/index.js.map +0 -1
- package/src/contexts/index.ts +0 -2
|
@@ -7,11 +7,14 @@ import { useConversationsJoltEvents } from '../hooks/use_conversation_jolt_event
|
|
|
7
7
|
import { useConversations } from '../hooks/use_conversations'
|
|
8
8
|
import { formatDatePreview } from '../utils/date'
|
|
9
9
|
import { AvatarGroup, Badge, Heading, Text, TextButton } from './display'
|
|
10
|
+
import { ActionButton } from './display/action_button'
|
|
11
|
+
import { useCanCreateConversations } from '../hooks/use_chat_permissions'
|
|
10
12
|
|
|
11
13
|
export const Conversations = () => {
|
|
12
14
|
const styles = useStyles()
|
|
13
15
|
|
|
14
16
|
const { conversations, fetchNextPage, refetch, isRefetching } = useConversations()
|
|
17
|
+
const canCreateConversations = useCanCreateConversations()
|
|
15
18
|
|
|
16
19
|
// TODO: Filter using the API
|
|
17
20
|
const nonEmptyConversations = conversations.filter(c => c.lastMessageTextPreview) || []
|
|
@@ -74,6 +77,11 @@ export const Conversations = () => {
|
|
|
74
77
|
)}
|
|
75
78
|
onEndReached={() => fetchNextPage()}
|
|
76
79
|
/>
|
|
80
|
+
<ActionButton
|
|
81
|
+
visible={canCreateConversations}
|
|
82
|
+
title="New conversation"
|
|
83
|
+
onPress={() => navigation.navigate('Create')}
|
|
84
|
+
/>
|
|
77
85
|
</View>
|
|
78
86
|
)
|
|
79
87
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { Animated, LayoutAnimation, StyleSheet } from 'react-native'
|
|
3
|
+
import { Button } from './button'
|
|
4
|
+
import { useEffect } from 'react'
|
|
5
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
6
|
+
import { useTheme } from '../../hooks'
|
|
7
|
+
import { Text } from './text'
|
|
8
|
+
|
|
9
|
+
export const ActionButton = ({
|
|
10
|
+
visible = true,
|
|
11
|
+
onPress,
|
|
12
|
+
title,
|
|
13
|
+
infoText,
|
|
14
|
+
}: {
|
|
15
|
+
visible?: boolean
|
|
16
|
+
onPress: () => void
|
|
17
|
+
title: string
|
|
18
|
+
infoText?: string
|
|
19
|
+
}) => {
|
|
20
|
+
const styles = useStyles()
|
|
21
|
+
const [show, setShow] = useState(visible)
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (show === visible) return
|
|
25
|
+
|
|
26
|
+
setShow(visible)
|
|
27
|
+
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
|
28
|
+
}, [show, visible])
|
|
29
|
+
|
|
30
|
+
if (!visible) return null
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Animated.View style={styles.container}>
|
|
34
|
+
{Boolean(infoText) && (
|
|
35
|
+
<Text style={styles.infoText} variant="tertiary">
|
|
36
|
+
{infoText}
|
|
37
|
+
</Text>
|
|
38
|
+
)}
|
|
39
|
+
<Button variant="fill" size="lg" onPress={onPress} title={title} />
|
|
40
|
+
</Animated.View>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const useStyles = () => {
|
|
45
|
+
const { bottom } = useSafeAreaInsets()
|
|
46
|
+
const { colors } = useTheme()
|
|
47
|
+
|
|
48
|
+
return StyleSheet.create({
|
|
49
|
+
container: {
|
|
50
|
+
paddingVertical: 16,
|
|
51
|
+
paddingHorizontal: 24,
|
|
52
|
+
paddingBottom: bottom,
|
|
53
|
+
borderTopWidth: 1,
|
|
54
|
+
borderTopColor: colors.fillColorNeutral060,
|
|
55
|
+
gap: 16,
|
|
56
|
+
},
|
|
57
|
+
infoText: {
|
|
58
|
+
textAlign: 'center',
|
|
59
|
+
color: colors.textColorDefaultSecondary,
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
}
|
|
@@ -12,9 +12,9 @@ const defaultQueryFn = ({ queryKey }: { queryKey: QueryKey }) => {
|
|
|
12
12
|
throw new Error('No token present')
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const [url, data, headers] = queryKey as RequestQueryKey
|
|
15
|
+
const [url, data, headers, app = 'chat'] = queryKey as RequestQueryKey
|
|
16
16
|
|
|
17
|
-
return apiClient.
|
|
17
|
+
return apiClient[app].get({ url, data, headers })
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export const queryClient = new QueryClient({
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnyUseSuspenseInfiniteQueryOptions,
|
|
3
|
+
InfiniteData,
|
|
4
|
+
useInfiniteQuery,
|
|
5
|
+
useQuery,
|
|
6
|
+
} from '@tanstack/react-query'
|
|
7
|
+
import { ApiCollection, ApiResource, ResourceObject } from '../types'
|
|
8
|
+
import { GetRequest, RequestData } from '../utils/client/types'
|
|
9
|
+
import { useApiClient } from './use_api_client'
|
|
10
|
+
import { getRequestQueryKey } from './use_suspense_api'
|
|
11
|
+
|
|
12
|
+
interface SuspenseGetOptions extends GetRequest {
|
|
13
|
+
app?: 'chat' | 'groups'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const useApiGet = <T extends ResourceObject | ResourceObject[]>(
|
|
17
|
+
args: SuspenseGetOptions
|
|
18
|
+
) => {
|
|
19
|
+
type Resource = ApiResource<T>
|
|
20
|
+
|
|
21
|
+
const { data, ...query } = useQuery<Resource, Response>({
|
|
22
|
+
queryKey: getRequestQueryKey(args),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
return { ...data, ...query }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type NextMeta = Partial<{
|
|
29
|
+
offset: string
|
|
30
|
+
idLt: string
|
|
31
|
+
}>
|
|
32
|
+
|
|
33
|
+
export type SuspensePaginatorOptions = Omit<
|
|
34
|
+
AnyUseSuspenseInfiniteQueryOptions,
|
|
35
|
+
'getNextPageParam' | 'initialPageParam' | 'queryFn' | 'queryKey'
|
|
36
|
+
>
|
|
37
|
+
|
|
38
|
+
export const useApiPaginator = <T extends ResourceObject>(
|
|
39
|
+
args: SuspenseGetOptions,
|
|
40
|
+
opts?: SuspensePaginatorOptions
|
|
41
|
+
) => {
|
|
42
|
+
const apiClient = useApiClient()
|
|
43
|
+
const query = useInfiniteQuery<
|
|
44
|
+
ApiCollection<T>,
|
|
45
|
+
Response,
|
|
46
|
+
InfiniteData<ApiCollection<T>>,
|
|
47
|
+
any,
|
|
48
|
+
Partial<RequestData> | undefined
|
|
49
|
+
>({
|
|
50
|
+
queryKey: getRequestQueryKey(args),
|
|
51
|
+
queryFn: ({ pageParam }) => {
|
|
52
|
+
const pageParmWhere = pageParam?.where || {}
|
|
53
|
+
const argsWhere = args.data.where || {}
|
|
54
|
+
const where = { ...argsWhere, ...pageParmWhere }
|
|
55
|
+
|
|
56
|
+
const offset = pageParam?.offset || args.data.offset
|
|
57
|
+
const data = { ...args.data, where, offset }
|
|
58
|
+
|
|
59
|
+
return apiClient.chat.get({
|
|
60
|
+
url: args.url,
|
|
61
|
+
data,
|
|
62
|
+
})
|
|
63
|
+
},
|
|
64
|
+
initialPageParam: {} as Partial<RequestData>,
|
|
65
|
+
getNextPageParam: lastPage => {
|
|
66
|
+
const next: NextMeta = lastPage.meta?.next || {}
|
|
67
|
+
const { offset, idLt } = next
|
|
68
|
+
|
|
69
|
+
if (idLt) return { where: { id_lt: idLt } }
|
|
70
|
+
if (offset) return { offset: Number(offset) }
|
|
71
|
+
|
|
72
|
+
return undefined
|
|
73
|
+
},
|
|
74
|
+
...(opts || {}),
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const data: T[] = query.data?.pages.flatMap(page => page.data) || []
|
|
78
|
+
|
|
79
|
+
return { ...query, data }
|
|
80
|
+
}
|
|
@@ -2,26 +2,24 @@ import { useContext, useMemo } from 'react'
|
|
|
2
2
|
import { ChatContext } from '../contexts/chat_context'
|
|
3
3
|
import { Client } from '../utils/client'
|
|
4
4
|
|
|
5
|
-
type App = 'chat' | 'groups'
|
|
6
|
-
const apps: App[] = ['chat', 'groups']
|
|
7
|
-
|
|
5
|
+
type App = 'chat' | 'groups' | 'services'
|
|
6
|
+
const apps: App[] = ['chat', 'groups', 'services']
|
|
7
|
+
|
|
8
|
+
export type ApiClient = { [_K in App]: Client }
|
|
8
9
|
|
|
9
10
|
export const useApiClient = () => {
|
|
10
11
|
const { session, onTokenExpired } = useContext(ChatContext)
|
|
11
12
|
const api = useMemo(
|
|
12
13
|
() =>
|
|
13
|
-
apps.reduce(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
},
|
|
23
|
-
{} as Record<App, Client>
|
|
24
|
-
),
|
|
14
|
+
apps.reduce((acc, app) => {
|
|
15
|
+
acc[app] = new Client({
|
|
16
|
+
app,
|
|
17
|
+
session,
|
|
18
|
+
version: '2018-11-01',
|
|
19
|
+
onTokenExpired,
|
|
20
|
+
})
|
|
21
|
+
return acc
|
|
22
|
+
}, {} as ApiClient),
|
|
25
23
|
[session, onTokenExpired]
|
|
26
24
|
)
|
|
27
25
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AppGrantsResource } from '../types'
|
|
2
|
+
import { useApiGet } from './use_api'
|
|
3
|
+
|
|
4
|
+
export function useAppGrants() {
|
|
5
|
+
return useApiGet<AppGrantsResource[]>({
|
|
6
|
+
url: '/me/app_grants',
|
|
7
|
+
data: {
|
|
8
|
+
fields: {
|
|
9
|
+
AppGrant: ['create_conversations', 'app_name'],
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
app: 'chat',
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useCanCreateConversations(): boolean {
|
|
17
|
+
const { data: appGrants = [] } = useAppGrants()
|
|
18
|
+
|
|
19
|
+
return appGrants.some(appGrant => appGrant.createConversations)
|
|
20
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { useMutation } from '@tanstack/react-query'
|
|
1
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
2
|
+
import { useState } from 'react'
|
|
2
3
|
import { ApiResource, ConversationResource } from '../types'
|
|
3
|
-
import { getRequestQueryKey, useSuspenseGet } from './use_suspense_api'
|
|
4
|
-
import { queryClient } from '../contexts'
|
|
5
|
-
import { useApiClient } from './use_api_client'
|
|
6
4
|
import { transformGetToPost } from '../utils/client/request_helpers'
|
|
7
|
-
import {
|
|
5
|
+
import { useApiClient } from './use_api_client'
|
|
6
|
+
import { getRequestQueryKey, useSuspenseGet } from './use_suspense_api'
|
|
8
7
|
|
|
9
8
|
export const getConversationRequestArgs = ({ conversation_id }: { conversation_id: string }) => ({
|
|
10
9
|
url: `/me/conversations/${conversation_id}`,
|
|
@@ -39,6 +38,7 @@ export const useConversation = ({ conversation_id }) => {
|
|
|
39
38
|
|
|
40
39
|
export const useConversationMute = ({ conversation_id }: { conversation_id: string }) => {
|
|
41
40
|
const apiClient = useApiClient()
|
|
41
|
+
const queryClient = useQueryClient()
|
|
42
42
|
const requestArgs = getConversationRequestArgs({ conversation_id })
|
|
43
43
|
const queryKey = getRequestQueryKey(requestArgs)
|
|
44
44
|
const { data: conversation } = useConversation({ conversation_id })
|
|
@@ -82,6 +82,7 @@ export const useConversationMute = ({ conversation_id }: { conversation_id: stri
|
|
|
82
82
|
|
|
83
83
|
export const useConversationUpdate = ({ conversation_id }: { conversation_id: string }) => {
|
|
84
84
|
const apiClient = useApiClient()
|
|
85
|
+
const queryClient = useQueryClient()
|
|
85
86
|
const requestArgs = getConversationRequestArgs({ conversation_id })
|
|
86
87
|
const queryKey = getRequestQueryKey(requestArgs)
|
|
87
88
|
|
|
@@ -8,7 +8,13 @@ import { ApiCollection, ApiResource, ResourceObject } from '../types'
|
|
|
8
8
|
import { GetRequest, RequestData } from '../utils/client/types'
|
|
9
9
|
import { useApiClient } from './use_api_client'
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
interface SuspenseGetOptions extends GetRequest {
|
|
12
|
+
app?: 'chat' | 'groups'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const useSuspenseGet = <T extends ResourceObject | ResourceObject[]>(
|
|
16
|
+
args: SuspenseGetOptions
|
|
17
|
+
) => {
|
|
12
18
|
type Resource = ApiResource<T>
|
|
13
19
|
|
|
14
20
|
const { data, ...query } = useSuspenseQuery<Resource, Response>({
|
|
@@ -29,7 +35,7 @@ export type SuspensePaginatorOptions = Omit<
|
|
|
29
35
|
>
|
|
30
36
|
|
|
31
37
|
export const useSuspensePaginator = <T extends ResourceObject>(
|
|
32
|
-
args:
|
|
38
|
+
args: SuspenseGetOptions,
|
|
33
39
|
opts?: SuspensePaginatorOptions
|
|
34
40
|
) => {
|
|
35
41
|
const apiClient = useApiClient()
|
|
@@ -72,9 +78,15 @@ export const useSuspensePaginator = <T extends ResourceObject>(
|
|
|
72
78
|
return { ...query, data }
|
|
73
79
|
}
|
|
74
80
|
|
|
75
|
-
export type RequestQueryKey = [
|
|
76
|
-
|
|
81
|
+
export type RequestQueryKey = [
|
|
82
|
+
SuspenseGetOptions['url'],
|
|
83
|
+
SuspenseGetOptions['data'],
|
|
84
|
+
SuspenseGetOptions['headers'],
|
|
85
|
+
SuspenseGetOptions['app'],
|
|
86
|
+
]
|
|
87
|
+
export const getRequestQueryKey = (args: SuspenseGetOptions): RequestQueryKey => [
|
|
77
88
|
args.url,
|
|
78
89
|
args.data,
|
|
79
90
|
args.headers,
|
|
91
|
+
args.app || 'chat',
|
|
80
92
|
]
|
package/src/index.tsx
CHANGED
package/src/navigation/index.tsx
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { HeaderBackButton
|
|
1
|
+
import { HeaderBackButton } from '@react-navigation/elements'
|
|
2
2
|
import { StaticParamList } from '@react-navigation/native'
|
|
3
3
|
import {
|
|
4
4
|
createNativeStackNavigator,
|
|
5
|
-
NativeStackHeaderLeftProps,
|
|
6
5
|
NativeStackHeaderRightProps,
|
|
7
6
|
} from '@react-navigation/native-stack'
|
|
8
7
|
import React from 'react'
|
|
@@ -10,6 +9,12 @@ import { Icon } from '../components'
|
|
|
10
9
|
import { ConversationDetailsScreen } from '../screens/conversation_details_screen'
|
|
11
10
|
import { ConversationScreen } from '../screens/conversation_screen'
|
|
12
11
|
import { ConversationsScreen } from '../screens/conversations_screen'
|
|
12
|
+
import { ConversationCreateScreen } from '../screens/create/conversation_create_screen'
|
|
13
|
+
import {
|
|
14
|
+
ConversationFilterReceipientsScreenOptions,
|
|
15
|
+
ConversationFilterRecipientsScreen,
|
|
16
|
+
} from '../screens/create/conversation_filter_recipients_screen'
|
|
17
|
+
import { ConversationSelectRecipientsScreen } from '../screens/create/conversation_select_recipients_screen'
|
|
13
18
|
import {
|
|
14
19
|
MessageActionsScreen,
|
|
15
20
|
MessageActionsScreenOptions,
|
|
@@ -19,6 +24,38 @@ import { ReactionsScreen, ReactionsScreenOptions } from '../screens/reactions_sc
|
|
|
19
24
|
import { HeaderRightButton } from './header'
|
|
20
25
|
import { ScreenLayout } from './screenLayout'
|
|
21
26
|
|
|
27
|
+
export const CreateConversationStack = createNativeStackNavigator({
|
|
28
|
+
initialRouteName: 'ConversationSelectRecipients',
|
|
29
|
+
screenOptions: {
|
|
30
|
+
headerBackButtonDisplayMode: 'minimal',
|
|
31
|
+
},
|
|
32
|
+
screenLayout: ScreenLayout,
|
|
33
|
+
screens: {
|
|
34
|
+
ConversationSelectRecipients: {
|
|
35
|
+
screen: ConversationSelectRecipientsScreen,
|
|
36
|
+
options: ({ navigation }) => ({
|
|
37
|
+
title: 'New conversation',
|
|
38
|
+
headerRight: (props: NativeStackHeaderRightProps) => (
|
|
39
|
+
<HeaderRightButton {...props} onPress={navigation.goBack}>
|
|
40
|
+
Cancel
|
|
41
|
+
</HeaderRightButton>
|
|
42
|
+
),
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
ConversationFilterRecipients: {
|
|
46
|
+
screen: ConversationFilterRecipientsScreen,
|
|
47
|
+
options: ConversationFilterReceipientsScreenOptions,
|
|
48
|
+
},
|
|
49
|
+
ConversationCreate: {
|
|
50
|
+
screen: ConversationCreateScreen,
|
|
51
|
+
options: {
|
|
52
|
+
title: 'New conversation',
|
|
53
|
+
headerLeft: () => null,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
|
|
22
59
|
export const ChatStack = createNativeStackNavigator({
|
|
23
60
|
screenOptions: {
|
|
24
61
|
headerBackButtonDisplayMode: 'minimal',
|
|
@@ -29,11 +66,6 @@ export const ChatStack = createNativeStackNavigator({
|
|
|
29
66
|
screen: ConversationsScreen,
|
|
30
67
|
options: ({ route, navigation }) => ({
|
|
31
68
|
headerTitle: (route.params as { title?: string })?.title ?? 'Chat',
|
|
32
|
-
headerLeft: ({ tintColor }: NativeStackHeaderLeftProps) => (
|
|
33
|
-
<HeaderButton>
|
|
34
|
-
<Icon name="general.threeReducingHorizontalBars" size={18} color={tintColor} />
|
|
35
|
-
</HeaderButton>
|
|
36
|
-
),
|
|
37
69
|
headerRight: (props: NativeStackHeaderRightProps) => (
|
|
38
70
|
<HeaderBackButton
|
|
39
71
|
backImage={() => <Icon name="general.x" size={18} color={props.tintColor} />}
|
|
@@ -58,6 +90,13 @@ export const ChatStack = createNativeStackNavigator({
|
|
|
58
90
|
),
|
|
59
91
|
}),
|
|
60
92
|
},
|
|
93
|
+
Create: {
|
|
94
|
+
screen: CreateConversationStack,
|
|
95
|
+
options: {
|
|
96
|
+
headerShown: false,
|
|
97
|
+
presentation: 'modal',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
61
100
|
MessageActions: {
|
|
62
101
|
screen: MessageActionsScreen,
|
|
63
102
|
// Something about sheetAllowedDetents declared inline breaks TS
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import ErrorBoundary from '../components/page/error_boundary'
|
|
3
3
|
import { Suspense } from 'react'
|
|
4
|
-
import { ApiProvider } from '../contexts'
|
|
4
|
+
import { ApiProvider } from '../contexts/api_provider'
|
|
5
5
|
import { DefaultLoading } from '../components/page/loading'
|
|
6
6
|
|
|
7
7
|
export function ScreenLayout({ children }: { children: React.ReactElement }) {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { StackActions, StaticScreenProps, useNavigation } from '@react-navigation/native'
|
|
2
|
+
import React, { useCallback, useState } from 'react'
|
|
3
|
+
import { StyleSheet, TextInput, View } from 'react-native'
|
|
4
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
5
|
+
import { Text } from '../../components'
|
|
6
|
+
import { ActionButton } from '../../components/display/action_button'
|
|
7
|
+
import { useSuspenseGet, useTheme } from '../../hooks'
|
|
8
|
+
import { useApiClient } from '../../hooks/use_api_client'
|
|
9
|
+
import { useMutation } from '@tanstack/react-query'
|
|
10
|
+
import { GroupsGroupResource } from '../../types'
|
|
11
|
+
|
|
12
|
+
type ConversationCreateScreenProps = StaticScreenProps<{
|
|
13
|
+
group_id?: string
|
|
14
|
+
team_ids?: string[]
|
|
15
|
+
}>
|
|
16
|
+
|
|
17
|
+
export const ConversationCreateScreen = ({ route }: ConversationCreateScreenProps) => {
|
|
18
|
+
const styles = useStyles()
|
|
19
|
+
const navigation = useNavigation()
|
|
20
|
+
const [title, setTitle] = useState<string>()
|
|
21
|
+
const apiClient = useApiClient()
|
|
22
|
+
const { data: group } = useSuspenseGet<GroupsGroupResource>({
|
|
23
|
+
url: `/me/groups/${route.params.group_id}`,
|
|
24
|
+
data: {
|
|
25
|
+
fields: {
|
|
26
|
+
Group: [],
|
|
27
|
+
},
|
|
28
|
+
include: ['location'],
|
|
29
|
+
},
|
|
30
|
+
app: 'groups',
|
|
31
|
+
})
|
|
32
|
+
const { mutate: handleSave } = useMutation({
|
|
33
|
+
throwOnError: true,
|
|
34
|
+
onSuccess: result => {
|
|
35
|
+
handleRedirectToConversation({ conversation_id: result.data.id })
|
|
36
|
+
},
|
|
37
|
+
mutationFn: () =>
|
|
38
|
+
apiClient.groups
|
|
39
|
+
.post({
|
|
40
|
+
url: `/me/groups/${route.params.group_id}/chat_conversation_payload`,
|
|
41
|
+
data: {
|
|
42
|
+
data: {
|
|
43
|
+
type: '',
|
|
44
|
+
attributes: {
|
|
45
|
+
title,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
.then(res => res.data.value)
|
|
51
|
+
.then(payload =>
|
|
52
|
+
apiClient.chat.post({
|
|
53
|
+
url: '/me/conversations',
|
|
54
|
+
data: {
|
|
55
|
+
data: {
|
|
56
|
+
type: 'Conversation',
|
|
57
|
+
attributes: {
|
|
58
|
+
payload,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const handleRedirectToConversation = useCallback(
|
|
67
|
+
({ conversation_id }: { conversation_id: string }) => {
|
|
68
|
+
// exit from the create stack
|
|
69
|
+
navigation.getParent()?.goBack()
|
|
70
|
+
// navigate to the conversation screen
|
|
71
|
+
navigation.dispatch(
|
|
72
|
+
StackActions.push('Conversation', {
|
|
73
|
+
conversation_id,
|
|
74
|
+
})
|
|
75
|
+
)
|
|
76
|
+
},
|
|
77
|
+
[navigation]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<View style={styles.container}>
|
|
82
|
+
<View style={styles.section}>
|
|
83
|
+
<View style={styles.to}>
|
|
84
|
+
<Text>To:</Text>
|
|
85
|
+
<View>
|
|
86
|
+
<Text>{group.name}</Text>
|
|
87
|
+
<Text>{group.membershipsCount} members</Text>
|
|
88
|
+
</View>
|
|
89
|
+
</View>
|
|
90
|
+
<View style={styles.titleContainer}>
|
|
91
|
+
<Text style={styles.titleLabel}>Title</Text>
|
|
92
|
+
<TextInput
|
|
93
|
+
placeholder="Topic of conversation (required)"
|
|
94
|
+
value={title}
|
|
95
|
+
onChangeText={setTitle}
|
|
96
|
+
style={styles.titleInput}
|
|
97
|
+
/>
|
|
98
|
+
</View>
|
|
99
|
+
</View>
|
|
100
|
+
<ActionButton title="Start Conversation" onPress={() => handleSave()} />
|
|
101
|
+
</View>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const useStyles = () => {
|
|
106
|
+
const { bottom } = useSafeAreaInsets()
|
|
107
|
+
const theme = useTheme()
|
|
108
|
+
|
|
109
|
+
return StyleSheet.create({
|
|
110
|
+
container: {
|
|
111
|
+
flex: 1,
|
|
112
|
+
gap: 8,
|
|
113
|
+
},
|
|
114
|
+
section: {
|
|
115
|
+
padding: 16,
|
|
116
|
+
flex: 1,
|
|
117
|
+
},
|
|
118
|
+
sectionHeader: {
|
|
119
|
+
flexDirection: 'row',
|
|
120
|
+
justifyContent: 'space-between',
|
|
121
|
+
},
|
|
122
|
+
selectTeamsButton: {},
|
|
123
|
+
routeDebug: {
|
|
124
|
+
alignContent: 'center',
|
|
125
|
+
padding: 16,
|
|
126
|
+
paddingBottom: bottom,
|
|
127
|
+
borderTopWidth: 1,
|
|
128
|
+
borderTopColor: theme.colors.fillColorNeutral050Base,
|
|
129
|
+
},
|
|
130
|
+
titleContainer: {
|
|
131
|
+
paddingVertical: 12,
|
|
132
|
+
borderBottomWidth: 1,
|
|
133
|
+
borderBottomColor: theme.colors.fillColorNeutral050Base,
|
|
134
|
+
gap: 8,
|
|
135
|
+
},
|
|
136
|
+
titleInput: {
|
|
137
|
+
fontSize: 18,
|
|
138
|
+
},
|
|
139
|
+
titleLabel: {},
|
|
140
|
+
to: {
|
|
141
|
+
flexDirection: 'row',
|
|
142
|
+
gap: 8,
|
|
143
|
+
paddingVertical: 12,
|
|
144
|
+
borderBottomWidth: 1,
|
|
145
|
+
borderBottomColor: theme.colors.fillColorNeutral050Base,
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { StackActions, StaticScreenProps, useNavigation } from '@react-navigation/native'
|
|
2
|
+
import { NativeStackNavigationOptions } from '@react-navigation/native-stack'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { StyleSheet, View } from 'react-native'
|
|
5
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
6
|
+
import { Button } from '../../components'
|
|
7
|
+
import { useTheme } from '../../hooks'
|
|
8
|
+
|
|
9
|
+
export const ConversationFilterReceipientsScreenOptions: NativeStackNavigationOptions = {
|
|
10
|
+
presentation: 'formSheet',
|
|
11
|
+
headerShown: false,
|
|
12
|
+
sheetAllowedDetents: [0.75],
|
|
13
|
+
sheetGrabberVisible: true,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type ConversationFilterRecipientsScreenProps = StaticScreenProps<{}>
|
|
17
|
+
|
|
18
|
+
export const ConversationFilterRecipientsScreen = ({}: ConversationFilterRecipientsScreenProps) => {
|
|
19
|
+
const styles = useStyles()
|
|
20
|
+
const navigation = useNavigation()
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<View style={styles.container}>
|
|
24
|
+
<View style={styles.section}>
|
|
25
|
+
<Button
|
|
26
|
+
style={styles.selectTeamsButton}
|
|
27
|
+
onPress={() =>
|
|
28
|
+
navigation.dispatch(
|
|
29
|
+
StackActions.popTo('ConversationSelectRecipients', {
|
|
30
|
+
chat_group_graph_id: 'TBD',
|
|
31
|
+
})
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
title="Redirect to select"
|
|
35
|
+
variant="outline"
|
|
36
|
+
/>
|
|
37
|
+
|
|
38
|
+
<Button
|
|
39
|
+
style={styles.selectTeamsButton}
|
|
40
|
+
onPress={() =>
|
|
41
|
+
navigation.dispatch(
|
|
42
|
+
StackActions.popTo('ConversationCreate', {
|
|
43
|
+
chat_group_graph_id: 'TBD',
|
|
44
|
+
})
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
title="Redirect to form"
|
|
48
|
+
variant="outline"
|
|
49
|
+
/>
|
|
50
|
+
</View>
|
|
51
|
+
</View>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const useStyles = () => {
|
|
56
|
+
const { bottom } = useSafeAreaInsets()
|
|
57
|
+
const theme = useTheme()
|
|
58
|
+
|
|
59
|
+
return StyleSheet.create({
|
|
60
|
+
container: {
|
|
61
|
+
flex: 1,
|
|
62
|
+
gap: 8,
|
|
63
|
+
},
|
|
64
|
+
section: {
|
|
65
|
+
padding: 16,
|
|
66
|
+
flexDirection: 'row',
|
|
67
|
+
justifyContent: 'space-between',
|
|
68
|
+
flex: 1,
|
|
69
|
+
},
|
|
70
|
+
selectTeamsButton: {},
|
|
71
|
+
routeDebug: {
|
|
72
|
+
alignContent: 'center',
|
|
73
|
+
padding: 16,
|
|
74
|
+
paddingBottom: bottom,
|
|
75
|
+
borderTopWidth: 1,
|
|
76
|
+
borderTopColor: theme.colors.fillColorNeutral050Base,
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
}
|