@planningcenter/chat-react-native 3.16.0-rc.1 → 3.16.0-rc.3

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 (56) hide show
  1. package/build/components/conversation/attachments/giphy_attachment.d.ts.map +1 -1
  2. package/build/components/conversation/attachments/giphy_attachment.js +1 -11
  3. package/build/components/conversation/attachments/giphy_attachment.js.map +1 -1
  4. package/build/components/conversation/message.d.ts.map +1 -1
  5. package/build/components/conversation/message.js +5 -21
  6. package/build/components/conversation/message.js.map +1 -1
  7. package/build/components/conversation/shadow_message.d.ts +7 -0
  8. package/build/components/conversation/shadow_message.d.ts.map +1 -0
  9. package/build/components/conversation/shadow_message.js +156 -0
  10. package/build/components/conversation/shadow_message.js.map +1 -0
  11. package/build/components/primitive/avatar_primitive.d.ts +1 -0
  12. package/build/components/primitive/avatar_primitive.d.ts.map +1 -1
  13. package/build/components/primitive/avatar_primitive.js +4 -0
  14. package/build/components/primitive/avatar_primitive.js.map +1 -1
  15. package/build/hooks/index.d.ts +1 -0
  16. package/build/hooks/index.d.ts.map +1 -1
  17. package/build/hooks/index.js +1 -0
  18. package/build/hooks/index.js.map +1 -1
  19. package/build/hooks/use_animated_message_background_color.d.ts +8 -0
  20. package/build/hooks/use_animated_message_background_color.d.ts.map +1 -0
  21. package/build/hooks/use_animated_message_background_color.js +29 -0
  22. package/build/hooks/use_animated_message_background_color.js.map +1 -0
  23. package/build/hooks/use_app_name.js +1 -1
  24. package/build/hooks/use_app_name.js.map +1 -1
  25. package/build/navigation/index.d.ts +2 -1
  26. package/build/navigation/index.d.ts.map +1 -1
  27. package/build/navigation/index.js +2 -1
  28. package/build/navigation/index.js.map +1 -1
  29. package/build/screens/bug_report_screen.d.ts.map +1 -1
  30. package/build/screens/bug_report_screen.js +12 -5
  31. package/build/screens/bug_report_screen.js.map +1 -1
  32. package/build/screens/get_help_screen.d.ts +2 -0
  33. package/build/screens/get_help_screen.d.ts.map +1 -1
  34. package/build/screens/get_help_screen.js +34 -4
  35. package/build/screens/get_help_screen.js.map +1 -1
  36. package/build/utils/assert_keys_are_numbers.d.ts +2 -0
  37. package/build/utils/assert_keys_are_numbers.d.ts.map +1 -0
  38. package/build/utils/assert_keys_are_numbers.js +12 -0
  39. package/build/utils/assert_keys_are_numbers.js.map +1 -0
  40. package/build/utils/index.d.ts +1 -0
  41. package/build/utils/index.d.ts.map +1 -1
  42. package/build/utils/index.js +1 -0
  43. package/build/utils/index.js.map +1 -1
  44. package/package.json +2 -2
  45. package/src/components/conversation/attachments/giphy_attachment.tsx +1 -14
  46. package/src/components/conversation/message.tsx +11 -35
  47. package/src/components/conversation/shadow_message.tsx +266 -0
  48. package/src/components/primitive/avatar_primitive.tsx +4 -0
  49. package/src/hooks/index.ts +1 -0
  50. package/src/hooks/use_animated_message_background_color.ts +44 -0
  51. package/src/hooks/use_app_name.ts +1 -1
  52. package/src/navigation/index.tsx +2 -1
  53. package/src/screens/bug_report_screen.tsx +12 -5
  54. package/src/screens/get_help_screen.tsx +36 -4
  55. package/src/utils/assert_keys_are_numbers.ts +13 -0
  56. package/src/utils/index.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.16.0-rc.1",
3
+ "version": "3.16.0-rc.3",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -55,5 +55,5 @@
55
55
  "react-native-url-polyfill": "^2.0.0",
56
56
  "typescript": "<5.6.0"
57
57
  },
