@planningcenter/chat-react-native 3.32.1-rc.1 → 3.33.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 (171) hide show
  1. package/build/components/conversation/message_form.d.ts.map +1 -1
  2. package/build/components/conversation/message_form.js +22 -1
  3. package/build/components/conversation/message_form.js.map +1 -1
  4. package/build/components/display/emoji_avatar.d.ts.map +1 -1
  5. package/build/components/display/emoji_avatar.js +2 -0
  6. package/build/components/display/emoji_avatar.js.map +1 -1
  7. package/build/components/display/icon_avatar.d.ts.map +1 -1
  8. package/build/components/display/icon_avatar.js +2 -0
  9. package/build/components/display/icon_avatar.js.map +1 -1
  10. package/build/components/display/utils/avatar_gradient_colors.d.ts +3 -0
  11. package/build/components/display/utils/avatar_gradient_colors.d.ts.map +1 -1
  12. package/build/components/display/utils/avatar_gradient_colors.js +8 -3
  13. package/build/components/display/utils/avatar_gradient_colors.js.map +1 -1
  14. package/build/components/primitive/avatar_primitive.d.ts +3 -1
  15. package/build/components/primitive/avatar_primitive.d.ts.map +1 -1
  16. package/build/components/primitive/avatar_primitive.js +10 -2
  17. package/build/components/primitive/avatar_primitive.js.map +1 -1
  18. package/build/hooks/attachments/fallback_chat_configuration.d.ts +4 -0
  19. package/build/hooks/attachments/fallback_chat_configuration.d.ts.map +1 -0
  20. package/build/hooks/attachments/fallback_chat_configuration.js +59 -0
  21. package/build/hooks/attachments/fallback_chat_configuration.js.map +1 -0
  22. package/build/hooks/groups/use_groups_conversation_create.d.ts.map +1 -1
  23. package/build/hooks/groups/use_groups_conversation_create.js +1 -1
  24. package/build/hooks/groups/use_groups_conversation_create.js.map +1 -1
  25. package/build/hooks/services/use_find_or_create_services_conversation.d.ts +43 -11
  26. package/build/hooks/services/use_find_or_create_services_conversation.d.ts.map +1 -1
  27. package/build/hooks/services/use_find_or_create_services_conversation.js +5 -5
  28. package/build/hooks/services/use_find_or_create_services_conversation.js.map +1 -1
  29. package/build/hooks/use_attachment_uploader.d.ts.map +1 -1
  30. package/build/hooks/use_attachment_uploader.js +39 -14
  31. package/build/hooks/use_attachment_uploader.js.map +1 -1
  32. package/build/hooks/use_chat_configuration.d.ts +6 -0
  33. package/build/hooks/use_chat_configuration.d.ts.map +1 -0
  34. package/build/hooks/use_chat_configuration.js +41 -0
  35. package/build/hooks/use_chat_configuration.js.map +1 -0
  36. package/build/hooks/use_conversation_avatar_update.d.ts +26 -0
  37. package/build/hooks/use_conversation_avatar_update.d.ts.map +1 -0
  38. package/build/hooks/use_conversation_avatar_update.js +130 -0
  39. package/build/hooks/use_conversation_avatar_update.js.map +1 -0
  40. package/build/hooks/use_features.d.ts +1 -0
  41. package/build/hooks/use_features.d.ts.map +1 -1
  42. package/build/hooks/use_features.js +1 -0
  43. package/build/hooks/use_features.js.map +1 -1
  44. package/build/navigation/index.d.ts +16 -0
  45. package/build/navigation/index.d.ts.map +1 -1
  46. package/build/navigation/index.js +9 -0
  47. package/build/navigation/index.js.map +1 -1
  48. package/build/screens/avatar_picker/avatar_picker_screen.d.ts +12 -0
  49. package/build/screens/avatar_picker/avatar_picker_screen.d.ts.map +1 -0
  50. package/build/screens/avatar_picker/avatar_picker_screen.js +193 -0
  51. package/build/screens/avatar_picker/avatar_picker_screen.js.map +1 -0
  52. package/build/screens/avatar_picker/avatar_picker_state.d.ts +38 -0
  53. package/build/screens/avatar_picker/avatar_picker_state.d.ts.map +1 -0
  54. package/build/screens/avatar_picker/avatar_picker_state.js +101 -0
  55. package/build/screens/avatar_picker/avatar_picker_state.js.map +1 -0
  56. package/build/screens/avatar_picker/avatar_preview.d.ts +9 -0
  57. package/build/screens/avatar_picker/avatar_preview.d.ts.map +1 -0
  58. package/build/screens/avatar_picker/avatar_preview.js +39 -0
  59. package/build/screens/avatar_picker/avatar_preview.js.map +1 -0
  60. package/build/screens/avatar_picker/color_picker.d.ts +9 -0
  61. package/build/screens/avatar_picker/color_picker.d.ts.map +1 -0
  62. package/build/screens/avatar_picker/color_picker.js +53 -0
  63. package/build/screens/avatar_picker/color_picker.js.map +1 -0
  64. package/build/screens/avatar_picker/constants.d.ts +3 -0
  65. package/build/screens/avatar_picker/constants.d.ts.map +1 -0
  66. package/build/screens/avatar_picker/constants.js +53 -0
  67. package/build/screens/avatar_picker/constants.js.map +1 -0
  68. package/build/screens/avatar_picker/emoji_tab.d.ts +7 -0
  69. package/build/screens/avatar_picker/emoji_tab.d.ts.map +1 -0
  70. package/build/screens/avatar_picker/emoji_tab.js +55 -0
  71. package/build/screens/avatar_picker/emoji_tab.js.map +1 -0
  72. package/build/screens/avatar_picker/icon_grid.d.ts +8 -0
  73. package/build/screens/avatar_picker/icon_grid.d.ts.map +1 -0
  74. package/build/screens/avatar_picker/icon_grid.js +48 -0
  75. package/build/screens/avatar_picker/icon_grid.js.map +1 -0
  76. package/build/screens/avatar_picker/upload_tab.d.ts +9 -0
  77. package/build/screens/avatar_picker/upload_tab.d.ts.map +1 -0
  78. package/build/screens/avatar_picker/upload_tab.js +39 -0
  79. package/build/screens/avatar_picker/upload_tab.js.map +1 -0
  80. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  81. package/build/screens/conversation_details_screen.js +37 -1
  82. package/build/screens/conversation_details_screen.js.map +1 -1
  83. package/build/screens/conversation_new/components/avatar_selection_row.d.ts +12 -0
  84. package/build/screens/conversation_new/components/avatar_selection_row.d.ts.map +1 -0
  85. package/build/screens/conversation_new/components/avatar_selection_row.js +60 -0
  86. package/build/screens/conversation_new/components/avatar_selection_row.js.map +1 -0
  87. package/build/screens/conversation_new/components/gender_filter_toggle.d.ts.map +1 -1
  88. package/build/screens/conversation_new/components/gender_filter_toggle.js +3 -9
  89. package/build/screens/conversation_new/components/gender_filter_toggle.js.map +1 -1
  90. package/build/screens/conversation_new/components/groups_form.d.ts +3 -1
  91. package/build/screens/conversation_new/components/groups_form.d.ts.map +1 -1
  92. package/build/screens/conversation_new/components/groups_form.js +22 -8
  93. package/build/screens/conversation_new/components/groups_form.js.map +1 -1
  94. package/build/screens/conversation_new/components/services_form.d.ts +3 -1
  95. package/build/screens/conversation_new/components/services_form.d.ts.map +1 -1
  96. package/build/screens/conversation_new/components/services_form.js +22 -8
  97. package/build/screens/conversation_new/components/services_form.js.map +1 -1
  98. package/build/screens/conversation_new/conversation_new_screen.d.ts +2 -0
  99. package/build/screens/conversation_new/conversation_new_screen.d.ts.map +1 -1
  100. package/build/screens/conversation_new/conversation_new_screen.js +3 -3
  101. package/build/screens/conversation_new/conversation_new_screen.js.map +1 -1
  102. package/build/screens/team_conversation_screen.d.ts.map +1 -1
  103. package/build/screens/team_conversation_screen.js +1 -1
  104. package/build/screens/team_conversation_screen.js.map +1 -1
  105. package/build/types/resources/chat_configuration_resource.d.ts +8 -0
  106. package/build/types/resources/chat_configuration_resource.d.ts.map +1 -0
  107. package/build/types/resources/chat_configuration_resource.js +2 -0
  108. package/build/types/resources/chat_configuration_resource.js.map +1 -0
  109. package/build/utils/native_adapters/configuration.d.ts +3 -0
  110. package/build/utils/native_adapters/configuration.d.ts.map +1 -1
  111. package/build/utils/native_adapters/configuration.js +8 -0
  112. package/build/utils/native_adapters/configuration.js.map +1 -1
  113. package/build/utils/native_adapters/document_picker.d.ts +21 -0
  114. package/build/utils/native_adapters/document_picker.d.ts.map +1 -0
  115. package/build/utils/native_adapters/document_picker.js +7 -0
  116. package/build/utils/native_adapters/document_picker.js.map +1 -0
  117. package/build/utils/native_adapters/image_picker.d.ts +7 -1
  118. package/build/utils/native_adapters/image_picker.d.ts.map +1 -1
  119. package/build/utils/native_adapters/image_picker.js.map +1 -1
  120. package/build/utils/native_adapters/index.d.ts +1 -0
  121. package/build/utils/native_adapters/index.d.ts.map +1 -1
  122. package/build/utils/native_adapters/index.js +1 -0
  123. package/build/utils/native_adapters/index.js.map +1 -1
  124. package/build/utils/request/get_chat_configuration.d.ts +10 -0
  125. package/build/utils/request/get_chat_configuration.d.ts.map +1 -0
  126. package/build/utils/request/get_chat_configuration.js +21 -0
  127. package/build/utils/request/get_chat_configuration.js.map +1 -0
  128. package/package.json +4 -3
  129. package/src/__tests__/hooks/use_attachment_uploader.test.tsx +219 -0
  130. package/src/__tests__/hooks/use_chat_configuration.test.tsx +80 -0
  131. package/src/__tests__/utils/native_adapters/configuration.ts +25 -1
  132. package/src/components/conversation/message_form.tsx +39 -1
  133. package/src/components/display/emoji_avatar.tsx +7 -2
  134. package/src/components/display/icon_avatar.tsx +7 -2
  135. package/src/components/display/utils/avatar_gradient_colors.ts +10 -3
  136. package/src/components/primitive/avatar_primitive.tsx +11 -2
  137. package/src/hooks/attachments/fallback_chat_configuration.ts +61 -0
  138. package/src/hooks/groups/use_groups_conversation_create.ts +2 -1
  139. package/src/hooks/services/use_find_or_create_services_conversation.ts +7 -7
  140. package/src/hooks/use_attachment_uploader.ts +39 -15
  141. package/src/hooks/use_chat_configuration.ts +54 -0
  142. package/src/hooks/use_conversation_avatar_update.ts +163 -0
  143. package/src/hooks/use_features.ts +1 -0
  144. package/src/navigation/index.tsx +13 -0
  145. package/src/screens/avatar_picker/__tests__/avatar_picker_state.test.ts +157 -0
  146. package/src/screens/avatar_picker/avatar_picker_screen.tsx +312 -0
  147. package/src/screens/avatar_picker/avatar_picker_state.ts +141 -0
  148. package/src/screens/avatar_picker/avatar_preview.tsx +46 -0
  149. package/src/screens/avatar_picker/color_picker.tsx +91 -0
  150. package/src/screens/avatar_picker/constants.ts +53 -0
  151. package/src/screens/avatar_picker/emoji_tab.tsx +76 -0
  152. package/src/screens/avatar_picker/icon_grid.tsx +81 -0
  153. package/src/screens/avatar_picker/upload_tab.tsx +62 -0
  154. package/src/screens/conversation_details_screen.tsx +60 -1
  155. package/src/screens/conversation_new/components/avatar_selection_row.tsx +82 -0
  156. package/src/screens/conversation_new/components/gender_filter_toggle.tsx +3 -9
  157. package/src/screens/conversation_new/components/groups_form.tsx +33 -6
  158. package/src/screens/conversation_new/components/services_form.tsx +37 -6
  159. package/src/screens/conversation_new/conversation_new_screen.tsx +17 -3
  160. package/src/screens/team_conversation_screen.tsx +2 -1
  161. package/src/types/resources/chat_configuration_resource.ts +11 -0
  162. package/src/utils/native_adapters/configuration.ts +10 -0
  163. package/src/utils/native_adapters/document_picker.ts +26 -0
  164. package/src/utils/native_adapters/image_picker.ts +8 -1
  165. package/src/utils/native_adapters/index.ts +1 -0
  166. package/src/utils/request/get_chat_configuration.ts +23 -0
  167. package/build/hooks/attachments/supported_extensions.d.ts +0 -2
  168. package/build/hooks/attachments/supported_extensions.d.ts.map +0 -1
  169. package/build/hooks/attachments/supported_extensions.js +0 -48
  170. package/build/hooks/attachments/supported_extensions.js.map +0 -1
  171. package/src/hooks/attachments/supported_extensions.ts +0 -47
