@planningcenter/chat-react-native 3.24.0-rc.6 → 3.24.0-rc.8

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 (31) hide show
  1. package/build/navigation/index.d.ts +82 -0
  2. package/build/navigation/index.d.ts.map +1 -1
  3. package/build/navigation/index.js +32 -0
  4. package/build/navigation/index.js.map +1 -1
  5. package/build/screens/conversation_select_type_screen.d.ts.map +1 -1
  6. package/build/screens/conversation_select_type_screen.js +12 -6
  7. package/build/screens/conversation_select_type_screen.js.map +1 -1
  8. package/build/screens/index.d.ts +2 -0
  9. package/build/screens/index.d.ts.map +1 -1
  10. package/build/screens/index.js +2 -0
  11. package/build/screens/index.js.map +1 -1
  12. package/build/screens/notification_settings_screen.d.ts +5 -0
  13. package/build/screens/notification_settings_screen.d.ts.map +1 -0
  14. package/build/screens/notification_settings_screen.js +216 -0
  15. package/build/screens/notification_settings_screen.js.map +1 -0
  16. package/build/screens/preferred_app/hooks/use_chat_types.d.ts +39 -0
  17. package/build/screens/preferred_app/hooks/use_chat_types.d.ts.map +1 -0
  18. package/build/screens/preferred_app/hooks/use_chat_types.js +12 -0
  19. package/build/screens/preferred_app/hooks/use_chat_types.js.map +1 -0
  20. package/build/screens/preferred_app_selection_screen.d.ts +10 -0
  21. package/build/screens/preferred_app_selection_screen.d.ts.map +1 -0
  22. package/build/screens/preferred_app_selection_screen.js +128 -0
  23. package/build/screens/preferred_app_selection_screen.js.map +1 -0
  24. package/package.json +2 -2
  25. package/src/navigation/index.tsx +35 -0
  26. package/src/screens/conversation_select_type_screen.tsx +12 -6
  27. package/src/screens/index.ts +2 -0
  28. package/src/screens/notification_settings_screen.tsx +356 -0
  29. package/src/screens/preferred_app/hooks/use_chat_types.ts +25 -0
  30. package/src/screens/preferred_app_selection_screen.tsx +169 -0
  31. package/src/types/images.d.ts +14 -0
