@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.
- package/build/components/conversations/conversation_preview.d.ts.map +1 -1
- package/build/components/conversations/conversation_preview.js +3 -5
- package/build/components/conversations/conversation_preview.js.map +1 -1
- package/build/components/display/conversation_avatar.d.ts +16 -0
- package/build/components/display/conversation_avatar.d.ts.map +1 -0
- package/build/components/display/conversation_avatar.js +43 -0
- package/build/components/display/conversation_avatar.js.map +1 -0
- package/build/components/display/emoji_avatar.d.ts +10 -0
- package/build/components/display/emoji_avatar.d.ts.map +1 -0
- package/build/components/display/emoji_avatar.js +37 -0
- package/build/components/display/emoji_avatar.js.map +1 -0
- package/build/components/display/icon_avatar.d.ts +10 -0
- package/build/components/display/icon_avatar.d.ts.map +1 -0
- package/build/components/display/icon_avatar.js +36 -0
- package/build/components/display/icon_avatar.js.map +1 -0
- package/build/components/display/index.d.ts +3 -0
- package/build/components/display/index.d.ts.map +1 -1
- package/build/components/display/index.js +3 -0
- package/build/components/display/index.js.map +1 -1
- package/build/components/display/utils/avatar_gradient_colors.d.ts +14 -0
- package/build/components/display/utils/avatar_gradient_colors.d.ts.map +1 -0
- package/build/components/display/utils/avatar_gradient_colors.js +49 -0
- package/build/components/display/utils/avatar_gradient_colors.js.map +1 -0
- package/build/hooks/use_conversation.d.ts.map +1 -1
- package/build/hooks/use_conversation.js +4 -0
- package/build/hooks/use_conversation.js.map +1 -1
- package/build/hooks/use_new_conversation_from_filter.d.ts +12 -0
- package/build/hooks/use_new_conversation_from_filter.d.ts.map +1 -0
- package/build/hooks/use_new_conversation_from_filter.js +47 -0
- package/build/hooks/use_new_conversation_from_filter.js.map +1 -0
- package/build/screens/conversation_select_type_screen.d.ts +2 -0
- package/build/screens/conversation_select_type_screen.d.ts.map +1 -1
- package/build/screens/conversation_select_type_screen.js +39 -4
- package/build/screens/conversation_select_type_screen.js.map +1 -1
- package/build/screens/conversations/components/list_header_component.d.ts.map +1 -1
- package/build/screens/conversations/components/list_header_component.js +8 -3
- package/build/screens/conversations/components/list_header_component.js.map +1 -1
- package/build/screens/design_system_screen.d.ts.map +1 -1
- package/build/screens/design_system_screen.js +47 -1
- package/build/screens/design_system_screen.js.map +1 -1
- package/build/types/resources/conversation.d.ts +4 -0
- package/build/types/resources/conversation.d.ts.map +1 -1
- package/build/types/resources/conversation.js.map +1 -1
- package/build/utils/request/conversation.d.ts.map +1 -1
- package/build/utils/request/conversation.js +4 -0
- package/build/utils/request/conversation.js.map +1 -1
- package/package.json +2 -2
- package/src/components/conversations/conversation_preview.tsx +3 -7
- package/src/components/display/conversation_avatar.tsx +90 -0
- package/src/components/display/emoji_avatar.tsx +48 -0
- package/src/components/display/icon_avatar.tsx +52 -0
- package/src/components/display/index.ts +3 -0
- package/src/components/display/utils/avatar_gradient_colors.ts +87 -0
- package/src/hooks/use_conversation.ts +4 -0
- package/src/hooks/use_new_conversation_from_filter.ts +65 -0
- package/src/screens/conversation_select_type_screen.tsx +61 -12
- package/src/screens/conversations/components/list_header_component.tsx +11 -3
- package/src/screens/design_system_screen.tsx +66 -0
- package/src/types/resources/conversation.ts +4 -0
- 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:
|
|
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:
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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', {
|
|
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',
|