@planningcenter/chat-react-native 3.24.0-rc.0 → 3.24.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.
Files changed (142) hide show
  1. package/build/components/conversations/conversation_actions.d.ts.map +1 -1
  2. package/build/components/conversations/conversation_actions.js +5 -1
  3. package/build/components/conversations/conversation_actions.js.map +1 -1
  4. package/build/components/conversations/conversations.d.ts.map +1 -1
  5. package/build/components/conversations/conversations.js +5 -1
  6. package/build/components/conversations/conversations.js.map +1 -1
  7. package/build/components/display/platform_modal_header_buttons.d.ts +8 -0
  8. package/build/components/display/platform_modal_header_buttons.d.ts.map +1 -1
  9. package/build/components/display/platform_modal_header_buttons.js +8 -0
  10. package/build/components/display/platform_modal_header_buttons.js.map +1 -1
  11. package/build/components/primitive/form_sheet.d.ts +1 -1
  12. package/build/components/primitive/form_sheet.d.ts.map +1 -1
  13. package/build/components/primitive/form_sheet.js +1 -1
  14. package/build/components/primitive/form_sheet.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_features.d.ts +1 -0
  20. package/build/hooks/use_features.d.ts.map +1 -1
  21. package/build/hooks/use_features.js +1 -0
  22. package/build/hooks/use_features.js.map +1 -1
  23. package/build/hooks/use_new_conversation_entry.d.ts +3 -0
  24. package/build/hooks/use_new_conversation_entry.d.ts.map +1 -0
  25. package/build/hooks/use_new_conversation_entry.js +20 -0
  26. package/build/hooks/use_new_conversation_entry.js.map +1 -0
  27. package/build/hooks/use_report_message.d.ts +6 -0
  28. package/build/hooks/use_report_message.d.ts.map +1 -0
  29. package/build/hooks/use_report_message.js +28 -0
  30. package/build/hooks/use_report_message.js.map +1 -0
  31. package/build/hooks/use_teams.d.ts.map +1 -1
  32. package/build/hooks/use_teams.js +1 -0
  33. package/build/hooks/use_teams.js.map +1 -1
  34. package/build/navigation/index.d.ts +52 -10
  35. package/build/navigation/index.d.ts.map +1 -1
  36. package/build/navigation/index.js +41 -7
  37. package/build/navigation/index.js.map +1 -1
  38. package/build/screens/conversation_filter_recipients/conversation_filter_recipients_screen.js +5 -3
  39. package/build/screens/conversation_filter_recipients/conversation_filter_recipients_screen.js.map +1 -1
  40. package/build/screens/conversation_select_recipients/conversation_new_entry_screen.d.ts +4 -0
  41. package/build/screens/conversation_select_recipients/conversation_new_entry_screen.d.ts.map +1 -0
  42. package/build/screens/conversation_select_recipients/conversation_new_entry_screen.js +67 -0
  43. package/build/screens/conversation_select_recipients/conversation_new_entry_screen.js.map +1 -0
  44. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.d.ts.map +1 -1
  45. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js +17 -2
  46. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js.map +1 -1
  47. package/build/screens/conversation_select_type_screen.d.ts +11 -0
  48. package/build/screens/conversation_select_type_screen.d.ts.map +1 -0
  49. package/build/screens/conversation_select_type_screen.js +38 -0
  50. package/build/screens/conversation_select_type_screen.js.map +1 -0
  51. package/build/screens/conversations/components/list_header_component.d.ts.map +1 -1
  52. package/build/screens/conversations/components/list_header_component.js +18 -7
  53. package/build/screens/conversations/components/list_header_component.js.map +1 -1
  54. package/build/screens/group_notification_settings_screen.d.ts +8 -0
  55. package/build/screens/group_notification_settings_screen.d.ts.map +1 -0
  56. package/build/screens/group_notification_settings_screen.js +79 -0
  57. package/build/screens/group_notification_settings_screen.js.map +1 -0
  58. package/build/screens/index.d.ts +4 -1
  59. package/build/screens/index.d.ts.map +1 -1
  60. package/build/screens/index.js +4 -1
  61. package/build/screens/index.js.map +1 -1
  62. package/build/screens/message_actions_screen.js +14 -2
  63. package/build/screens/message_actions_screen.js.map +1 -1
  64. package/build/screens/message_report/components/message_preview.d.ts +10 -0
  65. package/build/screens/message_report/components/message_preview.d.ts.map +1 -0
  66. package/build/screens/message_report/components/message_preview.js +74 -0
  67. package/build/screens/message_report/components/message_preview.js.map +1 -0
  68. package/build/screens/message_report/components/report_reason_list.d.ts +9 -0
  69. package/build/screens/message_report/components/report_reason_list.d.ts.map +1 -0
  70. package/build/screens/message_report/components/report_reason_list.js +67 -0
  71. package/build/screens/message_report/components/report_reason_list.js.map +1 -0
  72. package/build/screens/message_report_screen.d.ts +10 -0
  73. package/build/screens/message_report_screen.d.ts.map +1 -0
  74. package/build/screens/message_report_screen.js +214 -0
  75. package/build/screens/message_report_screen.js.map +1 -0
  76. package/build/screens/notification_settings/hooks/groups.d.ts +94 -0
  77. package/build/screens/notification_settings/hooks/groups.d.ts.map +1 -0
  78. package/build/screens/notification_settings/hooks/groups.js +92 -0
  79. package/build/screens/notification_settings/hooks/groups.js.map +1 -0
  80. package/build/screens/notification_settings_screen.d.ts +5 -0
  81. package/build/screens/notification_settings_screen.d.ts.map +1 -0
  82. package/build/screens/notification_settings_screen.js +234 -0
  83. package/build/screens/notification_settings_screen.js.map +1 -0
  84. package/build/screens/preferred_app/hooks/use_chat_types.d.ts +39 -0
  85. package/build/screens/preferred_app/hooks/use_chat_types.d.ts.map +1 -0
  86. package/build/screens/preferred_app/hooks/use_chat_types.js +12 -0
  87. package/build/screens/preferred_app/hooks/use_chat_types.js.map +1 -0
  88. package/build/screens/preferred_app_selection_screen.d.ts +10 -0
  89. package/build/screens/preferred_app_selection_screen.d.ts.map +1 -0
  90. package/build/screens/preferred_app_selection_screen.js +128 -0
  91. package/build/screens/preferred_app_selection_screen.js.map +1 -0
  92. package/build/types/resources/group_membership.d.ts +6 -0
  93. package/build/types/resources/group_membership.d.ts.map +1 -0
  94. package/build/types/resources/group_membership.js +2 -0
  95. package/build/types/resources/group_membership.js.map +1 -0
  96. package/build/types/resources/group_resource.d.ts +4 -0
  97. package/build/types/resources/group_resource.d.ts.map +1 -1
  98. package/build/types/resources/group_resource.js.map +1 -1
  99. package/build/types/resources/index.d.ts +2 -0
  100. package/build/types/resources/index.d.ts.map +1 -1
  101. package/build/types/resources/index.js +2 -0
  102. package/build/types/resources/index.js.map +1 -1
  103. package/build/types/resources/message_report.d.ts +19 -0
  104. package/build/types/resources/message_report.d.ts.map +1 -0
  105. package/build/types/resources/message_report.js +9 -0
  106. package/build/types/resources/message_report.js.map +1 -0
  107. package/package.json +2 -2
  108. package/src/components/conversations/conversation_actions.tsx +6 -1
  109. package/src/components/conversations/conversations.tsx +6 -1
  110. package/src/components/display/platform_modal_header_buttons.tsx +16 -0
  111. package/src/components/primitive/form_sheet.tsx +4 -2
  112. package/src/hooks/index.ts +1 -0
  113. package/src/hooks/use_features.ts +1 -0
  114. package/src/hooks/use_new_conversation_entry.ts +31 -0
  115. package/src/hooks/use_report_message.ts +37 -0
  116. package/src/hooks/use_teams.ts +1 -0
  117. package/src/navigation/index.tsx +51 -7
  118. package/src/screens/conversation_filter_recipients/conversation_filter_recipients_screen.tsx +10 -3
  119. package/src/screens/conversation_select_recipients/conversation_new_entry_screen.tsx +100 -0
  120. package/src/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.tsx +23 -3
  121. package/src/screens/conversation_select_type_screen.tsx +59 -0
  122. package/src/screens/conversations/components/list_header_component.tsx +20 -7
  123. package/src/screens/group_notification_settings_screen.tsx +92 -0
  124. package/src/screens/index.ts +4 -1
  125. package/src/screens/message_actions_screen.tsx +24 -2
  126. package/src/screens/message_report/components/message_preview.tsx +92 -0
  127. package/src/screens/message_report/components/report_reason_list.tsx +106 -0
  128. package/src/screens/message_report_screen.tsx +278 -0
  129. package/src/screens/notification_settings/hooks/groups.ts +101 -0
  130. package/src/screens/notification_settings_screen.tsx +383 -0
  131. package/src/screens/preferred_app/hooks/use_chat_types.ts +25 -0
  132. package/src/screens/preferred_app_selection_screen.tsx +169 -0
  133. package/src/types/images.d.ts +14 -0
  134. package/src/types/resources/group_membership.ts +6 -0
  135. package/src/types/resources/group_resource.ts +5 -0
  136. package/src/types/resources/index.ts +2 -0
  137. package/src/types/resources/message_report.ts +20 -0
  138. package/build/screens/conversation_select_recipients/conversation_select_recipients_screen.d.ts +0 -4
  139. package/build/screens/conversation_select_recipients/conversation_select_recipients_screen.d.ts.map +0 -1
  140. package/build/screens/conversation_select_recipients/conversation_select_recipients_screen.js +0 -146
  141. package/build/screens/conversation_select_recipients/conversation_select_recipients_screen.js.map +0 -1
  142. package/src/screens/conversation_select_recipients/conversation_select_recipients_screen.tsx +0 -215