@@ -0,0 +1,356 @@
1
+ import { PlatformPressable } from '@react-navigation/elements'
2
+ import { StaticScreenProps, useNavigation } from '@react-navigation/native'
3
+ import type { NativeStackNavigationProp } from '@react-navigation/native-stack'
4
+ import React, { useCallback, useEffect, type ReactNode } from 'react'
5
+ import { FlatList, Platform, StyleSheet, View, type ViewProps, type ViewStyle } from 'react-native'
6
+ import { Heading, Icon, Text } from '../components'
7
+ import { HeaderTextButton } from '../components/display/platform_modal_header_buttons'
8
+ import { useTheme } from '../hooks'
9
+ import { isDefined } from '../types'
10
+ import { useChatTypes } from './preferred_app/hooks/use_chat_types'
11
+ import type { PreferredAppSelectionScreenProps } from './preferred_app_selection_screen'
12
+
13
+ // =========================================
14
+ // ====== Factory Constants & Types ========
15
+ // =========================================
16
+
17
+ type NotificationSettingsStackParamList = {
18
+ NotificationSettings: {}
19
+ PreferredAppSelection: PreferredAppSelectionScreenProps['route']['params']
20
+ }
21
+
22
+ enum SectionTypes {
23
+ header,
24
+ hidden,
25
+ setting,
26
+ view,
27
+ link,
28
+ }
29
+
30
+ type SectionListData = Array<
31
+ | DataItem<{ title: string }, SectionTypes.header>
32
+ | DataItem<ViewProps, SectionTypes.view>
33
+ | DataItem<SettingRowProps, SectionTypes.setting>
34
+ | DataItem<LinkRowProps, SectionTypes.link>
35
+ | DataItem<any, SectionTypes.hidden>
36
+ >
37
+
38
+ interface DataItem<T, TName extends SectionTypes> {
39
+ type: TName
40
+ data: T
41
+ sectionOuterStyle?: ViewStyle
42
+ sectionInnerStyle?: ViewStyle
43
+ showBottomBorder?: boolean
44
+ }
45
+
46
+ // =================================
47
+ // ====== Components ===============
48
+ // =================================
49
+
50
+ export type NotificationSettingsScreenProps = StaticScreenProps<{}>
51
+
52
+ export function NotificationSettingsScreen({}: NotificationSettingsScreenProps) {
53
+ const navigation = useNavigation<NativeStackNavigationProp<NotificationSettingsStackParamList>>()
54
+ const styles = useStyles()
55
+ const { data: chatTypes } = useChatTypes()
56
+
57
+ // Header configuration
58
+ const HeaderRight = useCallback(() => {
59
+ return (
60
+ <HeaderTextButton
61
+ onPress={() => {
62
+ // Save settings logic would go here
63
+ navigation.goBack()
64
+ }}
65
+ title="Done"
66
+ />
67
+ )
68
+ }, [navigation])
69
+
70
+ useEffect(() => {
71
+ navigation.setOptions({
72
+ headerRight: HeaderRight,
73
+ })
74
+ }, [HeaderRight, navigation])
75
+
76
+ // Build section data
77
+ const listData = [
78
+ {
79
+ type: SectionTypes.header,
80
+ data: { title: 'Preferred app' },
81
+ sectionInnerStyle: styles.sectionInnerHeader,
82
+ },
83
+ {
84
+ type: SectionTypes.view,
85
+ data: {
86
+ children: (
87
+ <Text variant="footnote" style={styles.sectionDescription}>
88
+ Choose which app receives each type of chat notification
89
+ </Text>
90
+ ),
91
+ },
92
+ showBottomBorder: true,
93
+ },
94
+ ...chatTypes.map(type => ({
95
+ type: SectionTypes.link,
96
+ data: {
97
+ title: `${type.title} Conversations`,
98
+ subtitle: type.preferredApp,
99
+ onPress: () =>
100
+ navigation.navigate('PreferredAppSelection', {
101
+ chatTypeId: type.id,
102
+ }),
103
+ },
104
+ showBottomBorder: true,
105
+ })),
106
+ // {
107
+ // type: SectionTypes.header,
108
+ // data: {
109
+ // title: 'Manage chat settings',
110
+ // },
111
+ // sectionInnerStyle: styles.sectionInnerHeader,
112
+ // },
113
+ // {
114
+ // type: SectionTypes.view,
115
+ // data: {
116
+ // children: (
117
+ // <Text variant="footnote" style={styles.sectionDescription}>
118
+ // Notification settings for all of your group, team and event related conversations
119
+ // </Text>
120
+ // ),
121
+ // },
122
+ // showBottomBorder: true,
123
+ // },
124
+ ].filter(item => item.type !== SectionTypes.hidden)
125
+
126
+ const headerIndices = listData
127
+ .map(({ type }, index) => (type === SectionTypes.header ? index : undefined))
128
+ .filter(isDefined)
129
+
130
+ return (
131
+ <View style={styles.listContainer}>
132
+ <FlatList
133
+ data={listData as SectionListData}
134
+ contentContainerStyle={styles.contentContainer}
135
+ renderItem={({ item, index }) => {
136
+ const [isStart, isEnd] = [
137
+ index === 0 || headerIndices.includes(index),
138
+ index === listData.length - 1 || headerIndices.includes(index + 1),
139
+ ]
140
+
141
+ switch (item.type) {
142
+ case SectionTypes.header:
143
+ return (
144
+ <ListSection
145
+ isStart={isStart}
146
+ isEnd={isEnd}
147
+ showBottomBorder={item?.showBottomBorder}
148
+ outerStyle={item?.sectionOuterStyle}
149
+ innerStyle={item?.sectionInnerStyle}
150
+ >
151
+ <Heading variant="h3">{item.data.title}</Heading>
152
+ </ListSection>
153
+ )
154
+ case SectionTypes.setting:
155
+ return (
156
+ <ListSection
157
+ isStart={isStart}
158
+ isEnd={isEnd}
159
+ showBottomBorder={item?.showBottomBorder}
160
+ outerStyle={item?.sectionOuterStyle}
161
+ innerStyle={item?.sectionInnerStyle}
162
+ >
163
+ <SettingRow {...item.data} />
164
+ </ListSection>
165
+ )
166
+ case SectionTypes.view:
167
+ return (
168
+ <ListSection
169
+ isStart={isStart}
170
+ isEnd={isEnd}
171
+ showBottomBorder={item?.showBottomBorder}
172
+ outerStyle={item?.sectionOuterStyle}
173
+ innerStyle={item?.sectionInnerStyle}
174
+ >
175
+ <View {...item.data} style={item.data.style} />
176
+ </ListSection>
177
+ )
178
+ case SectionTypes.link:
179
+ return (
180
+ <ListSection
181
+ isStart={isStart}
182
+ isEnd={isEnd}
183
+ showBottomBorder={item?.showBottomBorder}
184
+ outerStyle={item?.sectionOuterStyle}
185
+ innerStyle={item?.sectionInnerStyle}
186
+ >
187
+ <LinkRow {...item.data} />
188
+ </ListSection>
189
+ )
190
+ default:
191
+ return null
192
+ }
193
+ }}
194
+ />
195
+ </View>
196
+ )
197
+ }
198
+
199
+ interface ListSectionProps {
200
+ isStart?: boolean
201
+ isEnd?: boolean
202
+ showBottomBorder?: boolean
203
+ outerStyle?: ViewStyle
204
+ innerStyle?: ViewStyle
205
+ children: ReactNode
206
+ }
207
+
208
+ function ListSection({
209
+ isStart,
210
+ isEnd,
211
+ showBottomBorder,
212
+ outerStyle,
213
+ innerStyle,
214
+ children,
215
+ }: ListSectionProps) {
216
+ const styles = useStyles({ isStart, isEnd })
217
+ const bottomBorder = showBottomBorder ? styles.sectionInnerBottomBorder : {}
218
+
219
+ return (
220
+ <View style={[styles.sectionOuterBase, outerStyle]}>
221
+ <View style={[styles.sectionInnerBase, bottomBorder, innerStyle]}>{children}</View>
222
+ </View>
223
+ )
224
+ }
225
+
226
+ interface SettingRowProps {
227
+ title: string
228
+ style?: ViewStyle
229
+ rightItem?: ReactNode
230
+ subtitle?: string
231
+ rightItemStyle?: ViewStyle
232
+ }
233
+
234
+ function SettingRow({ title, style, subtitle, rightItem, rightItemStyle = {} }: SettingRowProps) {
235
+ const styles = useStyles()
236
+
237
+ return (
238
+ <View style={[styles.settingRow, style]}>
239
+ <View style={styles.settingRowContent}>
240
+ <Text variant="plain" style={styles.settingRowText}>
241
+ {title}
242
+ </Text>
243
+ {Boolean(subtitle) && <Text variant="footnote">{subtitle}</Text>}
244
+ </View>
245
+ {Boolean(rightItem) && <View style={rightItemStyle}>{rightItem}</View>}
246
+ </View>
247
+ )
248
+ }
249
+
250
+ interface LinkRowProps {
251
+ title: string
252
+ subtitle: string
253
+ onPress: () => void
254
+ }
255
+
256
+ function LinkRow({ title, subtitle, onPress }: LinkRowProps) {
257
+ const styles = useLinkRowStyles()
258
+
259
+ return (
260
+ <PlatformPressable
261
+ style={styles.row}
262
+ onPress={onPress}
263
+ accessibilityRole="link"
264
+ accessibilityLabel={title}
265
+ accessibilityHint={`Navigate to ${title} settings`}
266
+ >
267
+ <View style={styles.innerContainer}>
268
+ <Text style={styles.title} numberOfLines={2}>
269
+ {title}
270
+ </Text>
271
+ <View style={styles.rightContent}>
272
+ <Text numberOfLines={1}>{subtitle}</Text>
273
+ {Platform.OS === 'ios' && (
274
+ <Icon name="general.rightChevron" size={16} style={styles.icon} />
275
+ )}
276
+ </View>
277
+ </View>
278
+ </PlatformPressable>
279
+ )
280
+ }
281
+
282
+ // =================================
283
+ // ====== Styles ===================
284
+ // =================================
285
+
286
+ const useStyles = ({ isStart, isEnd }: { isStart?: boolean; isEnd?: boolean } = {}) => {
287
+ const { colors } = useTheme()
288
+ const headerBottomPadding = 0
289
+
290
+ return StyleSheet.create({
291
+ listContainer: {
292
+ flex: 1,
293
+ backgroundColor: colors.surfaceColor080,
294
+ },
295
+ contentContainer: {},
296
+ sectionOuterBase: {
297
+ paddingLeft: 16,
298
+ backgroundColor: colors.surfaceColor100,
299
+ },
300
+ sectionInnerBase: {
301
+ paddingRight: 16,
302
+ paddingTop: isStart ? 16 : 12,
303
+ paddingBottom: isEnd ? 16 : 12,
304
+ },
305
+ sectionInnerHeader: {
306
+ paddingBottom: headerBottomPadding,
307
+ },
308
+ sectionInnerBottomBorder: {
309
+ borderBottomColor: colors.borderColorDefaultBase,
310
+ borderBottomWidth: 1,
311
+ },
312
+ sectionDescription: {
313
+ color: colors.textColorDefaultSecondary,
314
+ },
315
+ settingRow: {
316
+ flexDirection: 'row',
317
+ justifyContent: 'space-between',
318
+ alignItems: 'center',
319
+ gap: 8,
320
+ },
321
+ settingRowContent: {
322
+ flex: 1,
323
+ },
324
+ settingRowText: {
325
+ lineHeight: 20,
326
+ },
327
+ })
328
+ }
329
+
330
+ const useLinkRowStyles = () => {
331
+ const theme = useTheme()
332
+
333
+ return StyleSheet.create({
334
+ row: {
335
+ paddingLeft: 0,
336
+ },
337
+ innerContainer: {
338
+ flexDirection: 'row',
339
+ alignItems: 'center',
340
+ justifyContent: 'space-between',
341
+ gap: 12,
342
+ paddingVertical: 4,
343
+ },
344
+ rightContent: {
345
+ flexDirection: 'row',
346
+ alignItems: 'center',
347
+ gap: 8,
348
+ },
349
+ title: {
350
+ flexShrink: 1,
351
+ },
352
+ icon: {
353
+ color: theme.colors.iconColorDefaultDisabled,
354
+ },
355
+ })
356
+ }
@@ -0,0 +1,25 @@
1
+ import { useSuspenseGet } from '../../../hooks'
2
+ import { ResourceObject } from '../../../types'
3
+
4
+ export type SourceAppName = 'Services' | 'Church Center'
5
+ export type PreferredApp = 'Services' | 'Church Center' | 'Chat' | 'None'
6
+
7
+ export interface ChatTypeResource extends ResourceObject {
8
+ type: 'ChatType'
9
+ defaultPreferredApp: PreferredApp
10
+ preferredApp: PreferredApp
11
+ preferredAppOptions: PreferredApp[]
12
+ sourceAppName: SourceAppName
13
+ title: string
14
+ }
15
+
16
+ export const useChatTypes = () => {
17
+ return useSuspenseGet<ChatTypeResource[]>({
18
+ url: '/me/chat_types',
19
+ data: {
20
+ fields: {
21
+ ChatType: [],
22
+ },
23
+ },
24
+ })
25
+ }
@@ -0,0 +1,169 @@
1
+ import { PlatformPressable } from '@react-navigation/elements'
2
+ import { StaticScreenProps } from '@react-navigation/native'
3
+ import { useMutation } from '@tanstack/react-query'
4
+ import React, { type PropsWithChildren } from 'react'
5
+ import { Image, StyleSheet, View, type ViewStyle } from 'react-native'
6
+ import churchCenterLogo from '../../assets/churchCenter.png'
7
+ import servicesLogo from '../../assets/servicesApp.png'
8
+ import chatLogo from '../../assets/chatLogo.png'
9
+ import { Heading, Icon, Text } from '../components'
10
+ import { useApiClient, useTheme } from '../hooks'
11
+ import { PreferredApp, useChatTypes } from './preferred_app/hooks/use_chat_types'
12
+ import { useAppName } from '../hooks/use_app_name'
13
+ import { throwResponseError } from '../utils/response_error'
14
+
15
+ export type PreferredAppSelectionScreenProps = StaticScreenProps<{
16
+ chatTypeId: number | string
17
+ conversationType?: 'group' | 'team'
18
+ currentSelection?: PreferredApp
19
+ }>
20
+
21
+ export function PreferredAppSelectionScreen({ route }: PreferredAppSelectionScreenProps) {
22
+ const { chatTypeId } = route.params
23
+ const { data: chatTypes, refetch: refetchChatTypes } = useChatTypes()
24
+ const appName = useAppName()
25
+
26
+ const apiClient = useApiClient()
27
+
28
+ const { mutate: updateChatType } = useMutation({
29
+ throwOnError: true,
30
+ onSuccess: () => refetchChatTypes(),
31
+ mutationFn: (app: PreferredApp) => {
32
+ return apiClient.chat
33
+ .post({
34
+ url: `/me/chat_types/${chatTypeId}/set_preferred_appz`,
35
+ data: { data: { type: 'ChatType', attributes: { app } } },
36
+ })
37
+ .catch(throwResponseError)
38
+ },
39
+ })
40
+ const chatType = chatTypes.find(t => t.id === chatTypeId)
41
+ const { preferredAppOptions } = chatType || {}
42
+
43
+ const handleSelection = (app: PreferredApp) => {
44
+ updateChatType(app)
45
+ }
46
+
47
+ const sectionTitle = `${chatType?.title} Conversations`
48
+
49
+ return (
50
+ <View style={styles.container}>
51
+ <View style={styles.section}>
52
+ <Heading variant="h3" style={styles.sectionHeading}>
53
+ {sectionTitle}
54
+ </Heading>
55
+ {preferredAppOptions?.sort(sortPreferredApp('asc')).map((option, key) => (
56
+ <PressableRow
57
+ key={`${key}-${option}`}
58
+ isActive={chatType?.preferredApp === option}
59
+ onPress={() => handleSelection(option)}
60
+ >
61
+ <Image source={getAppImage(option)} style={styles.logo} />
62
+ <Text>
63
+ {option}
64
+ {appName.includes(option.replace(/\s/, '').toLowerCase()) ? ' ( current )' : ''}
65
+ </Text>
66
+ </PressableRow>
67
+ ))}
68
+ </View>
69
+ </View>
70
+ )
71
+ }
72
+
73
+ const sortPreferredApp = (dir?: 'asc' | 'desc') => (a: PreferredApp, b: PreferredApp) => {
74
+ if (a === 'None') return 1
75
+ if (a > b) return dir === 'asc' ? 1 : -1
76
+ if (b > a) return dir === 'asc' ? -1 : 1
77
+
78
+ return 0
79
+ }
80
+
81
+ const getAppImage = (option: PreferredApp) => {
82
+ switch (option) {
83
+ case 'Church Center':
84
+ return churchCenterLogo
85
+ case 'Services':
86
+ return servicesLogo
87
+ case 'Chat':
88
+ return chatLogo
89
+ default:
90
+ return undefined
91
+ }
92
+ }
93
+
94
+ const PressableRow = ({
95
+ children,
96
+ isActive,
97
+ onPress,
98
+ style,
99
+ }: PropsWithChildren<{ isActive: boolean; onPress: () => void; style?: ViewStyle }>) => {
100
+ const styles = useRowStyles({ isActive })
101
+ return (
102
+ <PlatformPressable
103
+ style={styles.pressable}
104
+ onPress={onPress}
105
+ accessibilityRole="radio"
106
+ accessibilityState={{ selected: isActive }}
107
+ >
108
+ <View style={styles.row}>
109
+ <View style={[styles.rowInner, style]}>{children}</View>
110
+ <Icon
111
+ name="general.check"
112
+ style={styles.rowIconRight}
113
+ accessibilityElementsHidden
114
+ maxFontSizeMultiplier={2.5}
115
+ />
116
+ </View>
117
+ </PlatformPressable>
118
+ )
119
+ }
120
+
121
+ const styles = StyleSheet.create({
122
+ container: {
123
+ flex: 1,
124
+ },
125
+ section: {
126
+ paddingTop: 16,
127
+ },
128
+ sectionHeading: {
129
+ paddingHorizontal: 16,
130
+ paddingBottom: 4,
131
+ },
132
+ logo: {
133
+ width: 32,
134
+ height: 32,
135
+ borderRadius: 8,
136
+ },
137
+ })
138
+
139
+ const useRowStyles = ({ isActive = false }: { isActive?: boolean } = {}) => {
140
+ const theme = useTheme()
141
+
142
+ return StyleSheet.create({
143
+ pressable: {
144
+ paddingLeft: 16,
145
+ backgroundColor: theme.colors.surfaceColor100,
146
+ },
147
+ row: {
148
+ flexDirection: 'row',
149
+ alignItems: 'center',
150
+ justifyContent: 'space-between',
151
+ gap: 12,
152
+ borderBottomWidth: 1,
153
+ borderBottomColor: theme.colors.borderColorDefaultBase,
154
+ paddingVertical: 12,
155
+ paddingRight: 16,
156
+ },
157
+ rowInner: {
158
+ flexDirection: 'row',
159
+ alignItems: 'center',
160
+ gap: 12,
161
+ flexShrink: 1,
162
+ },
163
+ rowIconRight: {
164
+ fontSize: 16,
165
+ color: theme.colors.statusSuccessIcon,
166
+ opacity: isActive ? 1 : 0,
167
+ },
168
+ })
169
+ }
@@ -0,0 +1,14 @@
1
+ declare module '*.png' {
2
+ const value: import('react-native').ImageSourcePropType
3
+ export default value
4
+ }
5
+
6
+ declare module '*.jpg' {
7
+ const value: import('react-native').ImageSourcePropType
8
+ export default value
9
+ }
10
+
11
+ declare module '*.jpeg' {
12
+ const value: import('react-native').ImageSourcePropType
13
+ export default value
14
+ }