@planningcenter/chat-react-native 3.31.0-rc.2 → 3.31.0-rc.4

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 (60) hide show
  1. package/build/components/conversations/conversation_preview.d.ts.map +1 -1
  2. package/build/components/conversations/conversation_preview.js +3 -5
  3. package/build/components/conversations/conversation_preview.js.map +1 -1
  4. package/build/components/display/conversation_avatar.d.ts +16 -0
  5. package/build/components/display/conversation_avatar.d.ts.map +1 -0
  6. package/build/components/display/conversation_avatar.js +43 -0
  7. package/build/components/display/conversation_avatar.js.map +1 -0
  8. package/build/components/display/emoji_avatar.d.ts +10 -0
  9. package/build/components/display/emoji_avatar.d.ts.map +1 -0
  10. package/build/components/display/emoji_avatar.js +37 -0
  11. package/build/components/display/emoji_avatar.js.map +1 -0
  12. package/build/components/display/icon_avatar.d.ts +10 -0
  13. package/build/components/display/icon_avatar.d.ts.map +1 -0
  14. package/build/components/display/icon_avatar.js +36 -0
  15. package/build/components/display/icon_avatar.js.map +1 -0
  16. package/build/components/display/index.d.ts +3 -0
  17. package/build/components/display/index.d.ts.map +1 -1
  18. package/build/components/display/index.js +3 -0
  19. package/build/components/display/index.js.map +1 -1
  20. package/build/components/display/utils/avatar_gradient_colors.d.ts +14 -0
  21. package/build/components/display/utils/avatar_gradient_colors.d.ts.map +1 -0
  22. package/build/components/display/utils/avatar_gradient_colors.js +49 -0
  23. package/build/components/display/utils/avatar_gradient_colors.js.map +1 -0
  24. package/build/hooks/use_conversation.d.ts.map +1 -1
  25. package/build/hooks/use_conversation.js +4 -0
  26. package/build/hooks/use_conversation.js.map +1 -1
  27. package/build/hooks/use_new_conversation_from_filter.d.ts +12 -0
  28. package/build/hooks/use_new_conversation_from_filter.d.ts.map +1 -0
  29. package/build/hooks/use_new_conversation_from_filter.js +47 -0
  30. package/build/hooks/use_new_conversation_from_filter.js.map +1 -0
  31. package/build/screens/conversation_select_type_screen.d.ts +2 -0
  32. package/build/screens/conversation_select_type_screen.d.ts.map +1 -1
  33. package/build/screens/conversation_select_type_screen.js +39 -4
  34. package/build/screens/conversation_select_type_screen.js.map +1 -1
  35. package/build/screens/conversations/components/list_header_component.d.ts.map +1 -1
  36. package/build/screens/conversations/components/list_header_component.js +8 -3
  37. package/build/screens/conversations/components/list_header_component.js.map +1 -1
  38. package/build/screens/design_system_screen.d.ts.map +1 -1
  39. package/build/screens/design_system_screen.js +47 -1
  40. package/build/screens/design_system_screen.js.map +1 -1
  41. package/build/types/resources/conversation.d.ts +4 -0
  42. package/build/types/resources/conversation.d.ts.map +1 -1
  43. package/build/types/resources/conversation.js.map +1 -1
  44. package/build/utils/request/conversation.d.ts.map +1 -1
  45. package/build/utils/request/conversation.js +4 -0
  46. package/build/utils/request/conversation.js.map +1 -1
  47. package/package.json +2 -2
  48. package/src/components/conversations/conversation_preview.tsx +3 -7
  49. package/src/components/display/conversation_avatar.tsx +90 -0
  50. package/src/components/display/emoji_avatar.tsx +48 -0
  51. package/src/components/display/icon_avatar.tsx +52 -0
  52. package/src/components/display/index.ts +3 -0
  53. package/src/components/display/utils/avatar_gradient_colors.ts +87 -0
  54. package/src/hooks/use_conversation.ts +4 -0
  55. package/src/hooks/use_new_conversation_from_filter.ts +65 -0
  56. package/src/screens/conversation_select_type_screen.tsx +61 -12
  57. package/src/screens/conversations/components/list_header_component.tsx +11 -3
  58. package/src/screens/design_system_screen.tsx +66 -0
  59. package/src/types/resources/conversation.ts +4 -0
  60. package/src/utils/request/conversation.ts +4 -0
