@planningcenter/chat-react-native 3.2.0-rc.4 → 3.2.0-rc.5

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 (40) hide show
  1. package/build/components/conversation/message_form.d.ts +2 -2
  2. package/build/components/conversation/message_form.d.ts.map +1 -1
  3. package/build/components/conversation/message_form.js +95 -41
  4. package/build/components/conversation/message_form.js.map +1 -1
  5. package/build/contexts/chat_context.d.ts +1 -0
  6. package/build/contexts/chat_context.d.ts.map +1 -1
  7. package/build/contexts/chat_context.js +3 -1
  8. package/build/contexts/chat_context.js.map +1 -1
  9. package/build/hooks/use_giphy.d.ts +9 -0
  10. package/build/hooks/use_giphy.d.ts.map +1 -0
  11. package/build/hooks/use_giphy.js +63 -0
  12. package/build/hooks/use_giphy.js.map +1 -0
  13. package/build/hooks/use_message_create.d.ts +11 -0
  14. package/build/hooks/use_message_create.d.ts.map +1 -0
  15. package/build/hooks/use_message_create.js +35 -0
  16. package/build/hooks/use_message_create.js.map +1 -0
  17. package/build/navigation/index.d.ts +5 -0
  18. package/build/navigation/index.d.ts.map +1 -1
  19. package/build/navigation/index.js +5 -0
  20. package/build/navigation/index.js.map +1 -1
  21. package/build/screens/conversation_screen.d.ts +1 -0
  22. package/build/screens/conversation_screen.d.ts.map +1 -1
  23. package/build/screens/conversation_screen.js +1 -1
  24. package/build/screens/conversation_screen.js.map +1 -1
  25. package/build/screens/message_actions_screen.d.ts +2 -2
  26. package/build/screens/message_actions_screen.d.ts.map +1 -1
  27. package/build/screens/message_actions_screen.js.map +1 -1
  28. package/build/screens/send_giphy_screen.d.ts +10 -0
  29. package/build/screens/send_giphy_screen.d.ts.map +1 -0
  30. package/build/screens/send_giphy_screen.js +98 -0
  31. package/build/screens/send_giphy_screen.js.map +1 -0
  32. package/package.json +2 -2
  33. package/src/components/conversation/message_form.tsx +127 -58
  34. package/src/contexts/chat_context.tsx +4 -1
  35. package/src/hooks/use_giphy.ts +97 -0
  36. package/src/hooks/use_message_create.ts +55 -0
  37. package/src/navigation/index.tsx +5 -0
  38. package/src/screens/conversation_screen.tsx +2 -1
  39. package/src/screens/message_actions_screen.tsx +2 -2
  40. package/src/screens/send_giphy_screen.tsx +155 -0
