@planningcenter/chat-react-native 2.1.1 → 2.2.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/build/components/conversation/message_form.d.ts.map +1 -1
  2. package/build/components/conversation/message_form.js +4 -4
  3. package/build/components/conversation/message_form.js.map +1 -1
  4. package/build/contexts/api_provider.js +4 -3
  5. package/build/contexts/api_provider.js.map +1 -1
  6. package/build/contexts/chat_context.d.ts +2 -2
  7. package/build/contexts/chat_context.d.ts.map +1 -1
  8. package/build/contexts/chat_context.js +3 -15
  9. package/build/contexts/chat_context.js.map +1 -1
  10. package/build/hooks/use_api_client.d.ts +6 -0
  11. package/build/hooks/use_api_client.d.ts.map +1 -0
  12. package/build/hooks/use_api_client.js +18 -0
  13. package/build/hooks/use_api_client.js.map +1 -0
  14. package/build/hooks/use_conversation.d.ts +122 -0
  15. package/build/hooks/use_conversation.d.ts.map +1 -0
  16. package/build/hooks/use_conversation.js +103 -0
  17. package/build/hooks/use_conversation.js.map +1 -0
  18. package/build/hooks/use_conversation_jolt_events.d.ts.map +1 -1
  19. package/build/hooks/use_conversation_jolt_events.js +3 -4
  20. package/build/hooks/use_conversation_jolt_events.js.map +1 -1
  21. package/build/hooks/use_jolt.d.ts +1 -1
  22. package/build/hooks/use_jolt.d.ts.map +1 -1
  23. package/build/hooks/use_jolt.js +6 -6
  24. package/build/hooks/use_jolt.js.map +1 -1
  25. package/build/hooks/use_suspense_api.d.ts.map +1 -1
  26. package/build/hooks/use_suspense_api.js +3 -4
  27. package/build/hooks/use_suspense_api.js.map +1 -1
  28. package/build/navigation/header.d.ts +10 -0
  29. package/build/navigation/header.d.ts.map +1 -0
  30. package/build/navigation/header.js +16 -0
  31. package/build/navigation/header.js.map +1 -0
  32. package/build/navigation/index.d.ts +17 -4
  33. package/build/navigation/index.d.ts.map +1 -1
  34. package/build/navigation/index.js +18 -6
  35. package/build/navigation/index.js.map +1 -1
  36. package/build/screens/conversation_details_screen.d.ts +7 -0
  37. package/build/screens/conversation_details_screen.d.ts.map +1 -0
  38. package/build/screens/conversation_details_screen.js +155 -0
  39. package/build/screens/conversation_details_screen.js.map +1 -0
  40. package/build/screens/conversation_screen.d.ts +5 -3
  41. package/build/screens/conversation_screen.d.ts.map +1 -1
  42. package/build/screens/conversation_screen.js +43 -15
  43. package/build/screens/conversation_screen.js.map +1 -1
  44. package/build/screens/message_actions_screen.d.ts.map +1 -1
  45. package/build/screens/message_actions_screen.js +7 -7
  46. package/build/screens/message_actions_screen.js.map +1 -1
  47. package/build/utils/client/request_helpers.d.ts +2 -1
  48. package/build/utils/client/request_helpers.d.ts.map +1 -1
  49. package/build/utils/client/request_helpers.js +17 -0
  50. package/build/utils/client/request_helpers.js.map +1 -1
  51. package/package.json +2 -2
  52. package/src/components/conversation/message_form.tsx +4 -4
  53. package/src/contexts/api_provider.tsx +5 -5
  54. package/src/contexts/chat_context.tsx +4 -21
  55. package/src/hooks/use_api_client.ts +29 -0
  56. package/src/hooks/use_conversation.ts +113 -0
  57. package/src/hooks/use_conversation_jolt_events.ts +3 -4
  58. package/src/hooks/use_jolt.ts +9 -9
  59. package/src/hooks/use_suspense_api.ts +3 -4
  60. package/src/navigation/header.tsx +24 -0
  61. package/src/navigation/index.tsx +20 -6
  62. package/src/screens/conversation_details_screen.tsx +205 -0
  63. package/src/screens/conversation_screen.tsx +65 -20
  64. package/src/screens/message_actions_screen.tsx +7 -7
  65. package/src/utils/client/request_helpers.ts +21 -1
@@ -1,21 +1,23 @@
1
- import React from 'react'
1
+ import { HeaderBackButton, HeaderButton } from '@react-navigation/elements'
2
2
  import { StaticParamList } from '@react-navigation/native'