@@ -0,0 +1,92 @@
1
+ import { StaticScreenProps, useNavigation } from '@react-navigation/native'
2
+ import React, { useEffect } from 'react'
3
+ import { StyleSheet, View } from 'react-native'
4
+ import { Heading, Switch, Text } from '../components'
5
+ import { useTheme } from '../hooks'
6
+ import { platformFontWeightBold } from '../utils/styles'
7
+ import { useGroup, useGroupMembershipUpdate } from './notification_settings/hooks/groups'
8
+
9
+ export type GroupNotificationSettingsScreenProps = StaticScreenProps<{
10
+ groupId: number | string
11
+ title: string
12
+ }>
13
+
14
+ export function GroupNotificationSettingsScreen({ route }: GroupNotificationSettingsScreenProps) {
15
+ const { groupId, title } = route.params
16
+ const navigation = useNavigation()
17
+ const styles = useStyles()
18
+ const { data: group } = useGroup({ groupId })
19
+ const { mutate: updateNotificationLevel } = useGroupMembershipUpdate({ groupId })
20
+
21
+ const notificationsEnabled = group.myGroupMembership?.notificationLevel === 'everything'
22
+
23
+ useEffect(() => {
24
+ if (!group.name || title === group.name) return
25
+
26
+ navigation.setOptions({ title: group.name })
27
+ }, [group.name, title, navigation])
28
+
29
+ const handleToggle = (value: boolean) => {
30
+ const notificationLevel = value ? 'everything' : 'nothing'
31
+ updateNotificationLevel(notificationLevel)
32
+ }
33
+
34
+ return (
35
+ <View style={styles.container}>
36
+ <View style={styles.sectionOuter}>
37
+ <View style={styles.sectionInner}>
38
+ <Heading variant="h3" style={styles.sectionHeading}>
39
+ Group notification settings
40
+ </Heading>
41
+ <Text variant="tertiary" style={styles.sectionSubtitle}>
42
+ The settings are applied to all conversations in{' '}
43
+ <Text style={styles.groupNameBold}>{group.name || title}</Text>
44
+ </Text>
45
+ </View>
46
+ </View>
47
+ <View style={styles.settingRow}>
48
+ <Text>Enable notifications</Text>
49
+ <Switch value={notificationsEnabled} onValueChange={handleToggle} />
50
+ </View>
51
+ </View>
52
+ )
53
+ }
54
+
55
+ const useStyles = () => {
56
+ const { colors } = useTheme()
57
+
58
+ return StyleSheet.create({
59
+ container: {
60
+ flex: 1,
61
+ backgroundColor: colors.surfaceColor100,
62
+ },
63
+ sectionOuter: {
64
+ paddingLeft: 16,
65
+ backgroundColor: colors.surfaceColor100,
66
+ },
67
+ sectionInner: {
68
+ paddingRight: 16,
69
+ paddingTop: 16,
70
+ paddingBottom: 12,
71
+ borderBottomWidth: 1,
72
+ borderBottomColor: colors.borderColorDefaultBase,
73
+ },
74
+ sectionHeading: {
75
+ paddingBottom: 4,
76
+ },
77
+ sectionSubtitle: {
78
+ color: colors.textColorDefaultSecondary,
79
+ },
80
+ groupNameBold: {
81
+ fontWeight: platformFontWeightBold,
82
+ },
83
+ settingRow: {
84
+ flexDirection: 'row',
85
+ justifyContent: 'space-between',
86
+ alignItems: 'center',
87
+ paddingHorizontal: 16,
88
+ paddingVertical: 12,
89
+ backgroundColor: colors.surfaceColor100,
90
+ },
91
+ })
92
+ }
@@ -2,9 +2,12 @@ export * from './design_system_screen'
2
2
  export * from './conversation_details_screen'