@@ -0,0 +1,97 @@
1
+ import { useCallback, useContext, useEffect, useState } from 'react'
2
+ import { DenormalizedGiphyAttachmentResourceForCreate } from '../types/resources/denormalized_attachment_resource'
3
+ import { ChatContext } from '../contexts/chat_context'
4
+
5
+ interface GiphyGif {
6
+ id: string
7
+ url: string
8
+ title: string
9
+ images: {
10
+ original: GiphyVariant
11
+ fixed_height: GiphyVariant
12
+ fixed_height_still: GiphyVariant
13
+ fixed_height_downsampled: GiphyVariant
14
+ fixed_width: GiphyVariant
15
+ fixed_width_still: GiphyVariant
16
+ fixed_width_downsampled: GiphyVariant
17
+ }
18
+ }
19
+
20
+ interface GiphyVariant {
21
+ url: string
22
+ width: number
23
+ height: number
24
+ size: string
25
+ frames: string
26
+ }
27
+
28
+ export function useGiphy(term: string = '') {
29
+ const { giphyApiKey } = useContext(ChatContext)
30
+ const [isSearching, setIsSearching] = useState(false)
31
+ const [results, setResults] = useState<DenormalizedGiphyAttachmentResourceForCreate[]>([])
32
+ const [resultIndex, setResultIndex] = useState(0)
33
+ const result = results[resultIndex]
34
+
35
+ useEffect(() => {
36
+ if (!giphyApiKey || !term) {
37
+ setResults([])
38
+ return
39
+ }
40
+
41
+ setIsSearching(true)
42
+ fetch(`https://api.giphy.com/v1/gifs/search?api_key=${giphyApiKey}&q=${term}&limit=20&rating=g`)
43
+ .then(response => response.json())
44
+ .then(data => {
45
+ const originalGiphys: GiphyGif[] = data.data
46
+ const denormalizedGiphys = denormalizeGiphys(term, originalGiphys)
47
+
48
+ setResults(denormalizedGiphys)
49
+ setResultIndex(0)
50
+ })
51
+ .catch(error => {
52
+ console.error('Error fetching gifs:', error)
53
+ })
54
+ .finally(() => {
55
+ setIsSearching(false)
56
+ })
57
+ }, [giphyApiKey, term])
58
+
59
+ const nextResult = useCallback(() => {
60
+ setResultIndex(oldResultIndex => (oldResultIndex < results.length - 1 ? oldResultIndex + 1 : 0))
61
+ }, [results.length])
62
+
63
+ const prevResult = useCallback(() => {
64
+ setResultIndex(oldResultIndex => (oldResultIndex > 0 ? oldResultIndex - 1 : results.length - 1))
65
+ }, [results.length])
66
+
67
+ return {
68
+ isSearching,
69
+ result,
70
+ resultIndex,
71
+ nextResult,
72
+ prevResult,
73
+ }
74
+ }
75
+
76
+ function denormalizeGiphys(
77
+ term: string,
78
+ giphys: GiphyGif[]
79
+ ): DenormalizedGiphyAttachmentResourceForCreate[] {
80
+ return giphys.map(giphy => ({
81
+ type: 'giphy',
82
+ id: giphy.id,
83
+ title: term,
84
+ original_giphy_title: giphy.title,
85
+ title_link: giphy.url,
86
+ thumb_url: giphy.images.fixed_width.url,
87
+ giphy: {
88
+ original: giphy.images.original,
89
+ fixed_height: giphy.images.fixed_height,
90
+ fixed_height_still: giphy.images.fixed_height_still,
91
+ fixed_height_downsampled: giphy.images.fixed_height_downsampled,
92
+ fixed_width: giphy.images.fixed_width,
93
+ fixed_width_still: giphy.images.fixed_width_still,
94
+ fixed_width_downsampled: giphy.images.fixed_width_downsampled,
95
+ },
96
+ }))
97
+ }
@@ -0,0 +1,55 @@
1
+ import { InfiniteData, useMutation } from '@tanstack/react-query'
2
+ import { getMessagesQueryKey, getMessagesRequestArgs } from './use_conversation_messages'
3
+ import { useApiClient } from './use_api_client'
4
+ import { ApiCollection, ApiResource, MessageResource } from '../types'
5
+ import { queryClient } from '../contexts/api_provider'
6
+ import { updateOrCreateRecordInPagesData } from '../utils'
7
+ import { DenormalizedAttachmentResourceForCreate } from '../types/resources/denormalized_attachment_resource'
8
+
9
+ interface Props {
10
+ conversationId: number
11
+ }
12
+
13
+ export function useMessageCreate({ conversationId }: Props) {
14
+ const apiClient = useApiClient()
15
+ const mutation = useMutation({
16
+ mutationFn: ({
17
+ text,
18
+ attachments,
19
+ }: {
20
+ text: string
21
+ attachments?: DenormalizedAttachmentResourceForCreate[]
22
+ }) => {
23
+ const requestParams = getMessagesRequestArgs({ conversation_id: conversationId })
24
+ const fieldsWithValueJoined = Object.fromEntries(
25
+ Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')])
26
+ )
27
+
28
+ return apiClient.chat.post<ApiResource<MessageResource>>({
29
+ url: `/me/conversations/${conversationId}/messages`,
30
+ data: {
31
+ ...requestParams.data,
32
+ data: {
33
+ type: 'Message',
34
+ attributes: { text, ...(attachments ? { attachments } : {}) },
35
+ },
36
+ fields: fieldsWithValueJoined,
37
+ },
38
+ })
39
+ },
40
+ onSuccess: (result: ApiResource<MessageResource>) => {
41
+ const updatedMessage = result.data
42
+ type QueryData = InfiniteData<ApiCollection<MessageResource>>
43
+ const queryKey = getMessagesQueryKey({ conversation_id: conversationId })
44
+
45
+ queryClient.setQueryData<QueryData>(queryKey, data =>
46
+ updateOrCreateRecordInPagesData({
47
+ data,
48
+ record: updatedMessage,
49
+ })
50
+ )
51
+ },
52
+ })
53
+
54
+ return mutation
55
+ }
@@ -19,6 +19,7 @@ import {
19
19
  MessageActionsScreen,
20
20
  MessageActionsScreenOptions,
21
21
  } from '../screens/message_actions_screen'