3
3
  import {
4
4
  createNativeStackNavigator,
5
5
  NativeStackHeaderLeftProps,
6
6
  NativeStackHeaderRightProps,
7
7
  } from '@react-navigation/native-stack'
8
- import { NotFound } from '../screens/not_found'
9
- import { ScreenLayout } from './screenLayout'
10
- import { ConversationsScreen } from '../screens/conversations_screen'
11
- import { ConversationScreen } from '../screens/conversation_screen'
12
- import { HeaderBackButton, HeaderButton } from '@react-navigation/elements'
8
+ import React from 'react'
13
9
  import { Icon } from '../components'
10
+ import { ConversationDetailsScreen } from '../screens/conversation_details_screen'
11
+ import { ConversationScreen } from '../screens/conversation_screen'
12
+ import { ConversationsScreen } from '../screens/conversations_screen'
14
13
  import {
15
14
  MessageActionsScreen,
16
15
  MessageActionsScreenOptions,
17
16
  } from '../screens/message_actions_screen'
17
+ import { NotFound } from '../screens/not_found'
18
18
  import { ReactionsScreen, ReactionsScreenOptions } from '../screens/reactions_screen'
19
+ import { HeaderRightButton } from './header'
20
+ import { ScreenLayout } from './screenLayout'
19
21
 