@@ -0,0 +1,90 @@
1
+ import React from 'react'
2
+ import type { ViewStyle } from 'react-native'
3
+ import type { ConversationResource } from '../../types'
4
+ import AvatarPrimitive, { type AvatarRootProps } from '../primitive/avatar_primitive'
5
+ import { AvatarGroup } from './avatar_group'
6
+ import { EmojiAvatar } from './emoji_avatar'
7
+ import { type IconString } from './icon'
8
+ import { IconAvatar } from './icon_avatar'
9
+
10
+ type ConversationAvatarData = Pick<
11
+ ConversationResource,
12
+ | 'customAvatarType'
13
+ | 'customAvatarKey'
14
+ | 'customAvatarColor'
15
+ | 'customAvatarImageUrl'
16
+ | 'previewAvatarUrls'
17
+ >
18
+
19
+ type ResolvedAvatar =
20
+ | { kind: 'image'; url: string }
21
+ | { kind: 'icon'; iconKey: string; color: string | null }
22
+ | { kind: 'emoji'; emoji: string; color: string | null }
23
+ | { kind: 'group'; sources: string[] | undefined }
24
+
25
+ function resolveAvatar(conversation: ConversationAvatarData): ResolvedAvatar {
26
+ if (conversation.customAvatarType === 'image' && conversation.customAvatarImageUrl) {
27
+ return { kind: 'image', url: conversation.customAvatarImageUrl }
28
+ }
29
+
30
+ if (conversation.customAvatarType === 'icon' && conversation.customAvatarKey) {
31
+ return {
32
+ kind: 'icon',
33
+ iconKey: conversation.customAvatarKey,
34
+ color: conversation.customAvatarColor ?? null,
35
+ }
36
+ }
37
+
38
+ if (conversation.customAvatarType === 'emoji' && conversation.customAvatarKey) {
39
+ return {
40
+ kind: 'emoji',
41
+ emoji: conversation.customAvatarKey,
42
+ color: conversation.customAvatarColor ?? null,
43
+ }
44
+ }
45
+
46
+ return { kind: 'group', sources: conversation.previewAvatarUrls }
47
+ }
48
+
49
+ interface ConversationAvatarProps {
50
+ conversation: ConversationAvatarData
51
+ size?: AvatarRootProps['size']
52
+ showFallback?: boolean
53
+ fallbackIconName?: IconString
54
+ style?: ViewStyle
55
+ }
56
+
57
+ export function ConversationAvatar({
58
+ conversation,
59
+ size = 'lg',
60
+ showFallback = false,
61
+ fallbackIconName = 'general.person',
62
+ style,
63
+ }: ConversationAvatarProps) {
64
+ const avatar = resolveAvatar(conversation)
65
+
66
+ switch (avatar.kind) {
67
+ case 'image':
68
+ return (
69
+ <AvatarPrimitive.Root size={size} style={style}>
70
+ <AvatarPrimitive.Mask>
71
+ <AvatarPrimitive.Image sourceUri={avatar.url} />
72
+ </AvatarPrimitive.Mask>
73
+ </AvatarPrimitive.Root>
74
+ )
75
+ case 'icon':
76
+ return <IconAvatar iconKey={avatar.iconKey} color={avatar.color} size={size} />
77
+ case 'emoji':
78
+ return <EmojiAvatar emoji={avatar.emoji} color={avatar.color} size={size} />
79
+ case 'group':
80
+ return (
81
+ <AvatarGroup
82
+ sourceUris={avatar.sources || []}
83
+ size={size}
84
+ showFallback={showFallback || !avatar.sources || avatar.sources.length === 0}
85
+ fallbackIconName={fallbackIconName}
86
+ style={style}
87
+ />
88
+ )
89
+ }
90
+ }
@@ -0,0 +1,48 @@
1
+ import React from 'react'
2
+ import { StyleSheet, Text, View } from 'react-native'
3
+ import LinearGradient from 'react-native-linear-gradient'
4
+ import AvatarPrimitive, { type AvatarRootProps } from '../primitive/avatar_primitive'
5
+ import { getAvatarGradientProps } from './utils/avatar_gradient_colors'
6
+
7
+ const EMOJI_SIZE: Record<string, number> = {
8
+ xs: 8,
9
+ sm: 10,
10
+ md: 14,
11
+ lg: 20,
12
+ }
13
+
14
+ interface EmojiAvatarProps {
15
+ emoji: string
16
+ color?: string | null
17
+ size?: AvatarRootProps['size']
18
+ }
19
+
20
+ export function EmojiAvatar({ emoji, color, size = 'lg' }: EmojiAvatarProps) {
21
+ const gradientProps = getAvatarGradientProps(color)
22
+
23
+ return (
24
+ <AvatarPrimitive.Root size={size}>
25
+ <AvatarPrimitive.Mask>
26
+ <LinearGradient {...gradientProps} style={styles.gradientFill}>
27
+ <View style={styles.contentContainer}>
28
+ <Text allowFontScaling={false} style={{ fontSize: EMOJI_SIZE[size] }}>
29
+ {emoji}
30
+ </Text>
31
+ </View>
32
+ </LinearGradient>
33
+ </AvatarPrimitive.Mask>
34
+ </AvatarPrimitive.Root>
35
+ )
36
+ }
37
+
38
+ const styles = StyleSheet.create({
39
+ gradientFill: {
40
+ width: '100%',
41
+ height: '100%',
42
+ },
43
+ contentContainer: {
44
+ flex: 1,
45
+ alignItems: 'center',
46
+ justifyContent: 'center',
47
+ },
48
+ })
@@ -0,0 +1,52 @@
1
+ import type { IconName } from '@fortawesome/fontawesome-svg-core'
2
+ import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'
3
+ import React from 'react'
4
+ import { StyleSheet, View } from 'react-native'
5
+ import LinearGradient from 'react-native-linear-gradient'
6
+ import AvatarPrimitive, { type AvatarRootProps } from '../primitive/avatar_primitive'
7
+ import { getAvatarGradientProps } from './utils/avatar_gradient_colors'
8
+
9
+ const ICON_SIZE: Record<string, number> = {
10
+ xs: 10,
11
+ sm: 12,
12
+ md: 16,
13
+ lg: 20,
14
+ }
15
+
16
+ interface IconAvatarProps {
17
+ iconKey: string
18
+ color?: string | null
19
+ size?: AvatarRootProps['size']
20
+ }
21
+
22
+ export function IconAvatar({ iconKey, color, size = 'lg' }: IconAvatarProps) {
23
+ const gradientProps = getAvatarGradientProps(color)
24
+
25
+ return (
26
+ <AvatarPrimitive.Root size={size}>
27
+ <AvatarPrimitive.Mask>
28
+ <LinearGradient {...gradientProps} style={styles.gradientFill}>
29
+ <View style={styles.contentContainer}>
30
+ <FontAwesomeIcon
31
+ icon={['fas', iconKey as IconName]}
32
+ size={ICON_SIZE[size]}
33
+ color="white"
34
+ />
35
+ </View>
36
+ </LinearGradient>
37
+ </AvatarPrimitive.Mask>
38
+ </AvatarPrimitive.Root>
39
+ )
40
+ }
41
+
42
+ const styles = StyleSheet.create({
43
+ gradientFill: {
44
+ width: '100%',
45
+ height: '100%',
46
+ },
47
+ contentContainer: {
48
+ flex: 1,
49
+ alignItems: 'center',
50
+ justifyContent: 'center',
51
+ },
52
+ })
@@ -1,6 +1,9 @@
1
1
  export * from './avatar_group'