22
+ import { SendGiphyScreen, SendGiphyScreenOptions } from '../screens/send_giphy_screen'
22
23
  import { NotFound } from '../screens/not_found'
23
24
  import { ReactionsScreen, ReactionsScreenOptions } from '../screens/reactions_screen'
24
25
  import { HeaderRightButton } from './header'
@@ -123,6 +124,10 @@ export const ChatStack = createNativeStackNavigator({
123
124
  presentation: 'modal',
124
125
  },
125
126
  },
127
+ SendGiphy: {
128
+ screen: SendGiphyScreen,
129
+ options: SendGiphyScreenOptions,
130
+ },
126
131
  MessageActions: {
127
132
  screen: MessageActionsScreen,
128
133
  // Something about sheetAllowedDetents declared inline breaks TS
@@ -18,6 +18,7 @@ import { useConversationMessages } from '../hooks/use_conversation_messages'
18
18
  type ConversationRouteProps = {
19
19
  conversation_id: number
20
20
  chat_group_graph_id?: string
21
+ clear_input?: boolean
21
22
  }
22
23
 
23
24
  export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
@@ -57,7 +58,7 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
57
58
  />
58
59
  <MessageForm.Root conversation={conversation}>
59
60
  {/* <MessageForm.AttachmentPicker /> */}
60
- {/* <MessageForm.Commands /> */}
61
+ <MessageForm.Commands />
61
62
  <MessageForm.TextInput />
62
63
  <MessageForm.SubmitButton />
63
64
  </MessageForm.Root>
@@ -22,12 +22,12 @@ export const MessageActionsScreenOptions: NativeStackNavigationOptions = {
22
22
  sheetGrabberVisible: true,
23
23
  }
24
24
 
25
- export type ReactionScreenProps = StaticScreenProps<{
25
+ export type MessageActionsScreenProps = StaticScreenProps<{
26
26
  message_id: string
27
27
  conversation_id: number
28
28
  }>
29
29
 
30
- export function MessageActionsScreen({ route }: ReactionScreenProps) {
30
+ export function MessageActionsScreen({ route }: MessageActionsScreenProps) {
31
31
  const navigation = useNavigation()
32
32
  const { conversation_id, message_id } = route.params
33
33
 
@@ -0,0 +1,155 @@
1
+ import { StackActions, StaticScreenProps, useNavigation } from '@react-navigation/native'
2
+ import { NativeStackNavigationOptions } from '@react-navigation/native-stack'
3
+ import React from 'react'
4
+ import { Image as NativeImage, StyleSheet, useWindowDimensions, View } from 'react-native'
5
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
6
+ import { useTheme } from '../hooks'
7
+ import { useGiphy } from '../hooks/use_giphy'
8
+ import { useMessageCreate } from '../hooks/use_message_create'
9
+ import { Button, IconButton } from '../components'
10
+ import { MAX_FONT_SIZE_MULTIPLIER } from '../utils'
11
+ import { DefaultLoading } from '../components/page/loading'
12
+
13
+ export const SendGiphyScreenOptions: NativeStackNavigationOptions = {
14
+ presentation: 'formSheet',
15
+ headerShown: false,
16
+ sheetAllowedDetents: [0.75],
17
+ sheetGrabberVisible: true,
18
+ }
19
+
20
+ export type SendGiphyScreenProps = StaticScreenProps<{
21
+ conversation_id: number
22
+ search_term: string
23
+ }>
24
+
25
+ export function SendGiphyScreen({ route }: SendGiphyScreenProps) {
26
+ const { conversation_id, search_term } = route.params
27
+ const styles = useStyles()
28
+ const navigation = useNavigation()
29
+
30
+ const { isPending, mutate } = useMessageCreate({ conversationId: conversation_id })
31
+ const { isSearching, result, nextResult, prevResult } = useGiphy(search_term)
32
+ const disabled = isPending || isSearching
33
+
34
+ const { width } = useWindowDimensions()
35
+ const size = width - 24
36
+
37
+ if (!result) return null
38
+
39
+ const gif = result.giphy.fixed_width
40
+ const { url } = gif
41
+
42
+ function goBack({ clearInput = false }) {
43
+ if (clearInput) {
44
+ const routes = navigation.getState()?.routes || []
45
+ const conversationParams = routes.find(r => r.name === 'Conversation')?.params || {}
46
+
47
+ navigation.dispatch(
48
+ StackActions.popTo('Conversation', {
49
+ ...conversationParams,
50
+ conversation_id,
51
+ clear_input: true,
52
+ })
53
+ )
54
+ } else {
55
+ navigation.goBack()
56
+ }
57
+ }
58
+
59
+ function sendGiphy() {
60
+ if (disabled) return
61
+
62
+ mutate({ text: '', attachments: [result] })
63
+ goBack({ clearInput: true })
64
+ }
65
+
66
+ return (
67
+ <View style={styles.container}>
68
+ <NativeImage
69
+ alt="Powered by Giphy"
70
+ style={styles.powered_by_giphy}
71
+ source={require('../../assets/images/powered_by_giphy.png')}
72
+ />
73
+ <View style={styles.control_stack}>
74
+ <View style={styles.button_group}>
75
+ <IconButton
76
+ name="general.leftArrow"
77
+ accessibilityLabel={'Previous GIF'}
78
+ size="md"
79
+ appearance="neutral"
80
+ onPress={prevResult}
81
+ disabled={disabled}
82
+ />
83
+ <IconButton
84
+ name="general.rightArrow"
85
+ accessibilityLabel={'Next GIF'}
86
+ size="md"
87
+ appearance="neutral"
88
+ onPress={nextResult}
89
+ disabled={disabled}
90
+ />
91
+ </View>
92
+ <View style={styles.button_group}>
93
+ <Button
94
+ onPress={() => goBack({ clearInput: false })}
95
+ title="Cancel"
96
+ variant="outline"
97
+ size="sm"
98
+ maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER}
99
+ />
100
+ <Button
101
+ onPress={sendGiphy}
102
+ title="Send"
103
+ size="sm"
104
+ disabled={disabled}
105
+ maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER}
106
+ />
107
+ </View>
108
+ </View>
109
+ {isSearching ? (
110
+ <DefaultLoading />
111
+ ) : (
112
+ <NativeImage
113
+ source={{ uri: url }}
114
+ alt={result.title}
115
+ style={[styles.image, { width: size, height: size }]}
116
+ />
117
+ )}
118
+ </View>
119
+ )
120
+ }
121
+
122
+ const useStyles = () => {
123
+ const theme = useTheme()
124
+ const { height } = useWindowDimensions()
125
+ const { bottom } = useSafeAreaInsets()
126
+
127
+ return StyleSheet.create({
128
+ container: {
129
+ justifyContent: 'flex-start',
130
+ paddingTop: 24,
131
+ paddingHorizontal: 12,
132
+ paddingBottom: bottom,
133
+ width: '100%',
134
+ backgroundColor: theme.colors.fillColorNeutral100Inverted,
135
+ height,
136
+ gap: 12,
137
+ },
138
+ powered_by_giphy: {
139
+ width: 200,
140
+ height: 22,
141
+ },
142
+ control_stack: {
143
+ flexDirection: 'row',
144
+ justifyContent: 'space-between',
145
+ alignItems: 'center',
146
+ },
147
+ button_group: {
148
+ flexDirection: 'row',
149
+ gap: 12,
150
+ },
151
+ image: {
152
+ borderRadius: 8,
153
+ },
154
+ })
155
+ }