@planningcenter/chat-react-native 3.7.0-rc.8 → 3.7.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 (38) hide show
  1. package/build/components/conversation/message_reaction.d.ts.map +1 -1
  2. package/build/components/conversation/message_reaction.js +18 -8
  3. package/build/components/conversation/message_reaction.js.map +1 -1
  4. package/build/components/display/button.d.ts +5 -5
  5. package/build/components/display/button.d.ts.map +1 -1
  6. package/build/components/display/button.js +9 -4
  7. package/build/components/display/button.js.map +1 -1
  8. package/build/components/display/icon.d.ts +1 -0
  9. package/build/components/display/icon.d.ts.map +1 -1
  10. package/build/components/display/icon.js +3 -0
  11. package/build/components/display/icon.js.map +1 -1
  12. package/build/components/primitive/form_sheet.d.ts +9 -2
  13. package/build/components/primitive/form_sheet.d.ts.map +1 -1
  14. package/build/components/primitive/form_sheet.js +48 -7
  15. package/build/components/primitive/form_sheet.js.map +1 -1
  16. package/build/screens/attachment_actions/attachment_actions_screen.d.ts.map +1 -1
  17. package/build/screens/attachment_actions/attachment_actions_screen.js +9 -3
  18. package/build/screens/attachment_actions/attachment_actions_screen.js.map +1 -1
  19. package/build/screens/conversation/message_read_receipts_screen.d.ts +1 -2
  20. package/build/screens/conversation/message_read_receipts_screen.d.ts.map +1 -1
  21. package/build/screens/conversation/message_read_receipts_screen.js +19 -40
  22. package/build/screens/conversation/message_read_receipts_screen.js.map +1 -1
  23. package/build/screens/conversation_screen.d.ts.map +1 -1
  24. package/build/screens/conversation_screen.js +7 -7
  25. package/build/screens/conversation_screen.js.map +1 -1
  26. package/build/screens/message_actions_screen.d.ts +1 -2
  27. package/build/screens/message_actions_screen.d.ts.map +1 -1
  28. package/build/screens/message_actions_screen.js +35 -41
  29. package/build/screens/message_actions_screen.js.map +1 -1
  30. package/package.json +2 -2
  31. package/src/components/conversation/message_reaction.tsx +18 -8
  32. package/src/components/display/button.tsx +15 -9
  33. package/src/components/display/icon.tsx +3 -0
  34. package/src/components/primitive/form_sheet.tsx +74 -6
  35. package/src/screens/attachment_actions/attachment_actions_screen.tsx +14 -3
  36. package/src/screens/conversation/message_read_receipts_screen.tsx +22 -44
  37. package/src/screens/conversation_screen.tsx +11 -7
  38. package/src/screens/message_actions_screen.tsx +65 -50