58
- "gitHead": "47e50810584fa31ba56562462c46916de42d51ec"
58
+ "gitHead": "41458eebf252d441367b1c30a6b35e5422486bf2"
59
59
  }
@@ -5,6 +5,7 @@ import { useTheme } from '../../../hooks'
5
5
  import { DenormalizedGiphyAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'
6
6
  import { PlatformPressable } from '@react-navigation/elements'
7
7
  import { Image } from '../../display'
8
+ import { assertKeysAreNumbers } from '../../../utils'
8
9
 
9
10
  export function GiphyAttachment({
10
11
  attachment,
@@ -46,20 +47,6 @@ export function GiphyAttachment({
46
47
  )
47
48
  }
48
49
 
49
- const assertKeysAreNumbers = (obj: Object): Record<any, number> => {
50
- return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, assertNumber(value)]))
51
- }
52
-
53
- const assertNumber = (value: string): number => {
54
- if (typeof value === 'number') return value
55
-
56
- if (typeof value === 'string' && !isNaN(Number(value))) {
57
- return Number(value)
58
- }
59
-
60
- return 0
61
- }
62
-
63
50
  const useStyles = ({ imageWidth, imageHeight }: { imageWidth: number; imageHeight: number }) => {
64
51
  const { colors } = useTheme()
65
52
  return StyleSheet.create({
@@ -1,9 +1,13 @@
1
1
  import { useNavigation } from '@react-navigation/native'
2
2
  import React, { useEffect } from 'react'
3
- import { Platform, Pressable, StyleSheet, useWindowDimensions, View } from 'react-native'
3
+ import { Pressable, StyleSheet, useWindowDimensions, View } from 'react-native'
4
4
  import { MessageReaction } from '../../components/conversation/message_reaction'
5
5
  import { Avatar, Icon, Spinner, Text, TextInlineButton } from '../../components/display'
6
- import { useInteractionGhostBackgroundColor, useTheme } from '../../hooks'
6
+ import {
7
+ useAnimatedMessageBackgroundColor,
8
+ useInteractionGhostBackgroundColor,
9
+ useTheme,
10
+ } from '../../hooks'
7
11
  import { MessageResource } from '../../types'
8
12
  import { ReactionCountResource } from '../../types/resources/reaction'
9
13
  import { MessageAttachments } from './message_attachments'
@@ -15,13 +19,7 @@ import {
15
19
  MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
16
20
  platformFontWeightMedium,
17
21
  } from '../../utils/styles'
18
- import Animated, {
19
- useSharedValue,
20
- useAnimatedStyle,
21
- withTiming,
22
- interpolateColor,
23
- Easing,
24
- } from 'react-native-reanimated'
22
+ import Animated from 'react-native-reanimated'
25
23
  import { useLiveRelativeTime } from '../../hooks/use_live_relative_time'
26
24
  import { MessageReadReceipts } from './message_read_receipts'
27
25
  import { isNewMessage, useMessageCreateOrUpdate } from '../../hooks/use_message_create_or_update'
@@ -56,6 +54,8 @@ export function Message({
56
54
  const isPersisted = !isNewMessage(message)
57
55
  const [temporarilyHidePendingState, setTemporarilyHidePendingState] = React.useState(true)
58
56
  const [messageBubbleHeight, setMessageBubbleHeight] = React.useState(0)
57
+ const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
58
+ useAnimatedMessageBackgroundColor()
59
59
 
60
60
  useEffect(() => {
61
61
  if (pending) {
@@ -81,30 +81,6 @@ export function Message({
81
81
  }
82
82
  }
83
83
 
84
- const bgFadeProgress = useSharedValue(0)
85
- const pressedColor = Platform.select({
86
- ios: colors.fillColorNeutral050Base,
87
- default: 'transparent',
88
- })
89
- const animatedBackgroundColor = useAnimatedStyle(() => {
90
- const backgroundColor = interpolateColor(
91
- bgFadeProgress.value,
92
- [0, 1],
93
- ['transparent', pressedColor]
94
- )
95
- return {
96
- backgroundColor,
97
- }
98
- })
99
-
100
- const handlePressIn = () => {
101
- bgFadeProgress.value = withTiming(1, { duration: 300, easing: Easing.inOut(Easing.ease) })
102
- }
103
-
104
- const handlePressOut = () => {
105
- bgFadeProgress.value = withTiming(0, { duration: 300, easing: Easing.inOut(Easing.ease) })
106
- }
107
-
108
84
  const handleMessageLongPress = () => {
109
85
  if (!isPersisted) return
110
86
 
@@ -149,8 +125,8 @@ export function Message({
149
125
  <Pressable
150
126
  onLongPress={handleMessageLongPress}
151
127
  onPress={() => setShowMessageMetaToggle(!showMessageMetaToggle)}
152
- onPressIn={handlePressIn}
153
- onPressOut={handlePressOut}
128
+ onPressIn={handleMessagePressIn}
129
+ onPressOut={handleMessagePressOut}
154
130
  android_ripple={{ color: colors.androidRippleNeutral }}
155
131
  accessibilityHint="Long press to view message actions like reacting and copying."
156
132
  >
@@ -0,0 +1,266 @@
1
+ import React from 'react'
2
+ import { Pressable, StyleSheet, useWindowDimensions, View } from 'react-native'
3
+ import { Avatar, Icon, IconProps, Image, Text } from '../display'
4
+ import {
5
+ useAnimatedMessageBackgroundColor,
6
+ useFontScale,
7
+ useScalableNumberOfLines,
8
+ useTheme,
9
+ } from '../../hooks'
10
+ import { MessageResource } from '../../types'
11
+ import {
12
+ DenormalizedAttachmentResource,
13
+ DenormalizedGiphyAttachmentResource,
14
+ DenormalizedExpandedLinkAttachmentResource,
15
+ DenormalizedMessageAttachmentResource,
16
+ } from '../../types/resources/denormalized_attachment_resource'
17
+ import {
18
+ CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
19
+ MAX_FONT_SIZE_MULTIPLIER,
20
+ MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
21
+ platformFontWeightMedium,
22
+ } from '../../utils/styles'
23
+ import Animated from 'react-native-reanimated'
24
+ import { TheirReplyConnector, MyReplyConnector } from './reply_connectors'
25
+ import { assertKeysAreNumbers } from '../../utils'
26
+
27
+ interface ShadowMessageProps extends MessageResource {}
28
+
29
+ export function ShadowMessage({ ...message }: ShadowMessageProps) {
30
+ const { text } = message
31
+ const styles = useStyles(message)
32
+ const { colors } = useTheme()
33
+ const [messageBubbleHeight, setMessageBubbleHeight] = React.useState(0)
34
+ const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
35
+ useAnimatedMessageBackgroundColor()
36
+ const scalableNumberOfLines = useScalableNumberOfLines(2)
37
+
38
+ const replyCount = 0 // TODO: Get reply count from message object
39
+
40
+ const handleNavigateToReplies = () => {
41
+ console.log('Navigate to replies') // TODO: Implement navigate to a reply screen
42
+ }
43
+
44
+ return (
45
+ <Pressable
46
+ android_ripple={{ color: colors.androidRippleNeutral }}
47
+ onPress={handleNavigateToReplies}
48
+ onPressIn={handleMessagePressIn}
49
+ onPressOut={handleMessagePressOut}
50
+ accessibilityHint="Navigate to all replies for this message."
51
+ >
52
+ <Animated.View style={[styles.message, animatedBackgroundColor]}>
53
+ {!message.mine && (
54
+ <View>
55
+ <View style={styles.avatarWrapper}>
56
+ <Avatar
57
+ size="xs"
58
+ sourceUri={message.author.avatar}
59
+ style={styles.avatar}
60
+ maxFontSizeMultiplier={1}
61
+ />
62
+ </View>
63
+ <TheirReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
64
+ </View>
65
+ )}
66
+ <View style={styles.messageContent}>
67
+ <View
68
+ style={styles.messageBubble}
69
+ onLayout={e => setMessageBubbleHeight(e.nativeEvent.layout.height)}
70
+ >
71
+ <MessageAttachmentImagery attachments={message.attachments} />
72
+ {text && (
73
+ <Text
74
+ variant="footnote"
75
+ style={styles.messageText}
76
+ numberOfLines={scalableNumberOfLines}
77
+ >
78
+ {text}
79
+ </Text>
80
+ )}
81
+ </View>
82
+ <View style={styles.messageMeta}>
83
+ <Text variant="footnote" style={styles.replyCountText}>
84
+ {replyCount} replies
85
+ </Text>
86
+ </View>
87
+ </View>
88
+ {message.mine && (
89
+ <MyReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
90
+ )}
91
+ </Animated.View>
92
+ </Pressable>
93
+ )
94
+ }
95
+
96
+ function MessageAttachmentImagery({
97
+ attachments,
98
+ }: {
99
+ attachments: DenormalizedAttachmentResource[]
100
+ }) {
101
+ if (!attachments || attachments.length === 0) return null
102
+
103
+ const attachment = attachments[0]
104
+
105
+ if (attachment.type === 'giphy') {
106
+ return <GiphyImage attachment={attachment} />
107
+ }
108
+ if (attachment.type === 'ExpandedLink') {
109
+ return <ExpandedLinkImage attachment={attachment} />
110
+ }
111
+ if (attachment.type === 'MessageAttachment') {
112
+ const contentType = attachment.attributes?.contentType
113
+ const basicType = contentType?.split('/')[0]
114
+
115
+ switch (basicType) {
116
+ case 'image':
117
+ return <MessageAttachmentImage attachment={attachment} />
118
+ case 'video':
119
+ return <MessageAttachmentIcon iconName="general.outlinedVideoFile" />
120
+ case 'audio':
121
+ return <MessageAttachmentIcon iconName="general.outlinedMusicFile" />
122
+ case 'application':
123
+ return <MessageAttachmentIcon iconName="general.outlinedGenericFile" />
124
+ default:
125
+ return null
126
+ }
127
+ }
128
+
129
+ return null
130
+ }
131
+
132
+ function GiphyImage({ attachment }: { attachment: DenormalizedGiphyAttachmentResource }) {
133
+ const { title, giphy } = attachment
134
+ const { url } = giphy.fixedWidth
135
+ const { width, height } = assertKeysAreNumbers(giphy.fixedWidth)
136
+ const styles = useStyles({ imageWidth: width, imageHeight: height })
137
+
138
+ return (
139
+ <Image
140
+ source={{ uri: url }}
141
+ wrapperStyle={styles.imageWrapper}
142
+ style={styles.image}
143
+ alt={title}
144
+ loaderSize={16}
145
+ />
146
+ )
147
+ }
148
+
149
+ function ExpandedLinkImage({
150
+ attachment,
151
+ }: {
152
+ attachment: DenormalizedExpandedLinkAttachmentResource
153
+ }) {
154
+ const { title = '', imageUrl, imageHeight, imageWidth } = attachment.attributes
155
+ const styles = useStyles({ imageWidth, imageHeight })
156
+
157
+ return (
158
+ <Image
159
+ source={{ uri: imageUrl }}
160
+ wrapperStyle={styles.imageWrapper}
161
+ style={styles.image}
162
+ alt={title}
163
+ loaderSize={16}
164
+ />
165
+ )
166
+ }
167
+
168
+ function MessageAttachmentImage({
169
+ attachment,
170
+ }: {
171
+ attachment: DenormalizedMessageAttachmentResource
172
+ }) {
173
+ const { url, urlMedium, filename, metadata = {} } = attachment.attributes
174
+ const styles = useStyles({ imageWidth: metadata.width, imageHeight: metadata.height })
175
+
176
+ return (
177
+ <Image
178
+ source={{ uri: urlMedium || url }}
179
+ style={styles.image}
180
+ wrapperStyle={styles.imageWrapper}
181
+ alt={filename}
182
+ loaderSize={16}
183
+ />
184
+ )
185
+ }
186
+
187
+ function MessageAttachmentIcon({ iconName }: { iconName: IconProps['name'] }) {
188
+ const styles = useStyles()
189
+ return (
190
+ <Icon
191
+ name={iconName}
192
+ style={styles.attachmentIcon}
193
+ maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER}
194
+ />
195
+ )
196
+ }
197
+
198
+ interface StylesProps {
199
+ imageWidth?: number
200
+ imageHeight?: number
201
+ mine?: boolean
202
+ }
203
+
204
+ const useStyles = ({ mine, imageWidth = 32, imageHeight = 32 }: StylesProps = {}) => {
205
+ const { colors } = useTheme()
206
+ const fontScale = useFontScale({ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER })
207
+ const { width } = useWindowDimensions()
208
+ const tabletWidth = width >= 744 // Smallest iPad Mini's width
209
+
210
+ return StyleSheet.create({
211
+ message: {
212
+ gap: 8,
213
+ flexDirection: mine ? 'row-reverse' : 'row',
214
+ paddingHorizontal: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
215
+ },
216
+ messageContent: {
217
+ flex: 1,
218
+ gap: 4,
219
+ marginBottom: 12,
220
+ },
221
+ avatarWrapper: {
222
+ width: MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
223
+ alignItems: 'center',
224
+ },
225
+ avatar: {
226
+ marginBottom: 8,
227
+ opacity: 0.5,
228
+ },
229
+ messageBubble: {
230
+ flexDirection: 'row',
231
+ alignSelf: mine ? 'flex-end' : 'flex-start',
232
+ alignItems: 'center',
233
+ gap: 8,
234
+ borderColor: colors.borderColorDefaultBase,
235
+ borderWidth: 1,
236
+ borderRadius: 8,
237
+ maxWidth: tabletWidth ? 360 : '80%',
238
+ paddingVertical: 6,
239
+ paddingHorizontal: 8,
240
+ },
241
+ messageText: {
242
+ color: colors.textColorDefaultPlaceholder,
243
+ flexShrink: 1,
244
+ },
245
+ messageMeta: {
246
+ flexDirection: 'row',
247
+ justifyContent: mine ? 'flex-end' : 'flex-start',
248
+ },
249
+ replyCountText: {
250
+ color: colors.interaction,
251
+ fontWeight: platformFontWeightMedium,
252
+ },
253
+ imageWrapper: {
254
+ width: 32 * fontScale,
255
+ aspectRatio: imageWidth / imageHeight,
256
+ opacity: 0.5,
257
+ },
258
+ image: {
259
+ borderRadius: 4,
260
+ },
261
+ attachmentIcon: {
262
+ color: colors.iconColorDefaultDim,
263
+ fontSize: 16,
264
+ },
265
+ })
266
+ }
@@ -45,6 +45,7 @@ export type {
45
45
  // =================================
46
46
 
47
47
  const AVATAR_SIZES = {
48
+ xs: 'xs',
48
49
  sm: 'sm',
49
50
  md: 'md',
50
51
  lg: 'lg',
@@ -60,18 +61,21 @@ type AvatarSize = (typeof AVATAR_SIZES)[keyof typeof AVATAR_SIZES]
60
61
  type AvatarPresenceType = (typeof AVATAR_PRESENCE_TYPES)[keyof typeof AVATAR_PRESENCE_TYPES]
61
62
 
62
63
  const AVATAR_PX: Record<AvatarSize, number> = {
64
+ [AVATAR_SIZES.xs]: 20,
63
65
  [AVATAR_SIZES.sm]: 24,
64
66
  [AVATAR_SIZES.md]: 32,
65
67
  [AVATAR_SIZES.lg]: 40,
66
68
  }
67
69
 
68
70
  const AVATAR_PRESENCE_PX: Record<AvatarSize, number> = {
71
+ [AVATAR_SIZES.xs]: 8,
69
72
  [AVATAR_SIZES.sm]: 10,
70
73
  [AVATAR_SIZES.md]: 12,
71
74
  [AVATAR_SIZES.lg]: 14,
72
75
  }
73
76
 
74
77
  const AVATAR_FALLBACK_ICON_PX: Record<AvatarSize, number> = {
78
+ [AVATAR_SIZES.xs]: 10,
75
79
  [AVATAR_SIZES.sm]: 12,
76
80
  [AVATAR_SIZES.md]: 16,
77
81
  [AVATAR_SIZES.lg]: 20,
@@ -1,4 +1,5 @@
1
1
  export * from './use_async_storage'
2
+ export * from './use_animated_message_background_color'
2
3
  export * from './use_theme'
3
4
  export * from './use_suspense_api'
4
5
  export * from './use_current_person'
@@ -0,0 +1,44 @@
1
+ import { Platform } from 'react-native'
2
+ import {
3
+ useSharedValue,
4
+ useAnimatedStyle,
5
+ withTiming,
6
+ interpolateColor,
7
+ Easing,
8
+ } from 'react-native-reanimated'
9
+ import { useTheme } from '../hooks'
10
+
11
+ export function useAnimatedMessageBackgroundColor() {
12
+ const { colors } = useTheme()
13
+ const bgFadeProgress = useSharedValue(0)
14
+
15
+ const pressedColor = Platform.select({
16
+ ios: colors.fillColorNeutral050Base,
17
+ default: 'transparent',
18
+ })
19
+
20
+ const animatedBackgroundColor = useAnimatedStyle(() => {
21
+ const backgroundColor = interpolateColor(
22
+ bgFadeProgress.value,
23
+ [0, 1],
24
+ ['transparent', pressedColor]
25
+ )
26
+ return {
27
+ backgroundColor,
28
+ }
29
+ })
30
+
31
+ const handleMessagePressIn = () => {
32
+ bgFadeProgress.value = withTiming(1, { duration: 300, easing: Easing.inOut(Easing.ease) })
33
+ }
34
+
35
+ const handleMessagePressOut = () => {
36
+ bgFadeProgress.value = withTiming(0, { duration: 300, easing: Easing.inOut(Easing.ease) })
37
+ }
38
+
39
+ return {
40
+ animatedBackgroundColor,
41
+ handleMessagePressIn,
42
+ handleMessagePressOut,
43
+ }
44
+ }
@@ -5,7 +5,7 @@ export type AppName = 'chat' | 'churchcenter' | 'services'
5
5
  export const useAppName = (): AppName => {
6
6
  const applicationName = DeviceInfo.getApplicationName()
7
7
 
8
- if (/churchcenter/i.test(applicationName)) {
8
+ if (/church\s?center/i.test(applicationName)) {
9
9
  return 'churchcenter'
10
10
  }
11
11
 
@@ -238,8 +238,9 @@ export const ChatStack = createNativeStackNavigator({
238
238
  screen: GetHelpScreen,
239
239
  options: ({ navigation }) => ({
240
240
  headerTitle: 'Get help',
241
+ headerBackVisible: false,
241
242
  presentation: 'modal',
242
- headerLeft: props => (
243
+ headerRight: props => (
243
244
  <HeaderTextButton {...props} onPress={navigation.goBack} title="Close" />
244
245
  ),
245
246
  }),
@@ -73,6 +73,12 @@ enum QualifiedMobileAppName {
73
73
  services = 'Services Mobile',
74
74
  }
75
75
 
76
+ enum DisplayableAppName {
77
+ chat = 'Chat',
78
+ churchcenter = 'Church Center',
79
+ services = 'Services',
80
+ }
81
+
76
82
  export function BugReportScreen() {
77
83
  const styles = useStyles()
78
84
  const navigation = useNavigation()
@@ -174,8 +180,8 @@ export function BugReportScreen() {
174
180
  <BlankState.Content>
175
181
  <BlankState.Heading style={styles.successTitle}>Thank you!</BlankState.Heading>
176
182
  <BlankState.Text style={styles.successSubtitle}>
177
- We appreciate you taking the time to help improve chat! We'll take a look at the issue
178
- you reported.
183
+ We appreciate you taking the time to help improve {DisplayableAppName[name]}! We'll take
184
+ a look at the issue you reported.
179
185
  </BlankState.Text>
180
186
  </BlankState.Content>
181
187
  <BlankState.Button
@@ -213,8 +219,9 @@ export function BugReportScreen() {
213
219
  <KeyboardView>
214
220
  <ScrollView contentContainerStyle={styles.container}>
215
221
  <Text style={styles.description}>
216
- Thanks for helping us improve chat. Please provide details about the issue you've
217
- encountered. We read every submission and your feedback helps us improve the experience.
222
+ Thanks for helping us improve {DisplayableAppName[name]}. We won't be able to respond to
223
+ your submission, but your feedback helps us improve the experience helps us improve the
224
+ experience.
218
225
  </Text>
219
226
 
220
227
  <View style={styles.textInputContainer}>
@@ -373,7 +380,7 @@ export function BugReportScreen() {
373
380
  }
374
381
 
375
382
  const VIDEO_RECORDING_HELP_URL = Platform.select({
376
- android: 'https://support.google.com/android/answer/6241341?hl=en',
383
+ android: 'https://support.google.com/android/answer/9075928?hl=en',
377
384
  default: 'https://support.apple.com/en-us/HT208721',
378
385
  })
379
386
 
@@ -1,10 +1,11 @@
1
1
  import { StaticScreenProps, useNavigation } from '@react-navigation/native'
2
2
  import { useCallback, useMemo } from 'react'
3
- import { Linking, StyleSheet, View } from 'react-native'
3
+ import { Alert, Linking, StyleSheet, View } from 'react-native'
4
4
  import { Heading, PressableRow, Text, TextInlineButton } from '../components'
5
5
  import { useApiGet, useTheme } from '../hooks'
6
6
  import { useAppName } from '../hooks/use_app_name'
7
7
  import { ResourceObject } from '../types'
8
+ import { Clipboard } from '../utils'
8
9
 
9
10
  type GetHelpScreenRouteProps = {
10
11
  type?: 'chat' | 'general'
@@ -121,13 +122,18 @@ export const GetHelpScreen = ({ route }: GetHelpScreenProps) => {
121
122
  }
122
123
 
123
124
  const ContactRow = ({ email, phone }: { email?: string; phone?: string }) => {
124
- if (!email && !phone) return null
125
-
126
125
  const contact = email || phone
126
+ if (!contact) return null
127
127
 
128
128
  return (
129
129
  <PressableRow
130
- onPress={() => {}}
130
+ onPress={() => {
131
+ if (email) {
132
+ sendEmail(contact)
133
+ } else {
134
+ placePhoneCall(contact)
135
+ }
136
+ }}
131
137
  text={contact || ''}
132
138
  isActive={true}
133
139
  iconPath={email ? 'services.email' : 'general.phone'}
@@ -135,6 +141,32 @@ const ContactRow = ({ email, phone }: { email?: string; phone?: string }) => {
135
141
  )
136
142
  }
137
143
 
144
+ export function sendEmail(email: string) {
145
+ if (!email) return
146
+
147
+ const url = encodeURI(`mailto:${email}`)
148
+
149
+ Linking.openURL(url).catch(() => {
150
+ Alert.alert('Oops', `Unable to open: ${email}`, [
151
+ { text: 'Copy to clipboard', onPress: () => Clipboard.setStringAsync(email) },
152
+ { text: 'OK' },
153
+ ])
154
+ })
155
+ }
156
+
157
+ export function placePhoneCall(phoneNumber: string) {
158
+ if (!phoneNumber) return
159
+
160
+ const url = encodeURI(`tel:${phoneNumber}`)
161
+
162
+ Linking.openURL(url).catch(() => {
163
+ Alert.alert('Oops', `Unable to open: ${phoneNumber}`, [
164
+ { text: 'Copy to clipboard', onPress: () => Clipboard.setStringAsync(phoneNumber) },
165
+ { text: 'OK' },
166
+ ])
167
+ })
168
+ }
169
+
138
170
  const useStyles = () => {
139
171
  const { colors } = useTheme()
140
172
 
@@ -0,0 +1,13 @@
1
+ export const assertKeysAreNumbers = (obj: Object): Record<any, number> => {
2
+ return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, assertNumber(value)]))
3
+ }
4
+
5
+ const assertNumber = (value: string): number => {
6
+ if (typeof value === 'number') return value
7
+
8
+ if (typeof value === 'string' && !isNaN(Number(value))) {
9
+ return Number(value)
10
+ }
11
+
12
+ return 0
13
+ }
@@ -8,3 +8,4 @@ export * from './native_adapters'
8
8
  export * from './pluralize'
9
9
  export * from './destructure_chat_group_graph_id'
10
10
  export * from './convert_attachments_for_create'
11
+ export * from './assert_keys_are_numbers'