@@ -0,0 +1,91 @@
1
+ import { PlatformPressable } from '@react-navigation/elements'
2
+ import React, { useCallback } from 'react'
3
+ import { FlatList, StyleSheet, useWindowDimensions } from 'react-native'
4
+ import LinearGradient from 'react-native-linear-gradient'
5
+ import {
6
+ COLOR_KEYS,
7
+ type CustomAvatarColorKey,
8
+ getAvatarGradientProps,
9
+ } from '../../components/display/utils/avatar_gradient_colors'
10
+ import { useTheme } from '../../hooks'
11
+ import { GRID_COLUMNS } from './constants'
12
+
13
+ interface ColorPickerProps {
14
+ selectedColor: string
15
+ onColorSelect: (color: CustomAvatarColorKey) => void
16
+ }
17
+
18
+ export function ColorPicker({ selectedColor, onColorSelect }: ColorPickerProps) {
19
+ const styles = useStyles()
20
+
21
+ const renderItem = useCallback(
22
+ ({ item }: { item: CustomAvatarColorKey }) => {
23
+ const gradientProps = getAvatarGradientProps(item)
24
+ const isSelected = item === selectedColor
25
+
26
+ return (
27
+ <PlatformPressable
28
+ onPress={() => onColorSelect(item)}
29
+ style={[styles.swatchOuter, isSelected && styles.swatchSelected]}
30
+ accessibilityRole="button"
31
+ accessibilityLabel={`${item.replace(/-/g, ' ')} color`}
32
+ accessibilityState={{ selected: isSelected }}
33
+ >
34
+ <LinearGradient {...gradientProps} style={styles.swatch} />
35
+ </PlatformPressable>
36
+ )
37
+ },
38
+ [selectedColor, onColorSelect, styles.swatch, styles.swatchOuter, styles.swatchSelected]
39
+ )
40
+
41
+ return (
42
+ <FlatList
43
+ data={COLOR_KEYS}
44
+ numColumns={GRID_COLUMNS}
45
+ keyExtractor={item => item}
46
+ renderItem={renderItem}
47
+ contentContainerStyle={styles.grid}
48
+ columnWrapperStyle={styles.row}
49
+ scrollEnabled={false}
50
+ />
51
+ )
52
+ }
53
+
54
+ const PADDING = 16
55
+ const GAP = 8
56
+
57
+ const useStyles = () => {
58
+ const { colors } = useTheme()
59
+ const { width: screenWidth } = useWindowDimensions()
60
+ const swatchSize = Math.floor(
61
+ (screenWidth - PADDING * 2 - (GRID_COLUMNS - 1) * GAP) / GRID_COLUMNS
62
+ )
63
+ const innerSize = swatchSize - 6
64
+
65
+ return StyleSheet.create({
66
+ grid: {
67
+ paddingHorizontal: PADDING,
68
+ paddingVertical: 12,
69
+ },
70
+ row: {
71
+ gap: GAP,
72
+ marginBottom: GAP,
73
+ },
74
+ swatchOuter: {
75
+ width: swatchSize,
76
+ height: swatchSize,
77
+ borderRadius: swatchSize / 2,
78
+ alignItems: 'center',
79
+ justifyContent: 'center',
80
+ },
81
+ swatchSelected: {
82
+ borderWidth: 3,
83
+ borderColor: colors.interaction,
84
+ },
85
+ swatch: {
86
+ width: innerSize,
87
+ height: innerSize,
88
+ borderRadius: innerSize / 2,
89
+ },
90
+ })
91
+ }
@@ -0,0 +1,53 @@
1
+ export const AVATAR_ICON_KEYS: string[] = [
2
+ // Church
3
+ 'book-bible',
4
+ 'church',
5
+ 'cross',
6
+ 'dove',
7
+ 'hands-heart',
8
+ 'person-rays',
9
+ 'praying-hands',
10
+ 'podium',
11
+ 'droplet',
12
+ 'person-drowning',
13
+ 'lighthouse',
14
+ // Services
15
+ 'guitar',
16
+ 'guitar-electric',
17
+ 'violin',
18
+ 'calendar-heart',
19
+ 'hand-heart',
20
+ 'hand-wave',
21
+ 'baby',
22
+ 'children',
23
+ 'mug-hot',
24
+ 'video-camera',
25
+ 'music',
26
+ 'piano-keyboard',
27
+ 'drum',
28
+ 'user-graduate',
29
+ 'comments-question',
30
+ 'presentation',
31
+ 'microphone-stand',
32
+ 'id-badge',
33
+ 'book-user',
34
+ 'hand-holding-heart',
35
+ 'guitars',
36
+ 'amp-guitar',
37
+ 'school-flag',
38
+ 'shield-check',
39
+ 'projector',
40
+ // Groups
41
+ 'people',
42
+ 'people-group',
43
+ 'person-dress',
44
+ 'person',
45
+ 'seedling',
46
+ 'comments-alt',
47
+ 'globe',
48
+ 'book-open-reader',
49
+ 'running',
50
+ 'leaf',
51
+ ]
52
+
53
+ export const GRID_COLUMNS = 6
@@ -0,0 +1,76 @@
1
+ import React, { useCallback } from 'react'
2
+ import { StyleSheet, View } from 'react-native'
3
+ import { EmojiKeyboard, type EmojiType } from 'rn-emoji-keyboard'
4
+ import { useTheme } from '../../hooks'
5
+
6
+ interface EmojiTabProps {
7
+ onEmojiSelect: (emoji: string) => void
8
+ }
9
+
10
+ export function EmojiTab({ onEmojiSelect }: EmojiTabProps) {
11
+ const styles = useStyles()
12
+ const { colors } = useTheme()
13
+
14
+ const handleEmojiSelected = useCallback(
15
+ (emojiObject: EmojiType) => {
16
+ onEmojiSelect(emojiObject.emoji)
17
+ },
18
+ [onEmojiSelect]
19
+ )
20
+
21
+ const emojiTheme = {
22
+ container: colors.fillColorNeutral100Inverted,
23
+ header: colors.textColorDefaultSecondary,
24
+ knob: colors.fillColorNeutral040,
25
+ skinTonesContainer: colors.fillColorNeutral060,
26
+ category: {
27
+ icon: colors.textColorDefaultSecondary,
28
+ iconActive: colors.interaction,
29
+ container: colors.fillColorNeutral100Inverted,
30
+ containerActive: colors.fillColorNeutral080,
31
+ },
32
+ search: {
33
+ background: colors.fillColorNeutral080,
34
+ text: colors.textColorDefaultPrimary,
35
+ placeholder: colors.textColorDefaultSecondary,
36
+ icon: colors.textColorDefaultSecondary,
37
+ },
38
+ emoji: {
39
+ selected: colors.fillColorNeutral080,
40
+ },
41
+ }
42
+
43
+ return (
44
+ <View style={styles.container}>
45
+ <EmojiKeyboard
46
+ categoryPosition="top"
47
+ onEmojiSelected={handleEmojiSelected}
48
+ enableSearchBar
49
+ enableRecentlyUsed
50
+ theme={emojiTheme}
51
+ styles={{
52
+ container: { borderRadius: 0 },
53
+ category: {
54
+ container: {
55
+ borderRadius: 0,
56
+ marginTop: -6,
57
+ borderBottomWidth: 1,
58
+ borderBottomColor: colors.borderColorDefaultDim,
59
+ },
60
+ },
61
+ }}
62
+ />
63
+ </View>
64
+ )
65
+ }
66
+
67
+ const useStyles = () => {
68
+ const { colors } = useTheme()
69
+
70
+ return StyleSheet.create({
71
+ container: {
72
+ flex: 1,
73
+ backgroundColor: colors.fillColorNeutral100Inverted,
74
+ },
75
+ })
76
+ }
@@ -0,0 +1,81 @@
1
+ import type { IconName } from '@fortawesome/fontawesome-svg-core'
2
+ import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'
3
+ import { PlatformPressable } from '@react-navigation/elements'
4
+ import React, { useCallback } from 'react'
5
+ import { FlatList, StyleSheet, useWindowDimensions } from 'react-native'
6
+ import { useTheme } from '../../hooks'
7
+ import { AVATAR_ICON_KEYS, GRID_COLUMNS } from './constants'
8
+
9
+ interface IconGridProps {
10
+ selectedIconKey: string | null
11
+ onIconSelect: (iconKey: string) => void
12
+ }
13
+
14
+ export function IconGrid({ selectedIconKey, onIconSelect }: IconGridProps) {
15
+ const styles = useStyles()
16
+
17
+ const renderItem = useCallback(
18
+ ({ item }: { item: string }) => {
19
+ const isSelected = item === selectedIconKey
20
+
21
+ return (
22
+ <PlatformPressable
23
+ onPress={() => onIconSelect(item)}
24
+ style={[styles.cell, isSelected && styles.cellSelected]}
25
+ accessibilityRole="button"
26
+ accessibilityLabel={`${item.replace(/-/g, ' ')} icon`}
27
+ accessibilityState={{ selected: isSelected }}
28
+ >
29
+ <FontAwesomeIcon icon={['fas', item as IconName]} size={20} color="white" />
30
+ </PlatformPressable>
31
+ )
32
+ },
33
+ [selectedIconKey, onIconSelect, styles.cell, styles.cellSelected]
34
+ )
35
+
36
+ return (
37
+ <FlatList
38
+ data={AVATAR_ICON_KEYS}
39
+ numColumns={GRID_COLUMNS}
40
+ keyExtractor={item => item}
41
+ renderItem={renderItem}
42
+ contentContainerStyle={styles.grid}
43
+ columnWrapperStyle={styles.row}
44
+ style={styles.list}
45
+ />
46
+ )
47
+ }
48
+
49
+ const PADDING = 16
50
+ const GAP = 8
51
+
52
+ const useStyles = () => {
53
+ const { colors } = useTheme()
54
+ const { width: screenWidth } = useWindowDimensions()
55
+ const cellSize = Math.floor((screenWidth - PADDING * 2 - (GRID_COLUMNS - 1) * GAP) / GRID_COLUMNS)
56
+
57
+ return StyleSheet.create({
58
+ list: {
59
+ flex: 1,
60
+ },
61
+ grid: {
62
+ padding: PADDING,
63
+ },
64
+ row: {
65
+ gap: GAP,
66
+ marginBottom: GAP,
67
+ },
68
+ cell: {
69
+ width: cellSize,
70
+ height: cellSize,
71
+ borderRadius: cellSize / 2,
72
+ backgroundColor: colors.fillColorNeutral040,
73
+ alignItems: 'center',
74
+ justifyContent: 'center',
75
+ },
76
+ cellSelected: {
77
+ borderWidth: 3,
78
+ borderColor: colors.interaction,
79
+ },
80
+ })
81
+ }
@@ -0,0 +1,62 @@
1
+ import React, { useCallback } from 'react'
2
+ import { Alert, StyleSheet, View } from 'react-native'
3
+ import { Button } from '../../components'
4
+ import { useTheme } from '../../hooks'
5
+ import { ImagePicker } from '../../utils/native_adapters'
6
+ import type { ImagePickerAsset } from '../../utils/native_adapters/image_picker'
7
+
8
+ const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
9
+
10
+ interface UploadTabProps {
11
+ imagePreviewUri: string | null
12
+ onImageSelect: (asset: ImagePickerAsset) => void
13
+ }
14
+
15
+ export function UploadTab({ imagePreviewUri, onImageSelect }: UploadTabProps) {
16
+ const styles = useStyles()
17
+
18
+ const pickImage = useCallback(async () => {
19
+ const result = await ImagePicker.openImageLibraryAsync({
20
+ mediaTypes: ['images'],
21
+ allowsEditing: true,
22
+ allowsMultipleSelection: false,
23
+ })
24
+
25
+ if (result.canceled || !result.assets?.[0]) return
26
+
27
+ const asset = result.assets[0]
28
+
29
+ if (asset.fileSize && asset.fileSize > MAX_FILE_SIZE) {
30
+ Alert.alert('Image too large', 'Please choose an image under 10MB.')
31
+ return
32
+ }
33
+
34
+ onImageSelect(asset)
35
+ }, [onImageSelect])
36
+
37
+ return (
38
+ <View style={styles.container}>
39
+ <Button
40
+ title={imagePreviewUri ? 'Change photo' : 'Choose photo'}
41
+ iconNameLeft="general.image"
42
+ onPress={pickImage}
43
+ variant="outline"
44
+ appearance="interaction"
45
+ size="md"
46
+ />
47
+ </View>
48
+ )
49
+ }
50
+
51
+ const useStyles = () => {
52
+ const { colors } = useTheme()
53
+
54
+ return StyleSheet.create({
55
+ container: {
56
+ flex: 1,
57
+ alignItems: 'center',
58
+ justifyContent: 'center',
59
+ backgroundColor: colors.fillColorNeutral100Inverted,
60
+ },
61
+ })
62
+ }
@@ -26,6 +26,7 @@ import {
26
26
  } from 'react-native'