2
2
  export * from './avatar'
3
3
  export * from './badge'
4
+ export * from './conversation_avatar'
5
+ export * from './emoji_avatar'
6
+ export * from './icon_avatar'
4
7
  export * from './banner_collapsible'
5
8
  export * from './banner'
6
9
  export * from './button'
@@ -0,0 +1,87 @@
1
+ // Gradient color map for custom conversation avatars.
2
+ // Values ported from chat web constants and @planningcenter/tapestry tokens.
3
+
4
+ interface Point {
5
+ x: number
6
+ y: number
7
+ }
8
+
9
+ interface AvatarGradientProps {
10
+ colors: string[]
11
+ start: Point
12
+ end: Point
13
+ }
14
+
15
+ export type CustomAvatarColorKey =
16
+ | 'warm-sunset'
17
+ | 'peach'
18
+ | 'rose-gold'
19
+ | 'purple-gold'
20
+ | 'gold'
21
+ | 'garden'
22
+ | 'gold-blue'
23
+ | 'green-blue'
24
+ | 'iris'
25
+ | 'cosmic'
26
+ | 'navy-purple'
27
+ | 'twilight'
28
+ | 'steel'
29
+ | 'mauve'
30
+ | 'rose-teal'
31
+ | 'teal-purple'
32
+ | 'amethyst'
33
+ | 'rainbow'
34
+
35
+ export const DEFAULT_AVATAR_COLOR_KEY: CustomAvatarColorKey = 'warm-sunset'
36
+
37
+ // CSS angle → RN coordinate mapping:
38
+ // 90° → start: {x:0, y:0.5}, end: {x:1, y:0.5}
39
+ // 120° → start: {x:0, y:0}, end: {x:0.5, y:1}
40
+ // 135° → start: {x:0, y:0}, end: {x:1, y:1}
41
+ // 160° → start: {x:0, y:0}, end: {x:0.35, y:1}
42
+
43
+ const ANGLE_135: Pick<AvatarGradientProps, 'start' | 'end'> = {
44
+ start: { x: 0, y: 0 },
45
+ end: { x: 1, y: 1 },
46
+ }
47
+
48
+ const ANGLE_90: Pick<AvatarGradientProps, 'start' | 'end'> = {
49
+ start: { x: 0, y: 0.5 },
50
+ end: { x: 1, y: 0.5 },
51
+ }
52
+
53
+ const ANGLE_120: Pick<AvatarGradientProps, 'start' | 'end'> = {
54
+ start: { x: 0, y: 0 },
55
+ end: { x: 0.5, y: 1 },
56
+ }
57
+
58
+ const ANGLE_160: Pick<AvatarGradientProps, 'start' | 'end'> = {
59
+ start: { x: 0, y: 0 },
60
+ end: { x: 0.35, y: 1 },
61
+ }
62
+
63
+ const AVATAR_GRADIENT_MAP: Record<CustomAvatarColorKey, AvatarGradientProps> = {
64
+ 'warm-sunset': { colors: ['#F6D06F', '#FB7946', '#B13825'], ...ANGLE_135 },
65
+ peach: { colors: ['#FCD983', '#FC9369'], ...ANGLE_90 },
66
+ 'rose-gold': { colors: ['#ED78BE', '#E19084', '#EDB32C'], ...ANGLE_135 },
67
+ 'purple-gold': { colors: ['#8B52D0', '#F8C73F'], ...ANGLE_135 },
68
+ gold: { colors: ['#F4C652', '#CCA32C'], ...ANGLE_90 },
69
+ garden: { colors: ['#FCD983', '#8DB95B', '#2A837C'], ...ANGLE_135 },
70
+ 'gold-blue': { colors: ['#F4C652', '#3980C6'], ...ANGLE_90 },
71
+ 'green-blue': { colors: ['#3FA05A', '#2466F5'], ...ANGLE_135 },
72
+ iris: { colors: ['#9773A5', '#2466F5', '#73BFBA'], ...ANGLE_120 },
73
+ cosmic: { colors: ['#2466F5', '#784E88', '#CD4932'], ...ANGLE_160 },
74
+ 'navy-purple': { colors: ['#2E58BB', '#8B52D0'], ...ANGLE_160 },
75
+ twilight: { colors: ['#7FA1EB', '#865F95', '#3B404A'], ...ANGLE_135 },
76
+ steel: { colors: ['#8F95A3', '#585F6F'], ...ANGLE_135 },
77
+ mauve: { colors: ['#AD8FB7', '#585F6F'], ...ANGLE_135 },
78
+ 'rose-teal': { colors: ['#E8638A', '#73BFBA'], ...ANGLE_135 },
79
+ 'teal-purple': { colors: ['#6ADCC7', '#713FB0'], ...ANGLE_135 },
80
+ amethyst: { colors: ['#C69CE8', '#CB691F'], ...ANGLE_135 },
81
+ rainbow: { colors: ['#E76958', '#CCA32C', '#3FA05A', '#8B52D0'], ...ANGLE_135 },
82
+ }
83
+
84
+ export function getAvatarGradientProps(colorKey?: string | null): AvatarGradientProps {
85
+ const key = (colorKey as CustomAvatarColorKey) || DEFAULT_AVATAR_COLOR_KEY
86
+ return AVATAR_GRADIENT_MAP[key] || AVATAR_GRADIENT_MAP[DEFAULT_AVATAR_COLOR_KEY]
87
+ }
@@ -24,6 +24,10 @@ export const getConversationRequestArgs = ({ conversation_id }: { conversation_i
24
24
  'last_message_text_preview',
25
25
  'latest_read_message_sort_key',
26
26
  'preview_avatar_urls',
27
+ 'custom_avatar_type',
28
+ 'custom_avatar_key',
29
+ 'custom_avatar_color',
30
+ 'custom_avatar_image_url',
27
31
  'member_ability',
28
32
  'muted',
29
33
  'replies_disabled',
@@ -0,0 +1,65 @@
1
+ import { useMemo } from 'react'
2
+ import { GroupResource, GraphId } from '../types/resources/group_resource'
3
+ import { GroupsGroupResource } from '../types/resources/groups/groups_group_resource'
4
+ import { destructureChatGroupGraphId } from '../utils'
5
+ import { useApiGet } from './use_api'
6
+ import { useNewConversationEntry } from './use_new_conversation_entry'
7
+
8
+ export type NewConversationFromFilter =
9
+ | {
10
+ name: string
11
+ sourceAppName: 'groups'
12
+ groupId: number
13
+ }
14
+ | {
15
+ name: string
16
+ sourceAppName: 'services'
17
+ teamIds: number[]
18
+ }
19
+
20
+ export function useNewConversationFromFilter(
21
+ chatGroupGraphId?: GraphId
22
+ ): NewConversationFromFilter | undefined {
23
+ const { sourceAppName, sourceType, sourceId } = destructureChatGroupGraphId(chatGroupGraphId)
24
+ const entryMode = useNewConversationEntry()
25
+ const isGroupsGroup = sourceAppName === 'Groups' && sourceType === 'Group'
26
+ const isServicesTeam = sourceAppName === 'Services' && sourceType === 'Team'
27
+
28
+ const { data: group } = useApiGet<GroupResource>({
29
+ url: `/me/groups/${chatGroupGraphId}`,
30
+ data: { fields: { Group: ['name'] } },
31
+ enabled: !!chatGroupGraphId,
32
+ app: 'chat',
33
+ })
34
+
35
+ const { data: groupsGroup } = useApiGet<GroupsGroupResource>({
36
+ url: `/me/groups/${sourceId}`,
37
+ data: { fields: { Group: ['can_create_conversation'] } },
38
+ enabled: isGroupsGroup && !!sourceId,
39
+ app: 'groups',
40
+ })
41
+
42
+ return useMemo(() => {
43
+ if (isGroupsGroup && group?.name && sourceId) {
44
+ const canCreate = entryMode === 'select_type' || entryMode === 'groups'
45
+ if (!canCreate) return undefined
46
+ if (!groupsGroup?.canCreateConversation) return undefined
47
+ return { name: group.name, sourceAppName: 'groups', groupId: sourceId }
48
+ }
49
+
50
+ if (isServicesTeam && group?.name && sourceId) {
51
+ const canCreate = entryMode === 'select_type' || entryMode === 'teams'
52
+ if (!canCreate) return undefined
53
+ return { name: group.name, sourceAppName: 'services', teamIds: [sourceId] }
54
+ }
55
+
56
+ return undefined
57
+ }, [
58
+ group?.name,
59
+ sourceId,
60
+ entryMode,
61
+ isGroupsGroup,
62
+ isServicesTeam,
63
+ groupsGroup?.canCreateConversation,
64
+ ])
65
+ }
@@ -1,6 +1,8 @@
1
1
  import { StaticScreenProps, useNavigation } from '@react-navigation/native'
2
2
  import React from 'react'
3
3
  import FormSheet, { getFormSheetScreenOptions } from '../components/primitive/form_sheet'
4
+ import { useNewConversationEntry } from '../hooks/use_new_conversation_entry'
5
+ import type { NewConversationFromFilter } from '../hooks/use_new_conversation_from_filter'
4
6
  import { AppName } from '../types/resources/app_name'
5
7
  import { GraphId } from '../types/resources/group_resource'
6
8
  import { Haptic } from '../utils/native_adapters'
@@ -12,10 +14,46 @@ export const ConversationSelectTypeScreenOptions = getFormSheetScreenOptions({
12
14
  export type ConversationSelectTypeScreenProps = StaticScreenProps<{
13
15
  chat_group_graph_id?: GraphId
14
16
  group_source_app_name?: AppName
17
+ newConversationFromFilter?: NewConversationFromFilter
15
18
  }>
16
19
 
17
20
  export function ConversationSelectTypeScreen({ route }: ConversationSelectTypeScreenProps) {
18
21
  const navigation = useNavigation()
22
+ const entryMode = useNewConversationEntry()
23
+ const { newConversationFromFilter, chat_group_graph_id, group_source_app_name } =
24
+ route.params || {}
25
+
26
+ const filterParams = { chat_group_graph_id, group_source_app_name }
27
+ const canCreateGroups = entryMode === 'select_type' || entryMode === 'groups'
28
+ const canCreateTeams = entryMode === 'select_type' || entryMode === 'teams'
29
+
30
+ const handleFilterPress = () => {
31
+ if (!newConversationFromFilter) return
32
+
33
+ Haptic.impactLight()
34
+ navigation.goBack()
35
+ requestAnimationFrame(() => {
36
+ if (newConversationFromFilter.sourceAppName === 'groups') {
37
+ navigation.navigate('New', {
38
+ screen: 'ConversationNew',
39
+ params: {
40
+ ...filterParams,
41
+ source_app_name: 'Groups' as AppName,
42
+ group_id: newConversationFromFilter.groupId,
43
+ },
44
+ })
45
+ } else {
46
+ navigation.navigate('New', {
47
+ screen: 'ConversationNew',
48
+ params: {
49
+ ...filterParams,
50
+ source_app_name: 'Services' as AppName,
51
+ team_ids: newConversationFromFilter.teamIds,
52
+ },
53
+ })
54
+ }
55
+ })
56
+ }
19
57
 
20
58
  const handleGroupPress = () => {
21
59
  Haptic.impactLight()
@@ -23,7 +61,7 @@ export function ConversationSelectTypeScreen({ route }: ConversationSelectTypeSc
23
61
  requestAnimationFrame(() => {
24
62
  navigation.navigate('New', {
25
63
  screen: 'ConversationSelectGroupRecipients',
26
- params: { ...route.params },
64
+ params: filterParams,
27
65
  })
28
66
  })
29
67
  }
@@ -34,7 +72,7 @@ export function ConversationSelectTypeScreen({ route }: ConversationSelectTypeSc
34
72
  requestAnimationFrame(() => {
35
73
  navigation.navigate('New', {
36
74
  screen: 'ConversationSelectTeamsILeadRecipients',
37
- params: { ...route.params },
75
+ params: filterParams,
38
76
  })
39
77
  })
40
78
  }
@@ -44,16 +82,27 @@ export function ConversationSelectTypeScreen({ route }: ConversationSelectTypeSc
44
82
  <FormSheet.Header>
45
83
  <FormSheet.HeaderTitle>Start a conversation by</FormSheet.HeaderTitle>
46
84
  </FormSheet.Header>
47
- <FormSheet.Action
48
- title="Group"
49
- onPress={handleGroupPress}
50
- accessibilityHint="Opens group selection screen"
51
- />
52
- <FormSheet.Action
53
- title="Team"
54
- onPress={handleTeamPress}
55
- accessibilityHint="Opens team selection screen"
56
- />
85
+ {newConversationFromFilter && (
86
+ <FormSheet.Action
87
+ title={`New conversation in ${newConversationFromFilter.name}`}
88
+ onPress={handleFilterPress}
89
+ accessibilityHint="Creates a conversation pre-scoped to the current filter"
90
+ />
91
+ )}
92
+ {canCreateGroups && (
93
+ <FormSheet.Action
94
+ title="Group"
95
+ onPress={handleGroupPress}
96
+ accessibilityHint="Opens group selection screen"
97
+ />
98
+ )}
99
+ {canCreateTeams && (
100
+ <FormSheet.Action
101
+ title="Team"
102
+ onPress={handleTeamPress}
103
+ accessibilityHint="Opens team selection screen"
104
+ />
105
+ )}
57
106
  </FormSheet.Root>
58
107
  )
59
108
  }
@@ -8,6 +8,8 @@ import { useAppName } from '../../../hooks/use_app_name'
8
8
  import { useMarkAllRead } from '../../../hooks/use_conversations_actions'
9
9
  import { useCanDisplayGroups } from '../../../hooks/use_groups'
10
10
  import { useNewConversationEntry } from '../../../hooks/use_new_conversation_entry'
11
+ import { useNewConversationFromFilter } from '../../../hooks/use_new_conversation_from_filter'
12
+ import { GraphId } from '../../../types/resources/group_resource'
11
13
  import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../../../utils'
12
14
  import { Haptic } from '../../../utils/native_adapters'
13
15
  import { ConversationsScreenProps } from '../conversations_screen'
@@ -33,6 +35,9 @@ export const ListHeaderComponent = () => {
33
35
  const { markAllRead, isPending } = useMarkAllRead()
34
36
  const canCreateConversations = useCanCreateConversations()
35
37
  const entryMode = useNewConversationEntry()
38
+ const newConversationFromFilter = useNewConversationFromFilter(
39
+ chat_group_graph_id as GraphId | undefined
40
+ )
36
41
  const appName = useAppName()
37
42
 
38
43
  const active: FilterTypes = useMemo(() => {
@@ -48,8 +53,11 @@ export const ListHeaderComponent = () => {
48
53
  }, [chat_group_graph_id, group_source_app_name])
49
54
 
50
55
  const handleNewConversationNavigation = useCallback(() => {
51
- if (entryMode === 'select_type') {
52
- return navigation.navigate('ConversationSelectType', { ...route.params })
56
+ if (newConversationFromFilter || entryMode === 'select_type') {
57
+ return navigation.navigate('ConversationSelectType', {
58
+ ...route.params,
59
+ newConversationFromFilter,
60
+ })
53
61
  }
54
62
 
55
63
  if (entryMode === 'groups') {
@@ -65,7 +73,7 @@ export const ListHeaderComponent = () => {
65
73
  params: { ...route.params },
66
74
  })
67
75
  }
68
- }, [navigation, route.params, entryMode])
76
+ }, [navigation, route.params, entryMode, newConversationFromFilter])
69
77
 
70
78
  const handleMoreOptions = useCallback(() => {
71
79
  navigation.navigate('ConversationsMoreActions')
@@ -10,6 +10,9 @@ import {
10
10
  Banner,
11
11
  BannerCollapsible,
12
12
  Button,
13
+ ConversationAvatar,
14
+ EmojiAvatar,
15
+ IconAvatar,
13
16
  ToggleButton,
14
17
  Heading,
15
18
  Icon,
@@ -811,6 +814,69 @@ function ImageIconsSection({ isLast }: SectionProps) {
811
814
  />
812
815
  </Row>
813
816
  </Group>
817
+ <Group
818
+ title="IconAvatar"
819
+ description="Renders a FontAwesome icon centered on a gradient background. Used by ConversationAvatar for icon-type custom avatars, and reusable in the avatar picker UI."
820
+ >
821
+ <Row>
822
+ <IconAvatar iconKey="church" color="warm-sunset" size="lg" />
823
+ <IconAvatar iconKey="guitar" color="cosmic" size="lg" />
824
+ <IconAvatar iconKey="music" color="iris" size="md" />
825
+ <IconAvatar iconKey="dove" color="rose-teal" size="sm" />
826
+ <IconAvatar iconKey="cross" color="gold" size="xs" />
827
+ </Row>
828
+ <Row>
829
+ <IconAvatar iconKey="church" color="garden" size="lg" />
830
+ <IconAvatar iconKey="church" color="twilight" size="lg" />
831
+ <IconAvatar iconKey="church" color="amethyst" size="lg" />
832
+ <IconAvatar iconKey="church" color="rainbow" size="lg" />
833
+ </Row>
834
+ </Group>
835
+ <Group
836
+ title="EmojiAvatar"
837
+ description="Renders an emoji centered on a gradient background. Used by ConversationAvatar for emoji-type custom avatars."
838
+ >
839
+ <Row>
840
+ <EmojiAvatar emoji="🎉" color="warm-sunset" size="lg" />
841
+ <EmojiAvatar emoji="🔥" color="cosmic" size="lg" />
842
+ <EmojiAvatar emoji="💬" color="iris" size="md" />
843
+ <EmojiAvatar emoji="⭐" color="gold" size="sm" />
844
+ <EmojiAvatar emoji="🎵" color="navy-purple" size="xs" />
845
+ </Row>
846
+ </Group>
847
+ <Group
848
+ title="ConversationAvatar"
849
+ description="Smart avatar that resolves a conversation's custom avatar (image, icon, or emoji) or falls back to the member AvatarGroup. Takes a conversation object and delegates to IconAvatar, EmojiAvatar, or AvatarGroup."
850
+ >
851
+ <Row>
852
+ <ConversationAvatar
853
+ conversation={{
854
+ customAvatarType: 'icon',
855
+ customAvatarKey: 'church',
856
+ customAvatarColor: 'warm-sunset',
857
+ }}
858
+ />
859
+ <ConversationAvatar
860
+ conversation={{
861
+ customAvatarType: 'emoji',
862
+ customAvatarKey: '🎉',
863
+ customAvatarColor: 'cosmic',
864
+ }}
865
+ />
866
+ <ConversationAvatar
867
+ conversation={{
868
+ customAvatarType: 'image',
869
+ customAvatarImageUrl: URL.avatar,
870
+ }}
871
+ />
872
+ <ConversationAvatar
873
+ conversation={{
874
+ previewAvatarUrls: URL.two_avatars,
875
+ }}
876
+ />
877
+ <ConversationAvatar conversation={{}} fallbackIconName="people.noTextMessage" />
878
+ </Row>
879
+ </Group>
814
880
  <Group
815
881
  title="Icon"
816
882
  description="Displays any icon from @planningcenter/icons. Missing icons will fallback to a grey circle. Styling with `fontSize` will allow it to scale with the device's text a11y size."
@@ -15,6 +15,10 @@ export interface ConversationResource {
15
15
  genderOption?: string | null
16
16
  groups?: GroupResource[]
17
17
  previewAvatarUrls?: string[]
18
+ customAvatarType?: 'image' | 'icon' | 'emoji' | null
19
+ customAvatarKey?: string | null
20
+ customAvatarColor?: string | null
21
+ customAvatarImageUrl?: string | null
18
22
  lastMessageAuthorId?: string
19
23
  lastMessageAuthorName?: string
20
24
  lastMessageCreatedAt?: string
@@ -37,6 +37,10 @@ export const getConversationsRequestArgs = ({
37
37
  'last_message_created_at',
38
38
  'last_message_text_preview',
39
39
  'preview_avatar_urls',
40
+ 'custom_avatar_type',
41
+ 'custom_avatar_key',
42
+ 'custom_avatar_color',
43
+ 'custom_avatar_image_url',
40
44
  'member_ability',
41
45
  'muted',
42
46
  'replies_disabled',