20
22
  export const ChatStack = createNativeStackNavigator({
21
23
  screenOptions: {
@@ -44,6 +46,18 @@ export const ChatStack = createNativeStackNavigator({
44
46
  Conversation: {
45
47
  screen: ConversationScreen,
46
48
  },
49
+ ConversationDetails: {
50
+ screen: ConversationDetailsScreen,
51
+ options: ({ navigation }) => ({
52
+ presentation: 'modal',
53
+ title: 'Conversation details',
54
+ headerRight: (props: NativeStackHeaderRightProps) => (
55
+ <HeaderRightButton {...props} onPress={navigation.goBack}>
56
+ Done
57
+ </HeaderRightButton>
58
+ ),
59
+ }),
60
+ },
47
61
  MessageActions: {
48
62
  screen: MessageActionsScreen,
49
63
  // Something about sheetAllowedDetents declared inline breaks TS
@@ -0,0 +1,205 @@
1
+ import {
2
+ StaticScreenProps,
3
+ useNavigation,
4
+ useTheme as useNavigationTheme,
5
+ } from '@react-navigation/native'
6
+ import React, { PropsWithChildren, useCallback, useEffect, useState } from 'react'
7
+ import { FlatList, StyleSheet, TextInput, View } from 'react-native'
8
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
9
+ import { Avatar, Badge, Heading, Icon, Switch, Text } from '../components'
10
+ import { useSuspenseGet, useTheme } from '../hooks'
11
+ import {
12
+ useConversation,
13
+ useConversationMute,
14
+ useConversationUpdate,
15
+ } from '../hooks/use_conversation'
16
+ import { MemberResource } from '../types'
17
+ import { HeaderRightButton } from '../navigation/header'
18
+
19
+ export type ConversationDetailsScreenProps = StaticScreenProps<{
20
+ conversation_id: string
21
+ }>
22
+
23
+ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenProps) {
24
+ const styles = useStyles()
25
+ const navigation = useNavigation()
26
+ const { data: conversation } = useConversation(route.params)
27
+ const canUpdate = conversation.memberAbility?.canUpdate
28
+
29
+ const { data: members } = useSuspenseGet<MemberResource[]>({
30
+ url: `/me/conversations/${route.params.conversation_id}/members`,
31
+ data: {
32
+ include: ['person'],
33
+ fields: {
34
+ Member: ['avatar', 'name', 'first_name', 'last_name', 'child', 'badges'],
35
+ },
36
+ },
37
+ })
38
+
39
+ const { muted, setMuted } = useConversationMute(route.params)
40
+ const { mutate: saveTitle } = useConversationUpdate(route.params)
41
+ const [title, setTitle] = useState(conversation.title)
42
+ const HeaderRight = useCallback(() => {
43
+ return (
44
+ <HeaderRightButton
45
+ onPress={() => {
46
+ saveTitle({ title })
47
+ navigation.goBack()
48
+ }}
49
+ >
50
+ Done
51
+ </HeaderRightButton>
52
+ )
53
+ }, [navigation, saveTitle, title])
54
+
55
+ useEffect(() => {
56
+ navigation.setOptions({
57
+ headerRight: HeaderRight,
58
+ })
59
+ }, [HeaderRight, navigation])
60
+
61
+ return (
62
+ <View style={styles.container}>
63
+ <SectionList>
64
+ <View style={styles.titleContainer}>
65
+ <View style={[styles.titleLabelContainer, !canUpdate && styles.titleInputDisabled]}>
66
+ <Text variant="tertiary">Title</Text>
67
+ {!canUpdate && <Icon name="general.lock" />}
68
+ </View>
69
+ <TextInput
70
+ editable={canUpdate}
71
+ onChangeText={setTitle}
72
+ style={[styles.titleInput, !canUpdate && styles.titleInputDisabled]}
73
+ value={title}
74
+ />
75
+ </View>
76
+ </SectionList>
77
+ <SectionList>
78
+ <SectionListHeader>Settings</SectionListHeader>
79
+ <View style={styles.muteContainer}>
80
+ <Text variant="plain" style={styles.muteText}>
81
+ Mute
82
+ </Text>
83
+ <Switch value={muted} onChange={() => setMuted(!muted)} />
84
+ </View>
85
+ </SectionList>
86
+ <SectionList>
87
+ <SectionListHeader divider={false}>Members</SectionListHeader>
88
+ <FlatList
89
+ data={members}
90
+ renderItem={({ item }) => (
91
+ <View style={styles.member}>
92
+ <Avatar sourceUri={item.avatar} />
93
+ <View style={styles.memberBody}>
94
+ <Text style={styles.memberName}>{item.name}</Text>
95
+ <View style={styles.memberBadges}>
96
+ {item.badges?.map((badge, index) => (
97
+ <View key={index} style={styles.memberBadge}>
98
+ <Badge label={badge.title} />
99
+ </View>
100
+ ))}
101
+ </View>
102
+ </View>
103
+ </View>
104
+ )}
105
+ keyExtractor={item => item.id}
106
+ contentContainerStyle={styles.listContainer}
107
+ />
108
+ </SectionList>
109
+ </View>
110
+ )
111
+ }
112
+
113
+ const useStyles = () => {
114
+ const theme = useTheme()
115
+ const { bottom } = useSafeAreaInsets()
116
+
117
+ return StyleSheet.create({
118
+ container: {
119
+ flex: 1,
120
+ paddingTop: 16,
121
+ paddingHorizontal: 16,
122
+ backgroundColor: theme.colors.fillColorNeutral080,
123
+ paddingBottom: bottom,
124
+ gap: 16,
125
+ },
126
+ listContainer: {
127
+ gap: 12,
128
+ },
129
+ member: {
130
+ flexDirection: 'row',
131
+ gap: 8,
132
+ },
133
+ memberBody: {
134
+ gap: 4,
135
+ },
136
+ memberName: {
137
+ lineHeight: 16,
138
+ },
139
+ memberBadges: {
140
+ flexDirection: 'row',
141
+ gap: 8,
142
+ },
143
+ memberBadge: {},
144
+ muteContainer: {
145
+ flexDirection: 'row',
146
+ justifyContent: 'space-between',
147
+ alignItems: 'center',
148
+ },
149
+ muteText: {
150
+ lineHeight: 20,
151
+ },
152
+ titleContainer: {},
153
+ titleLabel: {},
154
+ titleLabelContainer: {
155
+ flexDirection: 'row',
156
+ alignItems: 'center',
157
+ gap: 8,
158
+ },
159
+ titleInput: {
160
+ color: theme.colors.textColorDefaultPrimary,
161
+ },
162
+ titleInputDisabled: { opacity: 0.7 },
163
+ })
164
+ }
165
+
166
+ const SectionList = ({ children }: PropsWithChildren) => {
167
+ const styles = useSectionListStyles()
168
+
169
+ return <View style={styles.container}>{children}</View>
170
+ }
171
+
172
+ const SectionListHeader = ({
173
+ children,
174
+ divider = true,
175
+ }: PropsWithChildren & { divider?: boolean }) => {
176
+ const styles = useSectionListStyles()
177
+
178
+ return (
179
+ <Heading variant="h3" style={[styles.heading, divider && styles.headingDivider]}>
180
+ {children}
181
+ </Heading>
182
+ )
183
+ }
184
+
185
+ const useSectionListStyles = () => {
186
+ const theme = useTheme()
187
+ const navigationTheme = useNavigationTheme()
188
+
189
+ return StyleSheet.create({
190
+ container: {
191
+ padding: 16,
192
+ backgroundColor: navigationTheme.colors.card,
193
+ borderRadius: 8,
194
+ flexDirection: 'column',
195
+ gap: 8,
196
+ },
197
+ heading: {
198
+ paddingBottom: 12,
199
+ },
200
+ headingDivider: {
201
+ borderBottomWidth: 1,
202
+ borderBottomColor: theme.colors.fillColorNeutral050Base,
203
+ },
204
+ })
205
+ }
@@ -1,44 +1,46 @@
1
+ import { HeaderTitle, HeaderTitleProps, PlatformPressable } from '@react-navigation/elements'
1
2
  import {
2
3
  StaticScreenProps,
3
4
  useNavigation,
4
5
  useTheme as useNavigationTheme,
6
+ useRoute,
5
7
  } from '@react-navigation/native'
6
- import React, { useEffect } from 'react'
7
- import { FlatList, StyleSheet, View } from 'react-native'
8
+ import React, { useCallback, useEffect } from 'react'
9
+ import { FlatList, Platform, StyleSheet, View } from 'react-native'
8
10
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
11
+ import { Icon, Text } from '../components'
9
12
  import { Message } from '../components/conversation/message'
10
13
  import { MessageForm } from '../components/conversation/message_form'
11
14
  import { KeyboardView } from '../components/display/keyboard_view'
15
+ import { useConversation } from '../hooks/use_conversation'
12
16
  import { useConversationMessages } from '../hooks/use_conversation_messages'
13
- import { useSuspenseGet } from '../hooks/use_suspense_api'
14
- import { ConversationResource } from '../types'
15
17
 
16
- export type ConversationScreenProps = StaticScreenProps<{
18
+ type ConversationRouteProps = {
17
19
  conversation_id: string
18
- chat_group_graph_id: string
19
- }>
20
+ chat_group_graph_id?: string
21
+ }
22
+
23
+ export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
20
24
 
21
25
  export function ConversationScreen({ route }: ConversationScreenProps) {
22
- const navigation = useNavigation()
23
- const { conversation_id } = route.params
24
26
  const styles = useStyles()
27
+ const navigation = useNavigation()
25
28
 
26
- const { data: conversation } = useSuspenseGet<ConversationResource>({
27
- url: `/me/conversations/${conversation_id}`,
28
- data: {
29
- fields: {
30
- Conversation: ['title'],
31
- },
32
- },
33
- })
34
-
29
+ const { conversation_id } = route.params
30
+ const { data: conversation } = useConversation(route.params)
35
31
  const { messages, refetch, isRefetching, fetchNextPage } = useConversationMessages({
36
32
  conversation_id,
37
33
  })
38
34
 
35
+ // Seems to be necessary to define this way so we get the route picked up
36
+ const headerTitle = useCallback(
37
+ (props: HeaderTitleProps) => <PressableHeaderTitle {...props} />,
38
+ []
39
+ )
40
+
39
41
  useEffect(() => {
40
- navigation.setOptions({ title: conversation?.title })
41
- }, [conversation, conversation_id, navigation])
42
+ navigation.setOptions({ headerTitle, title: conversation?.title })
43
+ }, [conversation, conversation_id, navigation, headerTitle, conversation?.title])
42
44
 
43
45
  return (
44
46
  <View style={styles.container}>
@@ -64,6 +66,49 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
64
66
  )
65
67
  }
66
68
 
69
+ const PressableHeaderTitle = ({ style, children }: HeaderTitleProps) => {
70
+ const styles = usePressableHeaderStyle()
71
+ const navigation = useNavigation()
72
+
73
+ const route = useRoute() as ConversationScreenProps['route']
74
+
75
+ const { data: conversation } = useConversation(route.params)
76
+ const badge = conversation.badges?.[0]
77
+ const subtitle = [badge?.pcoResourceType, badge?.text].filter(f => f).join(': ')
78
+
79
+ return (
80
+ <PlatformPressable
81
+ style={styles.container}
82
+ onPress={() =>
83
+ navigation.navigate('ConversationDetails', { conversation_id: conversation?.id })
84
+ }
85
+ >
86
+ <View style={styles.titleWrapper}>
87
+ <HeaderTitle style={[styles.title, style]}>{children}</HeaderTitle>
88
+ <Icon name="general.downChevron" size={12} />
89
+ </View>
90
+ <Text variant="tertiary">{subtitle}</Text>
91
+ </PlatformPressable>
92
+ )
93
+ }
94
+
95
+ const usePressableHeaderStyle = () => {
96
+ return StyleSheet.create({
97
+ container: {
98
+ alignItems: Platform.select({ android: 'flex-start', default: 'center' }),
99
+ },
100
+ titleWrapper: {
101
+ alignItems: 'center',
102
+ columnGap: 8,
103
+ flexDirection: 'row',
104
+ },
105
+ title: {
106
+ padding: 0,
107
+ margin: 0,
108
+ },
109
+ })
110
+ }
111
+
67
112
  const useStyles = () => {
68
113
  const navigationTheme = useNavigationTheme()
69
114
  const { bottom } = useSafeAreaInsets()
@@ -2,13 +2,13 @@ import { PlatformPressable } from '@react-navigation/elements'
2
2
  import { StaticScreenProps, useNavigation } from '@react-navigation/native'
3
3
  import { NativeStackNavigationOptions } from '@react-navigation/native-stack'
4
4
  import { InfiniteData, useMutation, useQueryClient } from '@tanstack/react-query'
5
- import React, { useCallback, useContext } from 'react'
5
+ import React, { useCallback } from 'react'
6
6
  import { Alert, Platform, StyleSheet, useWindowDimensions, View } from 'react-native'
7
7
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
8
8
  import { Text, TextButton } from '../components'
9
9
  import { REACTION_EMOJIS, useReactionStyles } from '../components/conversation/message_reaction'
10
- import { ChatContext } from '../contexts'
11
10
  import { useTheme } from '../hooks'
11
+ import { useApiClient } from '../hooks/use_api_client'
12
12
  import { getMessagesRequestArgs, useConversationMessages } from '../hooks/use_conversation_messages'
13
13
  import { ApiCollection, ApiResource, MessageResource } from '../types'
14
14
  import { ReactionCountResource } from '../types/resources/reaction'
@@ -31,7 +31,7 @@ export function MessageActionsScreen({ route }: ReactionScreenProps) {
31
31
  const navigation = useNavigation()
32
32
  const { conversation_id, message_id } = route.params
33
33
 
34
- const { client } = useContext(ChatContext)
34
+ const apiClient = useApiClient()
35
35
  const queryClient = useQueryClient()
36
36
  const styles = useStyles()
37
37
 
@@ -68,7 +68,7 @@ export function MessageActionsScreen({ route }: ReactionScreenProps) {
68
68
  Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')])
69
69
  )
70
70
 
71
- return client.post({
71
+ return apiClient.chat.post({
72
72
  url,
73
73
  data: {
74
74
  ...requestParams.data,
@@ -80,14 +80,14 @@ export function MessageActionsScreen({ route }: ReactionScreenProps) {
80
80
  },
81
81
  })
82
82
  },
83
- [client, conversation_id, message_id]
83
+ [apiClient, conversation_id, message_id]
84
84
  )
85
85
 
86
86
  const deleteMessage = useCallback(() => {
87
87
  const url = `/me/conversations/${conversation_id}/messages/${message_id}/`
88
88
 
89
- return client.delete({ url })
90
- }, [client, conversation_id, message_id])
89
+ return apiClient.chat.delete({ url })
90
+ }, [apiClient, conversation_id, message_id])
91
91
 
92
92
  const { mutate: handleReaction, isPending } = useMutation({
93
93
  mutationFn: handleReactionPress,
@@ -2,7 +2,7 @@ import _ from 'lodash'
2
2
  import URI from 'urijs'
3
3
  import { transformRequestData } from './transform_request_data'
4
4
  import transformResponse from './transform_response'
5
- import { Accumulator, RequestData } from './types'
5
+ import { Accumulator, GetRequest, PostRequest, RequestData } from './types'
6
6
 
7
7
  export type MakeRequestArgs = {
8
8
  action: 'GET' | 'POST' | 'PATCH' | 'DELETE'
@@ -112,3 +112,23 @@ export const throwErrorIfQueryParams = url => {
112
112
 
113
113
  return Promise.resolve()
114
114
  }
115
+
116
+ export const transformGetToPost = (args: GetRequest): PostRequest => {
117
+ const { data, ...rest } = args
118
+ const { fields, include } = data
119
+ const fieldsArray = Object.entries(fields).map(([key, value]) => {
120
+ if (Array.isArray(value)) {
121
+ return [key, value.join(',')]
122
+ }
123
+
124
+ return [key, value]
125
+ })
126
+
127
+ return {
128
+ ...rest,
129
+ data: {
130
+ fields: Object.fromEntries(fieldsArray),
131
+ include,
132
+ },
133
+ }
134
+ }