27
27
  import {
28
28
  Badge,
29
+ Button,
29
30
  ChildNotice,
30
31
  Heading,
31
32
  Icon,
@@ -35,6 +36,7 @@ import {
35
36
  TextButton,
36
37
  type TextStyle,
37
38
  } from '../components'
39
+ import { ConversationAvatar } from '../components/display/conversation_avatar'
38
40
  import { HeaderTextButton } from '../components/display/platform_modal_header_buttons'
39
41
  import { ButtonAppearanceUnion } from '../components/display/utils/button_colors'
40
42
  import { useSuspensePaginator, useTheme } from '../hooks'
@@ -46,7 +48,7 @@ import {
46
48
  useConversationUpdate,
47
49
  } from '../hooks/use_conversation'
48
50
  import { availableFeatures, useFeatures } from '../hooks/use_features'
49
- import { MemberResource, isDefined } from '../types'
51
+ import { type ConversationResource, MemberResource, isDefined } from '../types'
50
52
  import { GroupResource } from '../types/resources/group_resource'
51
53
  import { genderDisplayLabel } from '../utils/gender_display_label'
52
54
  import { tokens } from '../vendor/tapestry/tokens'
@@ -102,6 +104,7 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
102
104
  const { mutate: deleteConversation } = useConversationDelete(route.params)
103
105
  const { featureEnabled } = useFeatures()
104
106
  const granularNotificationsEnabled = featureEnabled(availableFeatures.granular_notifications_ui)
107
+ const customAvatarsEnabled = featureEnabled(availableFeatures.custom_conversation_avatars)
105
108
  const showGenderFilter =
106
109
  featureEnabled(availableFeatures.gender_specific_conversations) && !!conversation.genderOption
107
110
  const genderLabel = conversation.genderOption ? genderDisplayLabel(conversation.genderOption) : ''
@@ -234,6 +237,25 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
234
237
  }, [HeaderRight, HeaderTitle, navigation])