@@ -64,13 +64,13 @@ export interface ButtonProps extends PressableProps {
64
64
  */
65
65
  appearance?: ButtonAppearanceUnion
66
66
  /**
67
- * Styles the inner View that wraps the button's content
67
+ * Styles the outer LinearGradient that gives the button its background and outline color
68
68
  */
69
- buttonInnerStyles?: ViewStyle
69
+ colorWrapperStyles?: ViewStyle
70
70
  /**
71
- * Styles the outer LinearGradient that gives the button it's backgrounnd and outline color
71
+ * Styles the inner View that wraps the button's content
72
72
  */
73
- buttonOuterStyles?: ViewStyle
73
+ contentWrapperStyles?: ViewStyle
74
74
  /**
75
75
  * Generates an icon to the left of the button text
76
76
  */
@@ -109,8 +109,8 @@ export function Button({
109
109
  adjustsFontSizeToFit = false,
110
110
  allowFontScaling = true,
111
111
  appearance = 'interaction',
112
- buttonInnerStyles,
113
- buttonOuterStyles,
112
+ contentWrapperStyles,
113
+ colorWrapperStyles,
114
114
  disabled = false,
115
115
  iconNameLeft,
116
116
  iconNameRight,
@@ -120,12 +120,14 @@ export function Button({
120
120
  size = 'md',
121
121
  title,
122
122
  variant = 'fill',
123
+ style,
123
124
  ...props
124
125
  }: ButtonProps) {
125
126
  const styles = useStyles({ appearance, disabled, loading, maxFontSizeMultiplier, size, variant })
126
127
  const gradientOptionsMap = useGradientColorMap()
127
128
  const colorKey = getColorKey({ disabled, loading, appearance })
128
129
 
130
+ const overrideStyles = StyleSheet.flatten(style)
129
131
  const textStyles = [styles.text, disabled && styles.textDisabled, loading && styles.iconLoading]
130
132
  const iconStyles = [styles.icon, disabled && styles.iconDisabled, loading && styles.textLoading]
131
133
 
@@ -133,7 +135,11 @@ export function Button({
133
135
 
134
136
  return (
135
137
  <Pressable
136
- style={({ pressed }) => [styles.pressable, pressed && platformPressedOpacityStyle]}
138
+ style={({ pressed }) => ({
139
+ ...styles.pressable,
140
+ ...(pressed && platformPressedOpacityStyle),
141
+ ...overrideStyles,
142
+ })}
137
143
  accessibilityRole="button"
138
144
  disabled={disabled || loading}
139
145
  accessibilityState={{ busy: loading }}
@@ -144,7 +150,7 @@ export function Button({
144
150
  start={{ x: 0.1, y: 0.1 }}
145
151
  end={{ x: 0.9, y: 0.9 }}
146
152
  colors={gradientOptionsMap[colorKey]}
147
- style={[styles.colorWrapper, buttonOuterStyles]}
153
+ style={[styles.colorWrapper, colorWrapperStyles]}
148
154
  >
149
155
  {loading && (
150
156
  <Spinner
@@ -152,7 +158,7 @@ export function Button({
152
158
  maxFontSizeMultiplier={maxFontSizeMultiplier || 0}
153
159
  />
154
160
  )}
155
- <View style={[styles.innerWrapper, buttonInnerStyles]}>
161
+ <View style={[styles.innerWrapper, contentWrapperStyles]}>
156
162
  {iconNameLeft && (
157
163
  <Icon
158
164
  name={iconNameLeft}
@@ -5,6 +5,8 @@ import { SvgXml } from 'react-native-svg'
5
5
  import type { XmlProps } from 'react-native-svg'
6
6
  import { useFontScale, useTheme } from '../../hooks'
7
7
 
8
+ // @ts-ignore
9
+ import * as accounts from '@planningcenter/icons/paths/accounts'
8
10
  // @ts-ignore
9
11
  import * as api from '@planningcenter/icons/paths/api'
10
12
  // @ts-ignore
@@ -33,6 +35,7 @@ import * as publishing from '@planningcenter/icons/paths/publishing'
33
35
  const FALLBACK_SIZE = 12
34
36
 
35
37
  const ICONS = {
38
+ accounts,
36
39
  api,
37
40
  brand,
38
41
  calendar,
@@ -1,10 +1,16 @@
1
1
  import { NativeStackNavigationOptions } from '@react-navigation/native-stack'
2
2
  import React, { ReactNode } from 'react'
3
- import { AccessibilityRole, Platform, StyleSheet, View } from 'react-native'
3
+ import {
4
+ Platform,
5
+ StyleSheet,
6
+ View,
7
+ useWindowDimensions,
8
+ type AccessibilityRole,
9
+ } from 'react-native'
4
10
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
5
11
  import { useTheme } from '../../hooks'
6
- import { PlatformPressable } from '@react-navigation/elements'
7
- import { Icon, IconString, Text } from '../display'
12
+ import { PlatformPressable, useHeaderHeight } from '@react-navigation/elements'
13
+ import { Heading, Icon, IconString, Text } from '../display'
8
14
 
9
15
  // =================================
10
16
  // ====== Exports ==================
@@ -34,15 +40,17 @@ export const getFormSheetScreenOptions = ({
34
40
  const FormSheet = {
35
41
  Root: FormSheetRoot,
36
42
  Action: FormSheetAction,
43
+ Header: FormSheetHeader,
37
44
  } as const
38
45
 
39
46
  type FormSheetComponents = {
40
47
  Root: React.FC<FormSheetRootProps>
41
48
  Action: React.FC<FormSheetActionProps>
49
+ Header: React.FC<FormSheetHeaderProps>
42
50
  }
43
51
 
44
52
  export default FormSheet as FormSheetComponents
45
- export type { FormSheetRootProps, FormSheetActionProps }
53
+ export type { FormSheetRootProps, FormSheetActionProps, FormSheetHeaderProps }
46
54
 
47
55
  // ====================================
48
56
  // ====== ActionsFormSheetRoot ========
@@ -56,7 +64,7 @@ export function FormSheetRoot({ children }: FormSheetRootProps) {
56
64
  const styles = useStyles()
57
65
 
58
66
  return (
59
- <View style={styles.container}>
67
+ <View style={styles.container} collapsable={false}>
60
68
  <AndroidSheetGrabber />
61
69
  <View style={styles.content}>{children}</View>
62
70
  </View>
@@ -78,6 +86,36 @@ function AndroidSheetGrabber() {
78
86
  })
79
87
  }
80
88
 
89
+ // ====================================
90
+ // ====== FormSheetHeader =============
91
+ // ====================================
92
+
93
+ interface FormSheetHeaderProps {
94
+ title: string
95
+ secondaryButton?: ReactNode
96
+ primaryButton?: ReactNode
97
+ }
98
+ function FormSheetHeader({ title, secondaryButton, primaryButton }: FormSheetHeaderProps) {
99
+ const styles = useStyles()
100
+ const hasActions = Boolean(secondaryButton) || Boolean(primaryButton)
101
+
102
+ return (
103
+ <View style={styles.header}>
104
+ <Heading variant="h3" style={styles.headerTitle}>
105
+ {title}
106
+ </Heading>
107
+ {hasActions && (
108
+ <View style={styles.headerActions}>
109
+ {secondaryButton}
110
+ {primaryButton}
111
+ </View>
112
+ )}
113
+ </View>
114
+ )
115
+ }
116
+
117
+ FormSheetHeader.displayName = 'FormSheet.Header'
118
+
81
119
  // ====================================
82
120
  // ====== ActionsFormSheetAction ======
83
121
  // ====================================
@@ -98,6 +136,7 @@ interface FormSheetActionProps {
98
136
  accessibilityHint?: string
99
137
  accessibilityRole?: AccessibilityRole
100
138
  appearance?: FormSheetActionAppearanceUnion
139
+ disabled?: boolean
101
140
  }
102
141
 
103
142
  function FormSheetAction({
@@ -108,6 +147,7 @@ function FormSheetAction({
108
147
  accessibilityHint,
109
148
  accessibilityRole = 'button',
110
149
  appearance = 'neutral',
150
+ disabled = false,
111
151
  }: FormSheetActionProps) {
112
152
  const styles = useStyles({ appearance })
113
153
 
@@ -118,6 +158,7 @@ function FormSheetAction({
118
158
  accessibilityHint={accessibilityHint}
119
159
  accessibilityRole={accessibilityRole}
120
160
  style={styles.actionPressable}
161
+ disabled={disabled}
121
162
  >
122
163
  <Icon name={iconName} size={16} accessibilityElementsHidden style={styles.actionIcon} />
123
164
  <Text style={styles.actionTitle}>{title}</Text>
@@ -137,7 +178,14 @@ interface Styles {
137
178
 
138
179
  const useStyles = ({ appearance = 'neutral' }: Styles = {}) => {
139
180
  const { colors } = useTheme()
140
- const { bottom } = useSafeAreaInsets()
181
+ const { height } = useWindowDimensions()
182
+ const { bottom, top } = useSafeAreaInsets()
183
+ const headerHeight = useHeaderHeight()
184
+
185
+ const containerHeight = Platform.select({
186
+ ios: height - top - headerHeight,
187
+ default: null,
188
+ })
141
189
 
142
190
  const appearanceColorsMap = {
143
191
  neutral: {
@@ -157,6 +205,7 @@ const useStyles = ({ appearance = 'neutral' }: Styles = {}) => {
157
205
  paddingBottom: bottom,
158
206
  width: '100%',
159
207
  backgroundColor: colors.fillColorNeutral100Inverted,
208
+ height: containerHeight,
160
209
  },
161
210
  androidSheetGrabber: {
162
211
  marginTop: 5,
@@ -169,6 +218,25 @@ const useStyles = ({ appearance = 'neutral' }: Styles = {}) => {
169
218
  content: {
170
219
  paddingTop: Platform.select({ android: 16, ios: 20 }),
171
220
  },
221
+ header: {
222
+ backgroundColor: colors.fillColorNeutral100Inverted,
223
+ paddingHorizontal: 16,
224
+ paddingBottom: 16,
225
+ gap: 16,
226
+ borderBottomWidth: 1,
227
+ borderBottomColor: colors.borderColorDefaultBase,
228
+ flexDirection: 'row',
229
+ alignItems: 'center',
230
+ justifyContent: 'space-between',
231
+ },
232
+ headerTitle: {
233
+ textAlign: 'left',
234
+ },
235
+ headerActions: {
236
+ flexDirection: 'row',
237
+ alignItems: 'center',
238
+ gap: 16,
239
+ },
172
240
  actionPressable: {
173
241
  flexDirection: 'row',
174
242
  alignItems: 'center',
@@ -1,6 +1,6 @@
1
1
  import { StaticScreenProps, useNavigation } from '@react-navigation/native'
2
- import React from 'react'
3
- import { Linking } from 'react-native'
2
+ import React, { useCallback } from 'react'
3
+ import { Alert, Linking } from 'react-native'
4
4
  import FormSheet, { getFormSheetScreenOptions } from '../../components/primitive/form_sheet'
5
5
  import { useDeleteAttachment } from './hooks/useDeleteAttachment'
6
6
 
@@ -37,6 +37,17 @@ export function AttachmentActionsScreen({ route }: AttachmentActionsScreenProps)
37
37
  attachmentName,
38
38
  })
39
39
 
40
+ const handleDeleteConfirm = useCallback(() => {
41
+ Alert.alert(
42
+ `Delete ${attachmentName}`,
43
+ 'Are you sure you want to permanently delete this attachment?',
44
+ [
45
+ { text: 'Cancel', style: 'cancel' },
46
+ { text: 'Delete', style: 'destructive', onPress: () => handleDeleteAttachment() },
47
+ ]
48
+ )
49
+ }, [attachmentName, handleDeleteAttachment])
50
+
40
51
  const handleOpenInBrowser = () => {
41
52
  Linking.openURL(attachmentUrl)
42
53
  navigation.goBack()
@@ -57,7 +68,7 @@ export function AttachmentActionsScreen({ route }: AttachmentActionsScreenProps)
57
68
  appearance="danger"
58
69
  iconName="publishing.trash"
59
70
  title={`Delete ${attachmentName}`}
60
- onPress={handleDeleteAttachment}
71
+ onPress={handleDeleteConfirm}
61
72
  />
62
73
  )}
63
74
  </FormSheet.Root>
@@ -1,26 +1,21 @@
1
- import { useHeaderHeight } from '@react-navigation/elements'
2
- import { StaticScreenProps } from '@react-navigation/native'
3
- import { NativeStackNavigationOptions } from '@react-navigation/native-stack'
1
+ import { StaticScreenProps, useNavigation } from '@react-navigation/native'
4
2
  import React, { memo } from 'react'
5
- import { Platform, StyleSheet, useWindowDimensions, View } from 'react-native'
3
+ import { Platform, StyleSheet, View } from 'react-native'
6
4
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
7
- import { Avatar, Heading, Text } from '../../components'
8
- import { useTheme } from '../../hooks'
5
+ import { Avatar, Text, TextButton } from '../../components'
9
6
  import { useReadReceipts } from '../../hooks/use_read_receipts'
10
7
  import { FlatList } from 'react-native-gesture-handler'
11
8
  import { ReadReceiptResource } from '../../types/resources/read_receipt'
9
+ import FormSheet, { getFormSheetScreenOptions } from '../../components/primitive/form_sheet'
10
+ import { useTheme } from '../../hooks'
12
11
 
13
- export const MessageReadReceiptsScreenOptions: NativeStackNavigationOptions = {
14
- presentation: 'formSheet',
12
+ export const MessageReadReceiptsScreenOptions = getFormSheetScreenOptions({
15
13
  sheetAllowedDetents: Platform.select({
16
- android: [0.25, 0.94], // Going straight to full 0.94 preserves height of screen on Android
17
- default: [0.25, 0.5, 1],
14
+ android: [0.5, 0.94], // Going straight to full 0.94 preserves height of screen on Android
15
+ default: [0.5, 1],
18
16
  }),
19
- sheetGrabberVisible: true,
20
- headerShown: false,
21
- sheetCornerRadius: 16,
22
17
  headerTitle: 'Read receipts',
23
- }
18
+ })
24
19
 
25
20
  export type MessageReadReceiptsScreenProps = StaticScreenProps<{
26
21
  conversation_id: number
@@ -29,6 +24,7 @@ export type MessageReadReceiptsScreenProps = StaticScreenProps<{
29
24
 
30
25
  export function MessageReadReceiptsScreen({ route }: MessageReadReceiptsScreenProps) {
31
26
  const styles = useStyles()
27
+ const navigation = useNavigation()
32
28
 
33
29
  const { conversation_id, message_id } = route.params
34
30
  const {
@@ -38,22 +34,22 @@ export function MessageReadReceiptsScreen({ route }: MessageReadReceiptsScreenPr
38
34
  } = useReadReceipts({ conversation_id, message_id })
39
35
 
40
36
  return (
41
- <View style={styles.container} collapsable={false}>
42
- <Heading variant="h3" style={styles.header}>
43
- Read receipts ({totalCount})
44
- </Heading>
37
+ <FormSheet.Root>
38
+ <FormSheet.Header
39
+ title={`Read receipts (${totalCount})`}
40
+ secondaryButton={<TextButton onPress={() => navigation.goBack()}>Close</TextButton>}
41
+ />
45
42
  <FlatList
46
43
  data={receipts}
47
- style={styles.contentContainer}
44
+ contentContainerStyle={styles.contentContainer}
48
45
  keyExtractor={item => item.id.toString()}
49
46
  renderItem={({ item }) => <Receipt receipt={item} />}
50
47
  nestedScrollEnabled
51
- ListFooterComponent={<View style={styles.footer} />}
52
48
  ListEmptyComponent={<Text style={styles.emptyText}>No one has read this message yet.</Text>}
53
49
  onEndReached={() => fetchNextPage()}
54
50
  onEndReachedThreshold={0.2}
55
51
  />
56
- </View>
52
+ </FormSheet.Root>
57
53
  )
58
54
  }
59
55
 
@@ -71,29 +67,14 @@ const Receipt = memo(({ receipt }: { receipt: ReadReceiptResource }) => {
71
67
  })
72
68
 
73
69
  const useStyles = () => {
74
- const theme = useTheme()
75
- const { height } = useWindowDimensions()
76
- const { bottom, top } = useSafeAreaInsets()
77
- const headerHeight = useHeaderHeight()
78
-
79
- const containerHeight = Platform.select({
80
- android: null,
81
- ios: height - top - headerHeight,
82
- })
70
+ const { bottom } = useSafeAreaInsets()
71
+ const { colors } = useTheme()
83
72
 
84
73
  return StyleSheet.create({
85
- container: {
86
- paddingTop: 16,
87
- backgroundColor: theme.colors.fillColorNeutral100Inverted,
88
- height: containerHeight,
89
- flex: 1,
90
- },
91
74
  contentContainer: {
92
75
  paddingHorizontal: 16,
93
- },
94
- header: {
95
- paddingHorizontal: 16,
96
- paddingBottom: 8,
76
+ paddingTop: 8,
77
+ paddingBottom: bottom + Platform.select({ android: 24, default: 16 }),
97
78
  },
98
79
  receiptRow: {
99
80
  flexDirection: 'row',
@@ -105,12 +86,9 @@ const useStyles = () => {
105
86
  name: {
106
87
  flex: 1,
107
88
  },
108
- footer: {
109
- height: bottom + 16,
110
- },
111
89
  emptyText: {
112
90
  textAlign: 'center',
113
- color: '#888',
91
+ color: colors.textColorDefaultSecondary,
114
92
  marginTop: 32,
115
93
  fontSize: 16,
116
94
  },
@@ -18,6 +18,7 @@ import {
18
18
  MemberDisabledRepliesBanner,
19
19
  } from '../components/conversation/disabled_replies_banners'
20
20
  import { EmptyConversationBlankState } from '../components/conversation/empty_conversation_blank_state'
21
+ import { BlankState } from '../components/display/blank_state'
21
22
  import { Message } from '../components/conversation/message'
22
23
  import { MessageForm } from '../components/conversation/message_form'
23
24
  import { TypingIndicator } from '../components/conversation/typing_indicator'
@@ -93,9 +94,16 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
93
94
  if (!conversation || conversation.deleted) {
94
95
  return (
95
96
  <View style={styles.container}>
96
- <Text variant="plain" style={styles.deletedAlert}>
97
- This conversation has been deleted.
98
- </Text>
97
+ <BlankState
98
+ iconName="general.outlinedTextMessage"
99
+ title="This conversation has been deleted"
100
+ buttonProps={{
101
+ onPress: navigation.goBack,
102
+ title: 'Back to conversations',
103
+ accessibilityHint: 'Navigates back to the conversations list',
104
+ accessibilityRole: 'link',
105
+ }}
106
+ />
99
107
  </View>
100
108
  )
101
109
  }
@@ -310,10 +318,6 @@ const useStyles = () => {
310
318
  // Just whitespace to provide space where the typing indicator can be
311
319
  height: 16,
312
320
  },
313
- deletedAlert: {
314
- textAlign: 'center',
315
- padding: 16,
316
- },
317
321
  })
318
322
  }
319
323
 
@@ -1,11 +1,9 @@
1
1
  import { PlatformPressable } from '@react-navigation/elements'
2
2
  import { StackActions, StaticScreenProps, useNavigation } from '@react-navigation/native'
3
- import { NativeStackNavigationOptions } from '@react-navigation/native-stack'
4
3
  import { useMutation } from '@tanstack/react-query'
5
4
  import React, { useCallback } from 'react'
6
- import { Alert, Platform, StyleSheet, useWindowDimensions, View } from 'react-native'
7
- import { useSafeAreaInsets } from 'react-native-safe-area-context'
8
- import { Text, TextButton } from '../components'
5
+ import { Alert, Platform, StyleSheet, View } from 'react-native'
6
+ import { Text } from '../components'
9
7
  import { REACTION_EMOJIS, useReactionStyles } from '../components/conversation/message_reaction'
10
8
  import { useTheme } from '../hooks'
11
9
  import { useApiClient } from '../hooks/use_api_client'
@@ -14,13 +12,12 @@ import { useMessageReactionToggle } from '../hooks/use_message_reaction_toggle'
14
12
  import { ReactionCountResource } from '../types/resources/reaction'
15
13
  import { Clipboard } from '../utils/native_adapters'
16
14
  import { isNil, omitBy } from 'lodash'
15
+ import FormSheet, { getFormSheetScreenOptions } from '../components/primitive/form_sheet'
17
16
 
18
- export const MessageActionsScreenOptions: NativeStackNavigationOptions = {
19
- presentation: 'formSheet',
20
- headerShown: false,
21
- sheetAllowedDetents: [0.35],
22
- sheetGrabberVisible: true,
23
- }
17
+ export const MessageActionsScreenOptions = getFormSheetScreenOptions({
18
+ sheetAllowedDetents: [0.5],
19
+ headerTitle: 'Message actions',
20
+ })
24
21
 
25
22
  export type MessageActionsScreenProps = StaticScreenProps<{
26
23
  message_id: string
@@ -92,6 +89,13 @@ export function MessageActionsScreen({ route }: MessageActionsScreenProps) {
92
89
  },
93
90
  })
94
91
 
92
+ const handleDeleteConfirm = useCallback(() => {
93
+ Alert.alert('Delete message', 'Are you sure you want to permanently delete this message?', [
94
+ { text: 'Cancel', style: 'cancel' },
95
+ { text: 'Delete', style: 'destructive', onPress: () => handleDeleteMessage() },
96
+ ])
97
+ }, [handleDeleteMessage])
98
+
95
99
  const handleEditPress = useCallback(() => {
96
100
  const state = navigation.getState?.()
97
101
  const targetRoute = state?.routes?.find(r => r.name === 'Conversation')
@@ -118,7 +122,7 @@ export function MessageActionsScreen({ route }: MessageActionsScreenProps) {
118
122
  }, [navigation, conversation_id, message_id])
119
123
 
120
124
  return (
121
- <View style={styles.container}>
125
+ <FormSheet.Root>
122
126
  <View style={styles.reactionList}>
123
127
  {availableReactions.map((reaction, index) => (
124
128
  <Reaction
@@ -134,26 +138,40 @@ export function MessageActionsScreen({ route }: MessageActionsScreenProps) {
134
138
  ))}
135
139
  </View>
136
140
  <View style={styles.actions}>
137
- <View style={styles.actionButton}>
138
- <TextButton onPress={handleCopyPress}>Copy</TextButton>
139
- {message?.mine && <TextButton onPress={() => handleEditPress()}>Edit</TextButton>}
140
- {message?.mine && (
141
- <TextButton onPress={() => handleViewReadReceiptsPress()}>
142
- View read receipts
143
- </TextButton>
144
- )}
145
- {(message?.mine || canDeleteNonAuthoredMessages) && (
146
- <TextButton
147
- appearance="danger"
148
- onPress={() => handleDeleteMessage()}
149
- disabled={isPending}
150
- >
151
- Delete
152
- </TextButton>
153
- )}
154
- </View>
141
+ <FormSheet.Action
142
+ onPress={handleCopyPress}
143
+ title="Copy text"
144
+ iconName="services.fileCopy"
145
+ accessibilityHint="Copies text and links to clipboard"
146
+ />
147
+ {message?.mine && (
148
+ <FormSheet.Action
149
+ onPress={() => handleEditPress()}
150
+ title="Edit message"
151
+ iconName="accounts.editor"
152
+ accessibilityHint="Opens existing text in the message form input."
153
+ />
154
+ )}
155
+ {message?.mine && (
156
+ <FormSheet.Action
157
+ onPress={() => handleViewReadReceiptsPress()}
158
+ title="View read receipts"
159
+ iconName="general.checkPerson"
160
+ accessibilityHint="Opens a modal with a list of people who read your message."
161
+ />
162
+ )}
163
+ {(message?.mine || canDeleteNonAuthoredMessages) && (
164
+ <FormSheet.Action
165
+ onPress={() => handleDeleteConfirm()}
166
+ title="Delete message"
167
+ iconName="publishing.trash"
168
+ appearance="danger"
169
+ disabled={isPending}
170
+ accessibilityHint="Opens a confirmation alert to delete this message permanently."
171
+ />
172
+ )}
155
173
  </View>
156
- </View>
174
+ </FormSheet.Root>
157
175
  )
158
176
  }
159
177
 
@@ -173,15 +191,14 @@ const Reaction = ({
173
191
  style={[reactionStyles.reaction, styles.reaction]}
174
192
  onPress={onPress}
175
193
  >
176
- <Text style={reactionStyles.reactionEmoji}>{REACTION_EMOJIS[reaction.value]}</Text>
194
+ <Text style={styles.reactionEmoji}>{REACTION_EMOJIS[reaction.value]}</Text>
177
195
  </PlatformPressable>
178
196
  )
179
197
  }
180
198
 
181
199
  const useStyles = () => {
182
- const theme = useTheme()
183
- const { height } = useWindowDimensions()
184
- const { bottom } = useSafeAreaInsets()
200
+ const { colors } = useTheme()
201
+
185
202
  const btnBorderWidth = 1
186
203
  const baseSize = 44
187
204
  const reactionBtnSize = Platform.select({
@@ -190,13 +207,15 @@ const useStyles = () => {
190
207
  })
191
208
 
192
209
  return StyleSheet.create({
193
- container: {
194
- justifyContent: 'flex-start',
195
- paddingTop: 12,
196
- paddingBottom: bottom,
197
- width: '100%',
198
- backgroundColor: theme.colors.fillColorNeutral100Inverted,
199
- height,
210
+ reactionList: {
211
+ flexDirection: 'row',
212
+ justifyContent: 'center',
213
+ alignItems: 'center',
214
+ gap: 16,
215
+ paddingTop: 8,
216
+ paddingBottom: 16,
217
+ borderBottomColor: colors.borderColorDefaultBase,
218
+ borderBottomWidth: 1,
200
219
  },
201
220
  reaction: {
202
221
  height: reactionBtnSize,
@@ -205,15 +224,11 @@ const useStyles = () => {
205
224
  borderRadius: 32,
206
225
  justifyContent: 'center',
207
226
  },
208
- reactionList: {
209
- justifyContent: 'center',
210
- gap: 24,
211
- paddingVertical: 12,
212
- flexDirection: 'row',
213
- borderBottomColor: theme.colors.fillColorNeutral040,
214
- borderBottomWidth: 1,
227
+ reactionEmoji: {
228
+ fontSize: 24,
229
+ },
230
+ actions: {
231
+ paddingTop: 4,
215
232
  },
216
- actions: { flex: 1 },
217
- actionButton: { padding: 12, paddingBottom: 100, gap: 12 },
218
233
  })
219
234
  }