3
3
  export * from './conversation_screen'
4
4
  export * from './conversation_new/conversation_new_screen'
5
+ export * from './notification_settings_screen'
6
+ export * from './preferred_app_selection_screen'
7
+ export * from './group_notification_settings_screen'
5
8
  export * from './conversation_filter_recipients/conversation_filter_recipients_screen'
6
- export * from './conversation_select_recipients/conversation_select_recipients_screen'
7
9
  export * from './message_actions_screen'
10
+ export * from './message_report_screen'
8
11
  export * from './send_giphy_screen'
9
12
  export * from './not_found'
10
13
  export * from './reactions_screen'
@@ -120,6 +120,14 @@ function MessageActionsScreenContent({
120
120
  navigation.goBack()
121
121
  }
122
122
 
123
+ const handleReportPress = useCallback(() => {
124
+ Haptic.impactLight()
125
+ navigation.navigate('MessageReport', {
126
+ conversation_id,
127
+ message_id: message.id,
128
+ })
129
+ }, [navigation, conversation_id, message.id])
130
+
123
131
  const { handleReactionToggle, isPending } = useMessageReactionToggle({
124
132
  conversation_id,
125
133
  message,
@@ -212,6 +220,9 @@ function MessageActionsScreenContent({
212
220
  ])
213
221
  }, [handleRemoveLinkPreview])
214
222
 
223
+ const showReportMessageAction =
224
+ !message?.mine && featureEnabled(availableFeatures.message_reporting)
225
+
215
226
  return (
216
227
  <FormSheet.Root style={styles.formSheetContent}>
217
228
  <View style={styles.reactionList}>
@@ -246,6 +257,14 @@ function MessageActionsScreenContent({
246
257
  iconName="services.fileCopy"
247
258
  accessibilityHint="Copies text and links to clipboard"
248
259
  />
260
+ {showReportMessageAction && (
261
+ <FormSheet.Action
262
+ onPress={handleReportPress}
263
+ title="Report message"
264
+ iconName="chat.reportMessageO"
265
+ accessibilityHint="Opens a form to report this message"
266
+ />
267
+ )}
249
268
  {message?.mine && (
250
269
  <FormSheet.Action
251
270
  onPress={() => handleEditPress()}
@@ -306,7 +325,9 @@ const Reaction = ({
306
325
  android_ripple={{ color: androidRippleColor, borderless: false, foreground: true }}
307
326
  onPress={onPress}
308
327
  >
309
- <Text style={styles.reactionEmoji}>{REACTION_EMOJIS[reaction.value]}</Text>
328
+ <Text style={styles.reactionEmoji} allowFontScaling={false}>
329
+ {REACTION_EMOJIS[reaction.value]}
330
+ </Text>
310
331
  </PlatformPressable>
311
332
  )
312
333
  }
@@ -316,7 +337,7 @@ const useStyles = () => {
316
337
  const fontScale = useFontScale({ maxFontSizeMultiplier: 1.3 })
317
338
 
318
339
  const btnBorderWidth = 1
319
- const baseSize = 46 * fontScale
340
+ const baseSize = 46 * Math.max(1, fontScale)
320
341
  const reactionBtnSize = Platform.select({
321
342
  ios: baseSize,
322
343
  android: baseSize + btnBorderWidth * 2,
@@ -346,6 +367,7 @@ const useStyles = () => {
346
367
  },
347
368
  reactionEmoji: {
348
369
  fontSize: 24,
370
+ textAlign: 'center',
349
371
  },
350
372
  actions: {
351
373
  paddingTop: 4,
@@ -0,0 +1,92 @@
1
+ import React from 'react'
2
+ import { View, StyleSheet, ViewStyle } from 'react-native'
3
+ import { Avatar, Text, Icon } from '../../../components'
4
+ import { useTheme } from '../../../hooks'
5
+ import { MessageResource } from '../../../types'
6
+
7
+ interface MessagePreviewProps {
8
+ message: MessageResource
9
+ style?: ViewStyle
10
+ }
11
+
12
+ export function MessagePreview({ message, style }: MessagePreviewProps) {
13
+ const hasImages = message.attachments.some(
14
+ att => att.type === 'MessageAttachment' && att.attributes?.contentType?.startsWith('image/')
15
+ )
16
+
17
+ const imageOnly = hasImages && !message.text
18
+ const styles = useStyles({ imageOnly })
19
+
20
+ return (
21
+ <View style={[styles.previewContainer, style]}>
22
+ <View style={styles.previewHeader}>
23
+ <Avatar sourceUri={message.author.avatar} size="md" />
24
+ <Text style={styles.authorName}>{message.author.name}</Text>
25
+ </View>
26
+
27
+ <View style={styles.messageBubble}>
28
+ {hasImages && (
29
+ <View style={styles.imagePlaceholder}>
30
+ <Icon name="general.image" style={styles.placeholderIcon} />
31
+ <Text variant="secondary" style={styles.placeholderText}>
32
+ Image hidden
33
+ </Text>
34
+ </View>
35
+ )}
36
+
37
+ {message.text && (
38
+ <Text style={styles.messageText} numberOfLines={5}>
39
+ {message.text}
40
+ </Text>
41
+ )}
42
+ </View>
43
+ </View>
44
+ )
45
+ }
46
+
47
+ const useStyles = ({ imageOnly }: { imageOnly: boolean }) => {
48
+ const { colors } = useTheme()
49
+
50
+ return StyleSheet.create({
51
+ previewContainer: {
52
+ gap: 12,
53
+ },
54
+ previewHeader: {
55
+ flexDirection: 'row',
56
+ alignItems: 'center',
57
+ gap: 12,
58
+ },
59
+ authorName: {
60
+ fontSize: 14,
61
+ fontWeight: '600',
62
+ color: colors.textColorDefaultPrimary,
63
+ },
64
+ messageBubble: {
65
+ backgroundColor: colors.fillColorNeutral060,
66
+ borderRadius: 16,
67
+ overflow: 'hidden',
68
+ alignSelf: 'flex-start',
69
+ maxWidth: imageOnly ? '40%' : '80%',
70
+ },
71
+ messageText: {
72
+ fontSize: 14,
73
+ color: colors.textColorDefaultPrimary,
74
+ lineHeight: 20,
75
+ paddingHorizontal: 16,
76
+ paddingVertical: 12,
77
+ },
78
+ imagePlaceholder: {
79
+ height: 120,
80
+ justifyContent: 'center',
81
+ alignItems: 'center',
82
+ gap: 8,
83
+ },
84
+ placeholderIcon: {
85
+ fontSize: 32,
86
+ color: colors.iconColorDefaultSecondary,
87
+ },
88
+ placeholderText: {
89
+ fontSize: 14,
90
+ },
91
+ })
92
+ }
@@ -0,0 +1,106 @@
1
+ import React from 'react'
2
+ import { View, StyleSheet } from 'react-native'
3
+ import { PlatformPressable } from '@react-navigation/elements'
4
+ import { Text } from '../../../components'
5
+ import { useTheme, useCreateAndroidRippleColor } from '../../../hooks'
6
+ import { Haptic } from '../../../utils/native_adapters'
7
+ import {
8
+ MESSAGE_REPORT_REASONS,
9
+ MessageReportReason,
10
+ } from '../../../types/resources/message_report'
11
+
12
+ const MESSAGE_REPORT_REASONS_LIST = Object.entries(MESSAGE_REPORT_REASONS).map(([key, label]) => ({
13
+ key: key as MessageReportReason,
14
+ label,
15
+ }))
16
+
17
+ interface ReportReasonListProps {
18
+ selectedReason: MessageReportReason | null
19
+ onReasonChange: (reason: MessageReportReason) => void
20
+ }
21
+
22
+ export function ReportReasonList({ selectedReason, onReasonChange }: ReportReasonListProps) {
23
+ return (
24
+ <View>
25
+ {MESSAGE_REPORT_REASONS_LIST.map(({ key, label }) => (
26
+ <ReportReasonButton
27
+ key={key}
28
+ reason={key}
29
+ label={label}
30
+ isSelected={selectedReason === key}
31
+ onPress={() => {
32
+ Haptic.impactLight()
33
+ onReasonChange(key)
34
+ }}
35
+ />
36
+ ))}
37
+ </View>
38
+ )
39
+ }
40
+
41
+ interface ReportReasonButtonProps {
42
+ reason: MessageReportReason
43
+ label: string
44
+ isSelected: boolean
45
+ onPress: () => void
46
+ }
47
+
48
+ function ReportReasonButton({ label, isSelected, onPress }: ReportReasonButtonProps) {
49
+ const styles = useStyles({ isSelected })
50
+ const { colors } = useTheme()
51
+ const androidRippleColor = useCreateAndroidRippleColor({ color: colors.fillColorNeutral060 })
52
+
53
+ return (
54
+ <PlatformPressable
55
+ onPress={onPress}
56
+ style={styles.pressable}
57
+ android_ripple={{ color: androidRippleColor }}
58
+ accessibilityRole="radio"
59
+ accessibilityState={{ selected: isSelected }}
60
+ accessibilityLabel={label}
61
+ >
62
+ <View style={styles.row}>
63
+ <View style={styles.radioButton}>
64
+ {isSelected && <View style={styles.radioButtonInner} />}
65
+ </View>
66
+ <Text style={styles.label}>{label}</Text>
67
+ </View>
68
+ </PlatformPressable>
69
+ )
70
+ }
71
+
72
+ const useStyles = ({ isSelected }: { isSelected: boolean }) => {
73
+ const { colors } = useTheme()
74
+
75
+ return StyleSheet.create({
76
+ pressable: {
77
+ paddingVertical: 16,
78
+ paddingHorizontal: 16,
79
+ },
80
+ row: {
81
+ flexDirection: 'row',
82
+ alignItems: 'center',
83
+ gap: 12,
84
+ },
85
+ radioButton: {
86
+ width: 20,
87
+ height: 20,
88
+ borderRadius: 10,
89
+ borderWidth: 2,
90
+ borderColor: isSelected ? colors.interaction : colors.borderColorDefaultBase,
91
+ justifyContent: 'center',
92
+ alignItems: 'center',
93
+ },
94
+ radioButtonInner: {
95
+ width: 10,
96
+ height: 10,
97
+ borderRadius: 5,
98
+ backgroundColor: colors.interaction,
99
+ },
100
+ label: {
101
+ fontSize: 16,
102
+ color: colors.textColorDefaultPrimary,
103
+ flex: 1,
104
+ },
105
+ })
106
+ }
@@ -0,0 +1,278 @@
1
+ import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react'
2
+ import { View, StyleSheet, TextInput, ScrollView } from 'react-native'
3
+ import type {
4
+ NativeStackNavigationOptions,
5
+ NativeStackScreenProps,
6
+ } from '@react-navigation/native-stack'
7
+ import { StaticScreenProps, useNavigation, StackActions } from '@react-navigation/native'
8
+ import { Spinner, Text, KeyboardView } from '../components'
9
+ import BlankState from '../components/primitive/blank_state_primitive'
10
+ import { useTheme } from '../hooks'
11
+ import { useReportMessage } from '../hooks/use_report_message'
12
+ import {
13
+ HeaderDismissButton,
14
+ HeaderTextButton,
15
+ } from '../components/display/platform_modal_header_buttons'
16
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
17
+ import { useConversationMessages } from '../hooks/use_conversation_messages'
18
+ import { MessageResource } from '../types'
19
+ import { MessageReportReason } from '../types/resources/message_report'
20
+ import { MessagePreview } from './message_report/components/message_preview'
21
+ import { ReportReasonList } from './message_report/components/report_reason_list'
22
+ import { platformFontWeightBold } from '../utils'
23
+
24
+ const MAX_DETAIL_LENGTH = 500
25
+
26
+ export const MessageReportScreenOptions = ({
27
+ navigation,
28
+ }: NativeStackScreenProps<any>): NativeStackNavigationOptions => ({
29
+ presentation: 'modal',
30
+ title: 'Report message',
31
+ headerLeft: () => <HeaderDismissButton title="Cancel" onPress={() => navigation.goBack()} />,
32
+ })
33
+
34
+ export type MessageReportScreenProps = StaticScreenProps<{
35
+ conversation_id: number
36
+ message_id: string
37
+ }>
38
+
39
+ export function MessageReportScreen({ route }: MessageReportScreenProps) {
40
+ const { conversation_id, message_id } = route.params
41
+
42
+ const { messages } = useConversationMessages({ conversation_id }, { refetchOnMount: false })
43
+ const message = messages.find(m => m.id === message_id)
44
+
45
+ if (!message) return null
46
+
47
+ return <MessageReportScreenContent message={message} conversation_id={conversation_id} />
48
+ }
49
+
50
+ function MessageReportScreenContent({
51
+ message,
52
+ conversation_id,
53
+ }: {
54
+ message: MessageResource
55
+ conversation_id: number
56
+ }) {
57
+ const styles = useStyles()
58
+ const navigation = useNavigation()
59
+ const [selectedReason, setSelectedReason] = useState<MessageReportReason | null>(null)
60
+ const [reasonDetail, setReasonDetail] = useState('')
61
+ const { mutate: submitReport, status } = useReportMessage(conversation_id, message.id)
62
+
63
+ const handleReasonChange = useCallback((reason: MessageReportReason) => {
64
+ setSelectedReason(reason)
65
+
66
+ if (reason !== 'other') {
67
+ setReasonDetail('')
68
+ }
69
+ }, [])
70
+
71
+ const isFormValid = useMemo(() => {
72
+ if (selectedReason === null) return false
73
+
74
+ if (selectedReason === 'other' && reasonDetail.trim().length === 0) {
75
+ return false
76
+ }
77
+
78
+ if (status === 'pending') return false
79
+
80
+ return true
81
+ }, [selectedReason, reasonDetail, status])
82
+
83
+ const handleSubmit = useCallback(() => {
84
+ if (!isFormValid || !selectedReason) return
85
+
86
+ submitReport({
87
+ reason: selectedReason,
88
+ reasonDetail: selectedReason === 'other' ? reasonDetail : undefined,
89
+ })
90
+ }, [isFormValid, selectedReason, reasonDetail, submitReport])
91
+
92
+ const handleClose = useCallback(() => {
93
+ navigation.dispatch(StackActions.popTo('Conversation', { conversation_id }))
94
+ }, [navigation, conversation_id])
95
+
96
+ const HeaderRight = useCallback(
97
+ () => <HeaderTextButton title="Submit" onPress={handleSubmit} disabled={!isFormValid} />,
98
+ [handleSubmit, isFormValid]
99
+ )
100
+
101
+ useLayoutEffect(() => {
102
+ if (status === 'pending' || status === 'success') {
103
+ navigation.setOptions({
104
+ headerRight: () => null,
105
+ })
106
+ } else {
107
+ navigation.setOptions({
108
+ headerTitle: 'Report message',
109
+ headerRight: HeaderRight,
110
+ })
111
+ }
112
+ }, [HeaderRight, navigation, status])
113
+
114
+ if (status === 'pending') {
115
+ return (
116
+ <View style={styles.fullScreenContainer}>
117
+ <Spinner />
118
+ <Text style={styles.loadingText}>Submitting report...</Text>
119
+ </View>
120
+ )
121
+ }
122
+
123
+ if (status === 'success') {
124
+ return (
125
+ <View style={styles.fullScreenContainer}>
126
+ <BlankState.Root>
127
+ <BlankState.Imagery name="general.checkCircle" style={styles.successIcon} />
128
+ <BlankState.Content>
129
+ <BlankState.Heading style={styles.successTitle}>Message reported</BlankState.Heading>
130
+ <BlankState.Text style={styles.successSubtitle}>
131
+ The message has been reported and a church leader will be contacted.
132
+ </BlankState.Text>
133
+ </BlankState.Content>
134
+ <BlankState.Button title="Close" onPress={handleClose} size="lg" variant="fill" />
135
+ </BlankState.Root>
136
+ </View>
137
+ )
138
+ }
139
+
140
+ if (status === 'error') {
141
+ return (
142
+ <BlankState.Root>
143
+ <BlankState.Imagery name="general.exclamationTriangle" />
144
+ <BlankState.Content>
145
+ <BlankState.Heading>Unable to submit report</BlankState.Heading>
146
+ <BlankState.Text>
147
+ We were unable to report this message. Please try again.
148
+ </BlankState.Text>
149
+ </BlankState.Content>
150
+ <BlankState.Button title="Close" onPress={handleClose} size="lg" variant="outline" />
151
+ </BlankState.Root>
152
+ )
153
+ }
154
+
155
+ return (
156
+ <KeyboardView>
157
+ <ScrollView contentContainerStyle={styles.container}>
158
+ <Text style={styles.warningText}>
159
+ Reporting this message will notify a church leader. The person you are reporting will not
160
+ receive a notification from Church Center. If someone is in immediate danger, call local
161
+ emergency services. Don&apos;t wait.
162
+ </Text>
163
+
164
+ <MessagePreview message={message} />
165
+
166
+ <View style={styles.reasonSection}>
167
+ <Text style={styles.fieldLabel}>
168
+ What best describes the problem? <Text style={styles.required}>*</Text>
169
+ </Text>
170
+
171
+ <View style={styles.reasonListContainer}>
172
+ <ReportReasonList selectedReason={selectedReason} onReasonChange={handleReasonChange} />
173
+ </View>
174
+ </View>
175
+
176
+ {selectedReason === 'other' && (
177
+ <View style={styles.detailSection}>
178
+ <Text style={styles.fieldLabel}>
179
+ Please provide more details <Text style={styles.required}>*</Text>
180
+ </Text>
181
+ <TextInput
182
+ style={styles.textInput}
183
+ multiline
184
+ placeholder="Describe the issue..."
185
+ value={reasonDetail}
186
+ onChangeText={setReasonDetail}
187
+ maxLength={MAX_DETAIL_LENGTH}
188
+ accessibilityLabel="Additional details"
189
+ accessibilityHint="Provide more information about why you're reporting this message"
190
+ />
191
+ {reasonDetail.length >= MAX_DETAIL_LENGTH - 100 && (
192
+ <Text variant="footnote">
193
+ {reasonDetail.length}/{MAX_DETAIL_LENGTH}
194
+ </Text>
195
+ )}
196
+ </View>
197
+ )}
198
+ </ScrollView>
199
+ </KeyboardView>
200
+ )
201
+ }
202
+
203
+ const useStyles = () => {
204
+ const { bottom } = useSafeAreaInsets()
205
+ const { colors } = useTheme()
206
+
207
+ return StyleSheet.create({
208
+ container: {
209
+ padding: 16,
210
+ paddingBottom: 16 + bottom,
211
+ gap: 24,
212
+ },
213
+ fullHeight: {
214
+ flex: 1,
215
+ justifyContent: 'center',
216
+ alignItems: 'center',
217
+ },
218
+ fullScreenContainer: {
219
+ flex: 1,
220
+ justifyContent: 'center',
221
+ alignItems: 'center',
222
+ backgroundColor: colors.surfaceColor100,
223
+ },
224
+ loadingText: {
225
+ marginTop: 16,
226
+ fontSize: 16,
227
+ fontWeight: '600',
228
+ color: colors.textColorDefaultPrimary,
229
+ },
230
+ warningText: {
231
+ fontSize: 14,
232
+ lineHeight: 20,
233
+ color: colors.textColorDefaultSecondary,
234
+ },
235
+ reasonSection: {
236
+ gap: 8,
237
+ },
238
+ fieldLabel: {
239
+ fontSize: 16,
240
+ fontWeight: platformFontWeightBold,
241
+ color: colors.textColorDefaultPrimary,
242
+ },
243
+ required: {
244
+ color: colors.statusErrorText,
245
+ },
246
+ reasonListContainer: {
247
+ backgroundColor: colors.surfaceColor100,
248
+ },
249
+ detailSection: {
250
+ gap: 8,
251
+ },
252
+ textInput: {
253
+ color: colors.textColorDefaultPrimary,
254
+ fontSize: 16,
255
+ textAlignVertical: 'top',
256
+ minHeight: 120,
257
+ maxHeight: 200,
258
+ borderWidth: 1,
259
+ borderColor: colors.borderColorDefaultBase,
260
+ borderRadius: 8,
261
+ paddingHorizontal: 16,
262
+ paddingVertical: 12,
263
+ backgroundColor: colors.surfaceColor100,
264
+ },
265
+ successIcon: {
266
+ color: colors.statusSuccessIcon,
267
+ fontSize: 48,
268
+ },
269
+ successTitle: {
270
+ fontSize: 24,
271
+ marginBottom: 4,
272
+ },
273
+ successSubtitle: {
274
+ fontSize: 16,
275
+ marginBottom: 4,
276
+ },
277
+ })
278
+ }