235
238
 
236
239
  const listData = [
240
+ {
241
+ type: canUpdate && customAvatarsEnabled ? SectionTypes.view : SectionTypes.hidden,
242
+ data: {
243
+ children: (
244
+ <AvatarCard
245
+ conversation={conversation}
246
+ onPress={() => navigation.navigate('AvatarPicker', { conversation_id })}
247
+ />
248
+ ),
249
+ },
250
+ sectionOuterStyle: styles.sectionOuterAvatarCard,
251
+ sectionInnerStyle: styles.sectionInnerAvatarCard,
252
+ },
253
+ {
254
+ type: SectionTypes.header,
255
+ data: { title: 'Basic info' },
256
+ showBottomBorder: true,
257
+ sectionInnerStyle: styles.sectionInnerHeaderWithBottomBorder,
258
+ },
237
259
  {
238
260
  type: SectionTypes.view,
239
261
  data: {
@@ -595,6 +617,30 @@ function NavigableSettingRow({ title, subtitle, onPress }: NavigableSettingRowPr
595
617
  )
596
618
  }
597
619
 
620
+ function AvatarCard({
621
+ conversation,
622
+ onPress,
623
+ }: {
624
+ conversation: ConversationResource
625
+ onPress: () => void
626
+ }) {
627
+ const styles = useStyles()
628
+
629
+ return (
630
+ <View style={styles.avatarCard}>
631
+ <ConversationAvatar conversation={conversation} size="2xl" />
632
+ <Button
633
+ title="Update avatar"
634
+ iconNameLeft="general.pencil"
635
+ onPress={onPress}
636
+ variant="outline"
637
+ appearance="interaction"
638
+ size="sm"
639
+ />
640
+ </View>
641
+ )
642
+ }
643
+
598
644
  function TeamsGroup({ teams }: { teams: GroupResource[] }) {
599
645
  const styles = useStyles()
600
646
 
@@ -725,6 +771,19 @@ const useStyles = ({ isStart, isEnd }: { isStart?: boolean; isEnd?: boolean } =
725
771
  navigableSettingChevron: {
726
772
  color: colors.iconColorDefaultDisabled,
727
773
  },
774
+ avatarCard: {
775
+ alignItems: 'center',
776
+ gap: 16,
777
+ },
778
+ sectionOuterAvatarCard: {
779
+ paddingLeft: 0,
780
+ },
781
+ sectionInnerAvatarCard: {
782
+ paddingTop: 24,
783
+ paddingBottom: 24,
784
+ paddingHorizontal: 16,
785
+ alignItems: 'center',
786
+ },
728
787
  teamGroup: {
729
788
  flexDirection: 'row',
730
789
  gap: 4,
@@ -0,0 +1,82 @@
1
+ import { useNavigation, useRoute } from '@react-navigation/native'
2
+ import React from 'react'
3
+ import { Pressable, StyleSheet, View } from 'react-native'
4
+ import { Icon, Text } from '../../../components'
5
+ import { EmojiAvatar } from '../../../components/display/emoji_avatar'
6
+ import { IconAvatar } from '../../../components/display/icon_avatar'
7
+ import AvatarPrimitive from '../../../components/primitive/avatar_primitive'
8
+ import { useTheme } from '../../../hooks'
9
+ import type { AvatarUpdatePayload } from '../../../hooks/use_conversation_avatar_update'
10
+
11
+ interface AvatarSelectionRowProps {
12
+ avatarSelection?: AvatarUpdatePayload
13
+ }
14
+
15
+ export function AvatarSelectionRow({ avatarSelection }: AvatarSelectionRowProps) {
16
+ const styles = useStyles()
17
+ const navigation = useNavigation()
18
+ const route = useRoute()
19
+
20
+ return (
21
+ <Pressable
22
+ style={styles.avatarSection}
23
+ onPress={() =>
24
+ navigation.navigate('AvatarPicker', {
25
+ source_params: route.params as Record<string, unknown>,
26
+ ...(avatarSelection && { avatar_selection: avatarSelection }),
27
+ })
28
+ }
29
+ >
30
+ <Text style={styles.avatarLabel}>Customize avatar</Text>
31
+ <View style={styles.avatarSectionTrailing}>
32
+ <AvatarSelectionPreview avatarSelection={avatarSelection} />
33
+ <Icon name="general.rightChevron" size={12} />
34
+ </View>
35
+ </Pressable>
36
+ )
37
+ }
38
+
39
+ interface AvatarSelectionPreviewProps {
40
+ avatarSelection?: AvatarUpdatePayload
41
+ }
42
+
43
+ export function AvatarSelectionPreview({ avatarSelection }: AvatarSelectionPreviewProps) {
44
+ if (!avatarSelection || avatarSelection.kind === 'clear') return null
45
+
46
+ switch (avatarSelection.kind) {
47
+ case 'icon':
48
+ return <IconAvatar iconKey={avatarSelection.key} color={avatarSelection.color} size="md" />
49
+ case 'emoji':
50
+ return <EmojiAvatar emoji={avatarSelection.key} color={avatarSelection.color} size="md" />
51
+ case 'image':
52
+ return (
53
+ <AvatarPrimitive.Root size="md">
54
+ <AvatarPrimitive.Mask>
55
+ <AvatarPrimitive.Image sourceUri={avatarSelection.imageAsset.uri} />
56
+ </AvatarPrimitive.Mask>
57
+ </AvatarPrimitive.Root>
58
+ )
59
+ }
60
+ }
61
+
62
+ const useStyles = () => {
63
+ const { colors } = useTheme()
64
+
65
+ return StyleSheet.create({
66
+ avatarSection: {
67
+ padding: 16,
68
+ flexDirection: 'row',
69
+ alignItems: 'center',
70
+ justifyContent: 'space-between',
71
+ },
72
+ avatarSectionTrailing: {
73
+ flexDirection: 'row',
74
+ alignItems: 'center',
75
+ gap: 12,
76
+ },
77
+ avatarLabel: {
78
+ fontSize: 18,
79
+ color: colors.textColorDefaultPrimary,
80
+ },
81
+ })
82
+ }
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
  import { ActivityIndicator, StyleSheet, View } from 'react-native'
3
- import { Heading, Switch, Text, TextInlineButton } from '../../../components'
3
+ import { Heading, Switch, Text } from '../../../components'
4
4
  import { useTheme } from '../../../hooks'
5
5
  import { genderDisplayLabel } from '../../../utils/gender_display_label'
6
6
 
@@ -26,10 +26,7 @@ function FilterableGenderContent({ genderValue, enabled, onToggle }: FilterableG
26
26
  />
27
27
  </View>
28
28
  <Text style={{ color: colors.textColorDefaultSecondary }}>
29
- Filter limited to your profile's set gender.{' '}
30
- <TextInlineButton nativeID="gender-filter-learn-more" onPress={() => {}}>
31
- Learn more.
32
- </TextInlineButton>
29
+ Filter limited to your profile's set gender.
33
30
  </Text>
34
31
  </>
35
32
  )
@@ -45,10 +42,7 @@ function NoGenderContent({ isFetching }: { isFetching: boolean }) {
45
42
  <ActivityIndicator size="small" />
46
43
  ) : (
47
44
  <Text style={{ color: colors.textColorDefaultSecondary }}>
48
- Set a gender in your Church Center profile to enable gender filtering.{' '}
49
- <TextInlineButton nativeID="gender-filter-learn-more" onPress={() => {}}>
50
- Learn more.
51
- </TextInlineButton>
45
+ Set a gender in your Church Center profile to enable gender filtering.
52
46
  </Text>
53
47
  )}
54
48
  </>