@planningcenter/chat-react-native 3.23.0 → 3.23.1-qa-544.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 (135) hide show
  1. package/build/components/display/platform_modal_header_buttons.d.ts +8 -0
  2. package/build/components/display/platform_modal_header_buttons.d.ts.map +1 -1
  3. package/build/components/display/platform_modal_header_buttons.js +10 -0
  4. package/build/components/display/platform_modal_header_buttons.js.map +1 -1
  5. package/build/components/display/toggle_button.d.ts +11 -4
  6. package/build/components/display/toggle_button.d.ts.map +1 -1
  7. package/build/components/display/toggle_button.js +6 -5
  8. package/build/components/display/toggle_button.js.map +1 -1
  9. package/build/components/primitive/form_sheet.d.ts +1 -1
  10. package/build/components/primitive/form_sheet.d.ts.map +1 -1
  11. package/build/components/primitive/form_sheet.js +1 -1
  12. package/build/components/primitive/form_sheet.js.map +1 -1
  13. package/build/hooks/index.d.ts +1 -0
  14. package/build/hooks/index.d.ts.map +1 -1
  15. package/build/hooks/index.js +1 -0
  16. package/build/hooks/index.js.map +1 -1
  17. package/build/hooks/use_features.d.ts +1 -0
  18. package/build/hooks/use_features.d.ts.map +1 -1
  19. package/build/hooks/use_features.js +1 -0
  20. package/build/hooks/use_features.js.map +1 -1
  21. package/build/hooks/use_new_conversation_entry.d.ts +3 -0
  22. package/build/hooks/use_new_conversation_entry.d.ts.map +1 -0
  23. package/build/hooks/use_new_conversation_entry.js +20 -0
  24. package/build/hooks/use_new_conversation_entry.js.map +1 -0
  25. package/build/hooks/use_report_message.d.ts +6 -0
  26. package/build/hooks/use_report_message.d.ts.map +1 -0
  27. package/build/hooks/use_report_message.js +28 -0
  28. package/build/hooks/use_report_message.js.map +1 -0
  29. package/build/navigation/index.d.ts +52 -10
  30. package/build/navigation/index.d.ts.map +1 -1
  31. package/build/navigation/index.js +41 -7
  32. package/build/navigation/index.js.map +1 -1
  33. package/build/screens/conversation_filter_recipients/conversation_filter_recipients_screen.js +5 -3
  34. package/build/screens/conversation_filter_recipients/conversation_filter_recipients_screen.js.map +1 -1
  35. package/build/screens/conversation_select_recipients/conversation_new_entry_screen.d.ts +4 -0
  36. package/build/screens/conversation_select_recipients/conversation_new_entry_screen.d.ts.map +1 -0
  37. package/build/screens/conversation_select_recipients/conversation_new_entry_screen.js +67 -0
  38. package/build/screens/conversation_select_recipients/conversation_new_entry_screen.js.map +1 -0
  39. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.d.ts.map +1 -1
  40. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js +17 -2
  41. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js.map +1 -1
  42. package/build/screens/conversation_select_type_screen.d.ts +11 -0
  43. package/build/screens/conversation_select_type_screen.d.ts.map +1 -0
  44. package/build/screens/conversation_select_type_screen.js +32 -0
  45. package/build/screens/conversation_select_type_screen.js.map +1 -0
  46. package/build/screens/conversations/components/list_header_component.d.ts.map +1 -1
  47. package/build/screens/conversations/components/list_header_component.js +54 -25
  48. package/build/screens/conversations/components/list_header_component.js.map +1 -1
  49. package/build/screens/group_notification_settings_screen.d.ts +8 -0
  50. package/build/screens/group_notification_settings_screen.d.ts.map +1 -0
  51. package/build/screens/group_notification_settings_screen.js +79 -0
  52. package/build/screens/group_notification_settings_screen.js.map +1 -0
  53. package/build/screens/index.d.ts +4 -1
  54. package/build/screens/index.d.ts.map +1 -1
  55. package/build/screens/index.js +4 -1
  56. package/build/screens/index.js.map +1 -1
  57. package/build/screens/message_actions_screen.js +14 -2
  58. package/build/screens/message_actions_screen.js.map +1 -1
  59. package/build/screens/message_report/components/message_preview.d.ts +10 -0
  60. package/build/screens/message_report/components/message_preview.d.ts.map +1 -0
  61. package/build/screens/message_report/components/message_preview.js +81 -0
  62. package/build/screens/message_report/components/message_preview.js.map +1 -0
  63. package/build/screens/message_report/components/report_reason_list.d.ts +9 -0
  64. package/build/screens/message_report/components/report_reason_list.d.ts.map +1 -0
  65. package/build/screens/message_report/components/report_reason_list.js +67 -0
  66. package/build/screens/message_report/components/report_reason_list.js.map +1 -0
  67. package/build/screens/message_report_screen.d.ts +10 -0
  68. package/build/screens/message_report_screen.d.ts.map +1 -0
  69. package/build/screens/message_report_screen.js +214 -0
  70. package/build/screens/message_report_screen.js.map +1 -0
  71. package/build/screens/notification_settings/hooks/groups.d.ts +94 -0
  72. package/build/screens/notification_settings/hooks/groups.d.ts.map +1 -0
  73. package/build/screens/notification_settings/hooks/groups.js +92 -0
  74. package/build/screens/notification_settings/hooks/groups.js.map +1 -0
  75. package/build/screens/notification_settings_screen.d.ts +5 -0
  76. package/build/screens/notification_settings_screen.d.ts.map +1 -0
  77. package/build/screens/notification_settings_screen.js +234 -0
  78. package/build/screens/notification_settings_screen.js.map +1 -0
  79. package/build/screens/preferred_app/hooks/use_chat_types.d.ts +39 -0
  80. package/build/screens/preferred_app/hooks/use_chat_types.d.ts.map +1 -0
  81. package/build/screens/preferred_app/hooks/use_chat_types.js +12 -0
  82. package/build/screens/preferred_app/hooks/use_chat_types.js.map +1 -0
  83. package/build/screens/preferred_app_selection_screen.d.ts +10 -0
  84. package/build/screens/preferred_app_selection_screen.d.ts.map +1 -0
  85. package/build/screens/preferred_app_selection_screen.js +128 -0
  86. package/build/screens/preferred_app_selection_screen.js.map +1 -0
  87. package/build/types/resources/group_membership.d.ts +6 -0
  88. package/build/types/resources/group_membership.d.ts.map +1 -0
  89. package/build/types/resources/group_membership.js +2 -0
  90. package/build/types/resources/group_membership.js.map +1 -0
  91. package/build/types/resources/group_resource.d.ts +4 -0
  92. package/build/types/resources/group_resource.d.ts.map +1 -1
  93. package/build/types/resources/group_resource.js.map +1 -1
  94. package/build/types/resources/index.d.ts +2 -0
  95. package/build/types/resources/index.d.ts.map +1 -1
  96. package/build/types/resources/index.js +2 -0
  97. package/build/types/resources/index.js.map +1 -1
  98. package/build/types/resources/message_report.d.ts +19 -0
  99. package/build/types/resources/message_report.d.ts.map +1 -0
  100. package/build/types/resources/message_report.js +9 -0
  101. package/build/types/resources/message_report.js.map +1 -0
  102. package/package.json +2 -2
  103. package/src/components/display/platform_modal_header_buttons.tsx +18 -0
  104. package/src/components/display/toggle_button.tsx +27 -15
  105. package/src/components/primitive/form_sheet.tsx +4 -2
  106. package/src/hooks/index.ts +1 -0
  107. package/src/hooks/use_features.ts +1 -0
  108. package/src/hooks/use_new_conversation_entry.ts +31 -0
  109. package/src/hooks/use_report_message.ts +37 -0
  110. package/src/navigation/index.tsx +51 -7
  111. package/src/screens/conversation_filter_recipients/conversation_filter_recipients_screen.tsx +10 -3
  112. package/src/screens/conversation_select_recipients/conversation_new_entry_screen.tsx +100 -0
  113. package/src/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.tsx +23 -3
  114. package/src/screens/conversation_select_type_screen.tsx +53 -0
  115. package/src/screens/conversations/components/list_header_component.tsx +103 -67
  116. package/src/screens/group_notification_settings_screen.tsx +92 -0
  117. package/src/screens/index.ts +4 -1
  118. package/src/screens/message_actions_screen.tsx +24 -2
  119. package/src/screens/message_report/components/message_preview.tsx +99 -0
  120. package/src/screens/message_report/components/report_reason_list.tsx +106 -0
  121. package/src/screens/message_report_screen.tsx +278 -0
  122. package/src/screens/notification_settings/hooks/groups.ts +101 -0
  123. package/src/screens/notification_settings_screen.tsx +383 -0
  124. package/src/screens/preferred_app/hooks/use_chat_types.ts +25 -0
  125. package/src/screens/preferred_app_selection_screen.tsx +169 -0
  126. package/src/types/images.d.ts +14 -0
  127. package/src/types/resources/group_membership.ts +6 -0
  128. package/src/types/resources/group_resource.ts +5 -0
  129. package/src/types/resources/index.ts +2 -0
  130. package/src/types/resources/message_report.ts +20 -0
  131. package/build/screens/conversation_select_recipients/conversation_select_recipients_screen.d.ts +0 -4
  132. package/build/screens/conversation_select_recipients/conversation_select_recipients_screen.d.ts.map +0 -1
  133. package/build/screens/conversation_select_recipients/conversation_select_recipients_screen.js +0 -146
  134. package/build/screens/conversation_select_recipients/conversation_select_recipients_screen.js.map +0 -1
  135. package/src/screens/conversation_select_recipients/conversation_select_recipients_screen.tsx +0 -215
