@planningcenter/chat-react-native 3.31.0-rc.3 → 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 (46) 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/screens/design_system_screen.d.ts.map +1 -1
  28. package/build/screens/design_system_screen.js +47 -1
  29. package/build/screens/design_system_screen.js.map +1 -1
  30. package/build/types/resources/conversation.d.ts +4 -0
  31. package/build/types/resources/conversation.d.ts.map +1 -1
  32. package/build/types/resources/conversation.js.map +1 -1
  33. package/build/utils/request/conversation.d.ts.map +1 -1
  34. package/build/utils/request/conversation.js +4 -0
  35. package/build/utils/request/conversation.js.map +1 -1
  36. package/package.json +2 -2
  37. package/src/components/conversations/conversation_preview.tsx +3 -7
  38. package/src/components/display/conversation_avatar.tsx +90 -0
  39. package/src/components/display/emoji_avatar.tsx +48 -0
  40. package/src/components/display/icon_avatar.tsx +52 -0
  41. package/src/components/display/index.ts +3 -0
  42. package/src/components/display/utils/avatar_gradient_colors.ts +87 -0
  43. package/src/hooks/use_conversation.ts +4 -0
  44. package/src/screens/design_system_screen.tsx +66 -0
  45. package/src/types/resources/conversation.ts +4 -0
  46. 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',
@@ -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',