@@ -0,0 +1,99 @@
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 styles = useStyles()
14
+
15
+ const hasImages = message.attachments.some(
16
+ att => att.type === 'MessageAttachment' && att.attributes?.contentType?.startsWith('image/')
17
+ )
18
+
19
+ return (
20
+ <View style={[styles.previewContainer, style]}>
21
+ <View style={styles.previewHeader}>
22
+ <Avatar sourceUri={message.author.avatar} size="md" />
23
+ <Text style={styles.authorName}>{message.author.name}</Text>
24
+ </View>
25
+
26
+ <View style={styles.messageBubbleContainer}>
27
+ {message.text && (
28
+ <View style={styles.messageBubble}>
29
+ <Text style={styles.messageText} numberOfLines={5}>
30
+ {message.text}
31
+ </Text>
32
+ </View>
33
+ )}
34
+
35
+ {hasImages && (
36
+ <View style={styles.imagePlaceholder}>
37
+ <Icon name="general.image" style={styles.placeholderIcon} />
38
+ <Text variant="secondary" style={styles.placeholderText}>
39
+ Image hidden
40
+ </Text>
41
+ </View>
42
+ )}
43
+ </View>
44
+ </View>
45
+ )
46
+ }
47
+
48
+ const useStyles = () => {
49
+ const { colors } = useTheme()
50
+
51
+ return StyleSheet.create({
52
+ previewContainer: {
53
+ gap: 12,
54
+ },
55
+ previewHeader: {
56
+ flexDirection: 'row',
57
+ alignItems: 'center',
58
+ gap: 12,
59
+ },
60
+ authorName: {
61
+ fontSize: 14,
62
+ fontWeight: '600',
63
+ color: colors.textColorDefaultPrimary,
64
+ },
65
+ messageBubbleContainer: {
66
+ gap: 8,
67
+ },
68
+ messageBubble: {
69
+ backgroundColor: colors.fillColorNeutral060,
70
+ borderRadius: 16,
71
+ paddingHorizontal: 16,
72
+ paddingVertical: 12,
73
+ alignSelf: 'flex-start',
74
+ maxWidth: '80%',
75
+ },
76
+ messageText: {
77
+ fontSize: 14,
78
+ color: colors.textColorDefaultPrimary,
79
+ lineHeight: 20,
80
+ },
81
+ imagePlaceholder: {
82
+ height: 120,
83
+ backgroundColor: colors.fillColorNeutral060,
84
+ borderRadius: 16,
85
+ justifyContent: 'center',
86
+ alignItems: 'center',
87
+ gap: 8,
88
+ maxWidth: '80%',
89
+ alignSelf: 'flex-start',
90
+ },
91
+ placeholderIcon: {
92
+ fontSize: 32,
93
+ color: colors.iconColorDefaultSecondary,
94
+ },
95
+ placeholderText: {
96
+ fontSize: 14,
97
+ },
98
+ })
99
+ }
@@ -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
+ }
@@ -0,0 +1,101 @@
1
+ import { useMutation, useQueryClient } from '@tanstack/react-query'
2
+ import { getRequestQueryKey, useApiClient } from '../../../hooks'
3
+ import { useSuspenseGet, useSuspensePaginator } from '../../../hooks'
4
+ import { ApiResource } from '../../../types'
5
+ import { GroupMembership, GroupResourceWithMembership } from '../../../types/resources'
6
+ import { throwResponseError } from '../../../utils/response_error'
7
+
8
+ export const getGroupsRequestArgs = () => ({
9
+ url: '/me/groups',
10
+ data: {
11
+ perPage: 100,
12
+ include: ['my_group_membership'],
13
+ filter: 'user_settable_notification_level',
14
+ fields: {
15
+ Group: [],
16
+ GroupMembership: ['notification_level', 'notification_level_description'],
17
+ },
18
+ order: 'name',
19
+ },
20
+ })
21
+
22
+ export const getGroupRequestArgs = ({ groupId }: { groupId: number | string }) => ({
23
+ url: `/me/groups/${groupId}`,
24
+ data: {
25
+ include: ['my_group_membership'],
26
+ fields: {
27
+ Group: ['name', 'source_app_name', 'source_type', 'my_group_membership'],
28
+ GroupMembership: ['notification_level', 'notification_level_description'],
29
+ },
30
+ },
31
+ })
32
+
33
+ export const useGroups = () => {
34
+ const args = getGroupsRequestArgs()
35
+
36
+ return useSuspensePaginator<GroupResourceWithMembership>(args)
37
+ }
38
+
39
+ export const useGroup = ({ groupId }: { groupId: number | string }) => {
40
+ const args = getGroupRequestArgs({ groupId })
41
+
42
+ return useSuspenseGet<GroupResourceWithMembership>(args)
43
+ }
44
+
45
+ export const useGroupMembershipUpdate = ({ groupId }: { groupId: number | string }) => {
46
+ const apiClient = useApiClient()
47
+ const queryClient = useQueryClient()
48
+ const requestArgs = getGroupRequestArgs({ groupId })
49
+ const queryKey = getRequestQueryKey(requestArgs)
50
+
51
+ return useMutation({
52
+ throwOnError: true,
53
+ onMutate: notificationLevel => {
54
+ queryClient.setQueryData<ApiResource<GroupResourceWithMembership>>(queryKey, groupData => {
55
+ if (!groupData?.data.myGroupMembership) return groupData
56
+
57
+ return {
58
+ ...groupData,
59
+ data: {
60
+ ...groupData.data,
61
+ myGroupMembership: {
62
+ ...groupData.data.myGroupMembership,
63
+ notificationLevel,
64
+ },
65
+ },
66
+ }
67
+ })
68
+ },
69
+ onSuccess: response => {
70
+ queryClient.setQueryData<ApiResource<GroupResourceWithMembership>>(queryKey, groupData => {
71
+ if (!groupData?.data.myGroupMembership) return groupData
72
+
73
+ return {
74
+ ...groupData,
75
+ data: {
76
+ ...groupData.data,
77
+ myGroupMembership: {
78
+ ...groupData.data.myGroupMembership,
79
+ ...response.data,
80
+ },
81
+ },
82
+ }
83
+ })
84
+ },
85
+ mutationFn: (notificationLevel: string) => {
86
+ return apiClient.chat
87
+ .patch<ApiResource<GroupMembership>>({
88
+ url: `/me/groups/${groupId}/my_group_membership`,
89
+ data: {
90
+ data: {
91
+ type: 'GroupMembership',
92
+ attributes: {
93
+ notification_level: notificationLevel,
94
+ },
95
+ },
96
+ },
97
+ })
98
+ .catch(throwResponseError)
99
+ },
100
+ })
101
+ }