@planningcenter/chat-react-native 3.42.3-qa-856.0 → 3.42.3-qa-858.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 (33) hide show
  1. package/build/components/display/platform_modal_header_buttons.d.ts +28 -8
  2. package/build/components/display/platform_modal_header_buttons.d.ts.map +1 -1
  3. package/build/components/display/platform_modal_header_buttons.js +86 -45
  4. package/build/components/display/platform_modal_header_buttons.js.map +1 -1
  5. package/build/components/primitive/form_sheet.js +2 -2
  6. package/build/components/primitive/form_sheet.js.map +1 -1
  7. package/build/navigation/index.d.ts +48 -51
  8. package/build/navigation/index.d.ts.map +1 -1
  9. package/build/navigation/index.js +38 -48
  10. package/build/navigation/index.js.map +1 -1
  11. package/build/screens/bug_report_screen.d.ts.map +1 -1
  12. package/build/screens/bug_report_screen.js +9 -3
  13. package/build/screens/bug_report_screen.js.map +1 -1
  14. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  15. package/build/screens/conversation_details_screen.js +2 -1
  16. package/build/screens/conversation_details_screen.js.map +1 -1
  17. package/build/screens/conversation_screen.d.ts.map +1 -1
  18. package/build/screens/conversation_screen.js +6 -2
  19. package/build/screens/conversation_screen.js.map +1 -1
  20. package/build/screens/message_report_screen.js +5 -3
  21. package/build/screens/message_report_screen.js.map +1 -1
  22. package/build/screens/notification_settings_screen.d.ts.map +1 -1
  23. package/build/screens/notification_settings_screen.js +0 -2
  24. package/build/screens/notification_settings_screen.js.map +1 -1
  25. package/package.json +4 -3
  26. package/src/components/display/platform_modal_header_buttons.tsx +145 -77
  27. package/src/components/primitive/form_sheet.tsx +2 -2
  28. package/src/navigation/index.tsx +46 -53
  29. package/src/screens/bug_report_screen.tsx +9 -3
  30. package/src/screens/conversation_details_screen.tsx +2 -1
  31. package/src/screens/conversation_screen.tsx +7 -2
  32. package/src/screens/message_report_screen.tsx +5 -3
  33. package/src/screens/notification_settings_screen.tsx +1 -4
@@ -1,15 +1,15 @@
1
- import { HeaderBackButton, HeaderButton } from '@react-navigation/elements'
1
+ import { HeaderBackButton, HeaderButton, PlatformPressable } from '@react-navigation/elements'
2
2
  import { useNavigation } from '@react-navigation/native'
3
3
  import type {
4
- NativeStackHeaderLeftProps,
5
- NativeStackHeaderRightProps,
4
+ NativeStackHeaderItemProps,
6
5
  NativeStackNavigationOptions,
7
6
  } from '@react-navigation/native-stack'
8
7
  import { useLayoutEffect } from 'react'
9
8
  import { ColorValue, Platform, StyleSheet } from 'react-native'
10
- import { useTheme } from '../../hooks'
11
- import { isLiquidGlassEnabled } from '../../utils/liquid_glass'
12
- import { Icon } from './icon'
9
+ import type { SFSymbol } from 'sf-symbols-typescript'
10
+ import { useFontScale, useTheme } from '../../hooks'
11
+ import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../../utils'
12
+ import { Icon, type IconString } from './icon'
13
13
  import { TextButton } from './text_button'
14
14
 
15
15
  interface HeaderTextButtonProps {
@@ -72,14 +72,10 @@ export const HeaderDismissButton = ({
72
72
 
73
73
  const createTextHeaderRight =
74
74
  ({ onPress, title, disabled }: HeaderTextButtonProps) =>
75
- (props: NativeStackHeaderRightProps) => (
75
+ (props: NativeStackHeaderItemProps) => (
76
76
  <HeaderTextButton {...props} onPress={onPress} title={title} disabled={disabled} />
77
77
  )
78
78
 
79
- const createDismissHeaderLeft =
80
- ({ onPress, title }: { onPress: () => void; title: string }) =>
81
- () => <HeaderDismissButton title={title} onPress={onPress} />
82
-
83
79
  export type OptionsDecorator = (
84
80
  options: NativeStackNavigationOptions
85
81
  ) => NativeStackNavigationOptions
@@ -95,9 +91,6 @@ interface TextButtonDecoratorProps {
95
91
  disabled?: boolean
96
92
  }
97
93
 
98
- // Native bar items pick up liquid glass on iOS 26; hosts on an older
99
- // react-navigation ignore the unstable option and keep using the
100
- // headerRight/headerLeft fallbacks, as does Android.
101
94
  export const withRightText =
102
95
  ({ text, onPress, disabled }: TextButtonDecoratorProps): OptionsDecorator =>
103
96
  options => ({
@@ -106,95 +99,159 @@ export const withRightText =
106
99
  unstable_headerRightItems: () => [{ type: 'button' as const, label: text, onPress, disabled }],
107
100
  })
108
101
 
109
- export const withLeftText =
110
- ({ text, onPress }: TextButtonDecoratorProps): OptionsDecorator =>
111
- options => ({
112
- ...options,
113
- headerLeft: createDismissHeaderLeft({ title: text, onPress }),
114
- unstable_headerLeftItems: () => [{ type: 'button' as const, label: text, onPress }],
115
- })
102
+ const HEADER_ICON_SIZE = 17
103
+ const HEADER_GLASS_BUTTON_SIZE = 44
104
+ const HEADER_BAR_INSET = 16
105
+ const HEADER_SLOT_GAP = 12
116
106
 
117
- const renderCloseImage = ({ tintColor }: { tintColor: string }) => (
118
- <Icon name="general.x" size={18} color={tintColor} />
119
- )
107
+ export const useHeaderSlotReserve = (): number => {
108
+ const fontScale = useFontScale({ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER_LANDMARK })
120
109
 
121
- const createCloseHeaderRight =
122
- ({ onPress }: { onPress: () => void }) =>
123
- (props: NativeStackHeaderRightProps) => (
124
- <HeaderBackButton backImage={renderCloseImage} onPress={onPress} {...props} />
110
+ return HEADER_BAR_INSET + HEADER_GLASS_BUTTON_SIZE * fontScale + HEADER_SLOT_GAP
111
+ }
112
+
113
+ const HEADER_FALLBACK_HIT_SLOP = 14
114
+
115
+ const iconStyles = StyleSheet.create({
116
+ disabled: {
117
+ opacity: 0.4,
118
+ },
119
+ fallbackButton: {
120
+ alignItems: 'center',
121
+ justifyContent: 'center',
122
+ paddingHorizontal: 8,
123
+ },
124
+ fallbackButtonLeft: {
125
+ marginRight: Platform.select({ android: 16, default: 0 }),
126
+ },
127
+ })
128
+
129
+ const SF_SYMBOL_NAMES: Partial<Record<IconString, SFSymbol>> = {
130
+ 'general.x': 'xmark',
131
+ 'general.check': 'checkmark',
132
+ 'general.upArrow': 'arrow.up',
133
+ 'general.leftChevron': 'chevron.left',
134
+ }
135
+
136
+ const sfSymbolFor = (icon: IconString): SFSymbol => {
137
+ const name = SF_SYMBOL_NAMES[icon]
138
+
139
+ if (!name) throw new Error(`No SF Symbol mapped for icon "${icon}" — add one to SF_SYMBOL_NAMES.`)
140
+
141
+ return name
142
+ }
143
+
144
+ export interface HeaderIconProps {
145
+ icon: IconString
146
+ accessibilityLabel: string
147
+ onPress: () => void
148
+ disabled?: boolean
149
+ prominent?: boolean
150
+ tintColor?: ColorValue
151
+ }
152
+
153
+ const createHeaderIconItem = ({
154
+ icon,
155
+ accessibilityLabel,
156
+ onPress,
157
+ disabled,
158
+ prominent,
159
+ tintColor,
160
+ }: HeaderIconProps) => ({
161
+ type: 'button' as const,
162
+ label: '',
163
+ accessibilityLabel,
164
+ onPress,
165
+ disabled,
166
+ icon: { type: 'sfSymbol' as const, name: sfSymbolFor(icon) },
167
+ ...(prominent ? { variant: 'prominent' as const, tintColor } : {}),
168
+ })
169
+
170
+ const createHeaderIconElement =
171
+ (position: 'left' | 'right') =>
172
+ ({ icon, accessibilityLabel, onPress, disabled, tintColor }: HeaderIconProps) =>
173
+ () => (
174
+ <PlatformPressable
175
+ onPress={onPress}
176
+ disabled={disabled}
177
+ hitSlop={HEADER_FALLBACK_HIT_SLOP}
178
+ accessibilityRole="button"
179
+ accessibilityLabel={accessibilityLabel}
180
+ style={[
181
+ iconStyles.fallbackButton,
182
+ position === 'left' && iconStyles.fallbackButtonLeft,
183
+ disabled && iconStyles.disabled,
184
+ ]}
185
+ >
186
+ <Icon name={icon} size={HEADER_ICON_SIZE} color={tintColor} />
187
+ </PlatformPressable>
125
188
  )
126
189
 
127
- // The native slot takes only SF Symbols or bundled rasters — we have no way
128
- // to systematize rasterizing our SVG icon library, and xmark matches
129
- // general.x closely. The fallback keeps our icon.
130
- export const withRightClose =
131
- ({ onPress }: { onPress: () => void }): OptionsDecorator =>
190
+ export const withRightIcon =
191
+ (props: HeaderIconProps): OptionsDecorator =>
132
192
  options => ({
133
193
  ...options,
134
- headerRight: createCloseHeaderRight({ onPress }),
135
- unstable_headerRightItems: () => [
136
- {
137
- type: 'button' as const,
138
- label: 'Close',
139
- icon: { type: 'sfSymbol' as const, name: 'xmark' as const },
140
- onPress,
141
- },
142
- ],
194
+ headerRight: createHeaderIconElement('right')(props),
195
+ unstable_headerRightItems: () => [createHeaderIconItem(props)],
143
196
  })
144
197
 
145
- // The default back icon reserves trailing space for a label that
146
- // displayMode="minimal" never shows; a custom image skips it so the glass
147
- // capsule hugs the glyph.
148
- const renderGlassBackImage = ({ tintColor }: { tintColor: string }) => (
149
- <Icon name="general.leftChevron" size={18} color={tintColor} />
150
- )
198
+ export const withLeftIcon =
199
+ (props: HeaderIconProps): OptionsDecorator =>
200
+ options => ({
201
+ ...options,
202
+ headerLeft: createHeaderIconElement('left')(props),
203
+ unstable_headerLeftItems: () => [createHeaderIconItem(props)],
204
+ })
205
+
206
+ export const withRightClose = ({ onPress }: { onPress: () => void }) =>
207
+ withRightIcon({ icon: 'general.x', accessibilityLabel: 'Close', onPress })
208
+
209
+ export const withLeftClose = ({ onPress }: { onPress: () => void }) =>
210
+ withLeftIcon({ icon: 'general.x', accessibilityLabel: 'Close', onPress })
151
211
 
152
- // The iOS 26 glass capsule manages its own insets negative margins clip
153
- // the button against the capsule edge.
154
- export const createMinimalHeaderBackButton =
212
+ export const withRightDone = ({ onPress }: { onPress: () => void }) =>
213
+ withRightIcon({ icon: 'general.check', accessibilityLabel: 'Done', onPress })
214
+
215
+ const createMinimalHeaderBackElement =
155
216
  ({ onPress }: { onPress: () => void }) =>
156
- (props: NativeStackHeaderLeftProps) => (
217
+ (props: NativeStackHeaderItemProps) => (
157
218
  <HeaderBackButton
158
- style={
159
- isLiquidGlassEnabled()
160
- ? undefined
161
- : {
162
- marginLeft: Platform.select({ ios: -8, default: -3 }),
163
- marginRight: Platform.select({ ios: 0, default: 30 }),
164
- }
165
- }
166
- backImage={isLiquidGlassEnabled() ? renderGlassBackImage : undefined}
219
+ style={Platform.select({
220
+ ios: { marginLeft: -8 },
221
+ default: { marginLeft: -3, marginRight: 30 },
222
+ })}
167
223
  displayMode="minimal"
168
224
  onPress={onPress}
169
225
  {...props}
170
226
  />
171
227
  )
172
228
 
173
- // Under glass the native minimal back button suffices; pre-26 needs the
174
- // custom button and its layout nudges.
175
229
  export const withMinimalBack =
176
230
  ({ onPress }: { onPress: () => void }): OptionsDecorator =>
177
231
  options => ({
178
232
  ...options,
179
- ...(isLiquidGlassEnabled() ? {} : { headerLeft: createMinimalHeaderBackButton({ onPress }) }),
233
+ headerLeft: createMinimalHeaderBackElement({ onPress }),
234
+ unstable_headerLeftItems: () => [
235
+ createHeaderIconItem({ icon: 'general.leftChevron', accessibilityLabel: 'Back', onPress }),
236
+ ],
180
237
  })
181
238
 
182
- interface UseHeaderRightOptions {
183
- title: string
239
+ type UseHeaderRightOptions = {
184
240
  onPress: () => void
185
241
  disabled?: boolean
186
242
  enabled?: boolean
187
- }
243
+ } & (
244
+ | (Omit<HeaderIconProps, 'tintColor'> & { title?: never })
245
+ | { title: string; icon?: never; accessibilityLabel?: never; prominent?: never }
246
+ )
188
247
 
189
- // For screens whose header button depends on live state (form validity,
190
- // mutation callbacks). Static buttons belong in the navigator options.
191
- export const useHeaderRight = ({
192
- title,
193
- onPress,
194
- disabled,
195
- enabled = true,
196
- }: UseHeaderRightOptions) => {
248
+ export const useHeaderRight = (options: UseHeaderRightOptions) => {
197
249
  const navigation = useNavigation()
250
+ const { colors } = useTheme()
251
+ const { onPress, disabled, enabled = true } = options
252
+ const icon = options.icon
253
+ const label = options.icon !== undefined ? options.accessibilityLabel : options.title
254
+ const prominent = options.icon !== undefined ? options.prominent : undefined
198
255
 
199
256
  useLayoutEffect(() => {
200
257
  if (!enabled) {
@@ -202,8 +259,19 @@ export const useHeaderRight = ({
202
259
  return
203
260
  }
204
261
 
205
- navigation.setOptions(withRightText({ text: title, onPress, disabled })({}))
206
- }, [disabled, enabled, navigation, onPress, title])
262
+ const decorate = icon
263
+ ? withRightIcon({
264
+ icon,
265
+ accessibilityLabel: label,
266
+ onPress,
267
+ disabled,
268
+ prominent,
269
+ tintColor: prominent ? colors.interaction : undefined,
270
+ })
271
+ : withRightText({ text: label, onPress, disabled })
272
+
273
+ navigation.setOptions(decorate({}))
274
+ }, [colors.interaction, disabled, enabled, icon, label, navigation, onPress, prominent])
207
275
  }
208
276
 
209
277
  const useStyles = () => {
@@ -12,7 +12,7 @@ import {
12
12
  } from 'react-native'
13
13
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
14
14
  import { useTheme } from '../../hooks'
15
- import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../../utils'
15
+ import { isLiquidGlassEnabled, MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../../utils'
16
16
  import type { ButtonProps, TextButtonProps } from '../display'
17
17
  import { Button, Heading, Icon, IconString, Text, TextButton } from '../display'
18
18
 
@@ -35,7 +35,7 @@ export const getFormSheetScreenOptions = ({
35
35
  presentation: 'formSheet',
36
36
  headerShown: false,
37
37
  sheetAllowedDetents,
38
- sheetCornerRadius: 16,
38
+ sheetCornerRadius: isLiquidGlassEnabled() ? undefined : 16,
39
39
  sheetGrabberVisible: true,
40
40
  headerTitle,
41
41
  ...rest,
@@ -2,15 +2,15 @@ import { StaticParamList } from '@react-navigation/native'
2
2
  import {
3
3
  createNativeStackNavigator,
4
4
  NativeStackHeaderRightProps,
5
+ NativeStackNavigationOptions,
5
6
  } from '@react-navigation/native-stack'
6
7
  import { CardStyleInterpolators } from '@react-navigation/stack'
7
8
  import React from 'react'
8
9
  import {
9
10
  composeOptions,
10
- createMinimalHeaderBackButton,
11
11
  withMinimalBack,
12
12
  withRightClose,
13
- withRightText,
13
+ withRightDone,
14
14
  } from '../components/display/platform_modal_header_buttons'
15
15
  import { useDirectMessagesEnabled, useQualifiedByAge } from '../hooks'
16
16
  import {
@@ -79,13 +79,17 @@ import {
79
79
  SystemMessagePeopleScreenOptions,
80
80
  } from '../screens/system_message_people_screen'
81
81
  import { TeamConversationScreen } from '../screens/team_conversation_screen'
82
+ import { isLiquidGlassEnabled } from '../utils/liquid_glass'
82
83
  import { ScreenLayoutWithChatAccessGate } from './screenLayout'
83
84
 
85
+ const globalScreenOptions: () => NativeStackNavigationOptions = () => ({
86
+ headerBackButtonDisplayMode: 'minimal',
87
+ ...(isLiquidGlassEnabled() ? { headerShadowVisible: false } : {}),
88
+ })
89
+
84
90
  export const NewConversationStack = createNativeStackNavigator({
85
91
  initialRouteName: 'ConversationNewEntry',
86
- screenOptions: {
87
- headerBackButtonDisplayMode: 'minimal',
88
- },
92
+ screenOptions: globalScreenOptions,
89
93
  screenLayout: ScreenLayoutWithChatAccessGate,
90
94
  screens: {
91
95
  ConversationNewEntry: {
@@ -96,7 +100,7 @@ export const NewConversationStack = createNativeStackNavigator({
96
100
  title: 'New conversation',
97
101
  headerBackVisible: false,
98
102
  },
99
- withRightText({ text: 'Cancel', onPress: navigation.goBack })
103
+ withRightClose({ onPress: navigation.goBack })
100
104
  ),
101
105
  },
102
106
  ConversationSelectGroupRecipients: {
@@ -106,8 +110,7 @@ export const NewConversationStack = createNativeStackNavigator({
106
110
  {
107
111
  title: 'New conversation',
108
112
  },
109
- withRightText({
110
- text: 'Cancel',
113
+ withRightClose({
111
114
  onPress: () => navigation.popTo('Conversations', route.params),
112
115
  })
113
116
  ),
@@ -119,8 +122,7 @@ export const NewConversationStack = createNativeStackNavigator({
119
122
  {
120
123
  title: 'New conversation',
121
124
  },
122
- withRightText({
123
- text: 'Cancel',
125
+ withRightClose({
124
126
  onPress: () => navigation.popTo('Conversations', route.params),
125
127
  })
126
128
  ),
@@ -133,8 +135,7 @@ export const NewConversationStack = createNativeStackNavigator({
133
135
  {
134
136
  title: 'New conversation',
135
137
  },
136
- withRightText({
137
- text: 'Cancel',
138
+ withRightClose({
138
139
  onPress: () => navigation.popTo('Conversations', route.params),
139
140
  })
140
141
  ),
@@ -151,8 +152,7 @@ export const NewConversationStack = createNativeStackNavigator({
151
152
  title: 'New conversation',
152
153
  headerLeft: () => null,
153
154
  },
154
- withRightText({
155
- text: 'Cancel',
155
+ withRightClose({
156
156
  onPress: () => {
157
157
  const { chat_group_graph_id, group_source_app_name } =
158
158
  route.params as ConversationSelectRecipientsParams
@@ -169,9 +169,7 @@ export const NewConversationStack = createNativeStackNavigator({
169
169
  })
170
170
 
171
171
  export const ChatStack = createNativeStackNavigator({
172
- screenOptions: {
173
- headerBackButtonDisplayMode: 'minimal',
174
- },
172
+ screenOptions: globalScreenOptions,
175
173
  screenLayout: ScreenLayoutWithChatAccessGate,
176
174
  screens: {
177
175
  Conversations: {
@@ -210,19 +208,19 @@ export const ChatStack = createNativeStackNavigator({
210
208
  </ConversationScreenTitle>
211
209
  )
212
210
  },
213
- headerLeft: createMinimalHeaderBackButton({
214
- onPress: () => {
215
- const params = route.params as ConversationRouteProps
216
- if ((params as ConversationFiltersParams)?.chat_group_graph_id) {
217
- // Ensure that conversations with a graph id pass them back to the conversation list
218
- const { chat_group_graph_id } = params
219
- navigation.popTo('Conversations', { chat_group_graph_id })
220
- } else {
221
- navigation.goBack()
222
- }
223
- },
224
- }),
225
211
  },
212
+ withMinimalBack({
213
+ onPress: () => {
214
+ const params = route.params as ConversationRouteProps
215
+ if ((params as ConversationFiltersParams)?.chat_group_graph_id) {
216
+ // Ensure that conversations with a graph id pass them back to the conversation list
217
+ const { chat_group_graph_id } = params
218
+ navigation.popTo('Conversations', { chat_group_graph_id })
219
+ } else {
220
+ navigation.goBack()
221
+ }
222
+ },
223
+ }),
226
224
  withRightClose({ onPress: () => navigation.getParent()?.goBack() })
227
225
  ),
228
226
  },
@@ -251,7 +249,7 @@ export const ChatStack = createNativeStackNavigator({
251
249
  presentation: 'modal',
252
250
  title: 'Conversation details',
253
251
  },
254
- withRightText({ text: 'Done', onPress: navigation.goBack })
252
+ withRightDone({ onPress: navigation.goBack })
255
253
  ),
256
254
  },
257
255
  AvatarPicker: {
@@ -261,46 +259,41 @@ export const ChatStack = createNativeStackNavigator({
261
259
  ChatSettings: {
262
260
  screen: SettingsScreen,
263
261
  options: ({ navigation }) =>
264
- composeOptions(
265
- {
266
- title: 'Chat settings',
267
- headerBackVisible: false,
268
- },
269
- withRightText({ text: 'Done', onPress: navigation.goBack })
270
- ),
262
+ composeOptions({ title: 'Chat settings' }, withMinimalBack({ onPress: navigation.goBack })),
271
263
  },
272
264
  NotificationSettings: {
273
265
  screen: NotificationSettingsScreen,
274
266
  options: ({ navigation }) =>
275
- composeOptions(
276
- {
277
- title: 'Chat',
278
- headerBackVisible: false,
279
- },
280
- withRightText({ text: 'Done', onPress: navigation.goBack })
281
- ),
267
+ navigation.getState().index === 0
268
+ ? composeOptions(
269
+ { title: 'Chat notifications' },
270
+ withRightClose({ onPress: navigation.goBack })
271
+ )
272
+ : composeOptions(
273
+ { title: 'Chat notifications' },
274
+ withMinimalBack({ onPress: navigation.goBack })
275
+ ),
282
276
  },
283
277
  PreferredAppSelection: {
284
278
  screen: PreferredAppSelectionScreen,
285
- options: {
286
- title: 'Preferred app',
287
- },
279
+ options: ({ navigation }) =>
280
+ composeOptions({ title: 'Preferred app' }, withMinimalBack({ onPress: navigation.goBack })),
288
281
  },
289
282
  GroupNotificationSettings: {
290
283
  screen: GroupNotificationSettingsScreen,
291
284
  options: ({ route, navigation }) => {
292
285
  const title = (route.params as { title?: string })?.title || 'Group Settings'
293
286
 
294
- // Done only shows when this is the entry screen (e.g. deep links);
295
- // deeper in the stack the back button covers dismissal.
287
+ // The dismiss button only shows when this is the entry screen (e.g.
288
+ // deep links); deeper in the stack the back button covers dismissal.
296
289
  return navigation.getState().index === 0
297
290
  ? composeOptions(
298
291
  {
299
292
  title,
300
293
  },
301
- withRightText({ text: 'Done', onPress: navigation.goBack })
294
+ withRightClose({ onPress: navigation.goBack })
302
295
  )
303
- : { title }
296
+ : composeOptions({ title }, withMinimalBack({ onPress: navigation.goBack }))
304
297
  },
305
298
  },
306
299
  GroupNotificationLevelSelect: {
@@ -328,7 +321,7 @@ export const ChatStack = createNativeStackNavigator({
328
321
  title: 'New conversation',
329
322
  headerLeft: () => null,
330
323
  },
331
- withRightText({ text: 'Cancel', onPress: () => navigation.getParent()?.goBack() })
324
+ withRightClose({ onPress: () => navigation.getParent()?.goBack() })
332
325
  ),
333
326
  },
334
327
  SendGiphy: {
@@ -377,7 +370,7 @@ export const ChatStack = createNativeStackNavigator({
377
370
  headerBackVisible: false,
378
371
  presentation: 'modal',
379
372
  },
380
- withRightText({ text: 'Close', onPress: navigation.goBack })
373
+ withRightClose({ onPress: navigation.goBack })
381
374
  ),
382
375
  },
383
376
  NotFound: {
@@ -28,7 +28,7 @@ import {
28
28
  composeOptions,
29
29
  HeaderDismissButton,
30
30
  useHeaderRight,
31
- withLeftText,
31
+ withLeftClose,
32
32
  } from '../components/display/platform_modal_header_buttons'
33
33
  import { VideoAttachmentPreview } from '../components/display/video_attachment_preview'
34
34
  import { DefaultLoading } from '../components/page/loading'
@@ -60,7 +60,7 @@ export const BugReportScreenOptions = ({
60
60
  }: NativeStackScreenProps<any>): NativeStackNavigationOptions =>
61
61
  composeOptions(
62
62
  { presentation: 'modal', title: 'Report a bug' },
63
- withLeftText({ text: 'Cancel', onPress: () => navigation.goBack() })
63
+ withLeftClose({ onPress: () => navigation.goBack() })
64
64
  )
65
65
 
66
66
  interface Attachment {
@@ -158,7 +158,13 @@ export function BugReportScreen() {
158
158
  return ImagePicker.openImageLibraryAsync().then(result => uploadImagePickerResult(result))
159
159
  }
160
160
 
161
- useHeaderRight({ title: 'Submit', onPress: handleSubmit, disabled: !formValid })
161
+ useHeaderRight({
162
+ icon: 'general.upArrow',
163
+ accessibilityLabel: 'Submit',
164
+ prominent: true,
165
+ onPress: handleSubmit,
166
+ disabled: !formValid,
167
+ })
162
168
 
163
169
  if (status === 'pending') {
164
170
  return (
@@ -223,7 +223,8 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
223
223
  )
224
224
 
225
225
  useHeaderRight({
226
- title: 'Done',
226
+ icon: 'general.check',
227
+ accessibilityLabel: 'Done',
227
228
  onPress: () => {
228
229
  saveTitle({ title: trimmedTitle || conversation.title })
229
230
  navigation.goBack()
@@ -9,7 +9,7 @@ import {
9
9
  useRoute,
10
10
  } from '@react-navigation/native'
11
11
  import React, { useCallback, useEffect, useRef, useState } from 'react'
12
- import { FlatList, Platform, StyleSheet, View } from 'react-native'
12
+ import { FlatList, Platform, StyleSheet, useWindowDimensions, View } from 'react-native'
13
13
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
14
14
  import { Badge, Icon, Text } from '../components'
15
15
  import { ConversationStartBanner } from '../components/conversation/conversation_start_banner'
@@ -25,6 +25,7 @@ import { ReplyShadowMessage } from '../components/conversation/reply_shadow_mess
25
25
  import { SystemMessage } from '../components/conversation/system_message'
26
26
  import { TypingIndicator } from '../components/conversation/typing_indicator'
27
27
  import { KeyboardView } from '../components/display/keyboard_view'
28
+ import { useHeaderSlotReserve } from '../components/display/platform_modal_header_buttons'
28
29
  import BlankState from '../components/primitive/blank_state_primitive'
29
30
  import { ConversationContextProvider } from '../contexts/conversation_context'
30
31
  import { useTheme } from '../hooks'
@@ -374,11 +375,15 @@ export const ConversationScreenTitle = ({
374
375
  }
375
376
 
376
377
  const usePressableHeaderStyle = () => {
378
+ const { width } = useWindowDimensions()
379
+ const slotReserve = useHeaderSlotReserve()
380
+ const headerContainerStyle = { maxWidth: width - slotReserve * 2 }
381
+
377
382
  return StyleSheet.create({
378
383
  container: {
379
384
  alignItems: Platform.select({ android: 'flex-start', default: 'center' }),
380
- marginRight: Platform.select({ ios: 20, default: 16 }),
381
385
  flex: 1,
386
+ ...headerContainerStyle,
382
387
  },
383
388
  titleWrapper: {
384
389
  alignItems: 'center',
@@ -10,7 +10,7 @@ import { Spinner, Text, KeyboardView } from '../components'
10
10
  import {
11
11
  composeOptions,
12
12
  useHeaderRight,
13
- withLeftText,
13
+ withLeftClose,
14
14
  } from '../components/display/platform_modal_header_buttons'
15
15
  import BlankState from '../components/primitive/blank_state_primitive'
16
16
  import { useTheme } from '../hooks'
@@ -37,7 +37,7 @@ export const MessageReportScreenOptions = ({
37
37
 
38
38
  return composeOptions(
39
39
  { presentation: 'modal', title: 'Report message' },
40
- withLeftText({ text: 'Cancel', onPress: handleClose })
40
+ withLeftClose({ onPress: handleClose })
41
41
  )
42
42
  }
43
43
 
@@ -106,7 +106,9 @@ function MessageReportScreenContent({
106
106
  const formVisible = status !== 'pending' && status !== 'success'
107
107
 
108
108
  useHeaderRight({
109
- title: 'Submit',
109
+ icon: 'general.upArrow',
110
+ accessibilityLabel: 'Submit',
111
+ prominent: true,
110
112
  onPress: handleSubmit,
111
113
  disabled: !isFormValid,
112
114
  enabled: formVisible,
@@ -1,10 +1,9 @@
1
1
  import { PlatformPressable } from '@react-navigation/elements'
2
2
  import { StaticScreenProps, useNavigation } from '@react-navigation/native'
3
- import React, { type ReactNode } from 'react'
3
+ import { type ReactNode } from 'react'
4
4
  import { FlatList, Platform, StyleSheet, View, type ViewProps, type ViewStyle } from 'react-native'
5
5
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
6
6
  import { Badge, Heading, Icon, Text } from '../components'
7
- import { useHeaderRight } from '../components/display/platform_modal_header_buttons'
8
7
  import { useTheme } from '../hooks'
9
8
  import { isDefined } from '../types'
10
9
  import { chatTypeLabel } from '../utils'
@@ -51,8 +50,6 @@ export function NotificationSettingsScreen({}: NotificationSettingsScreenProps)
51
50
  const { data: chatTypes } = useChatTypes()
52
51
  const { data: groups } = useGroups()
53
52
 
54
- useHeaderRight({ title: 'Done', onPress: navigation.goBack })
55
-
56
53
  // Build section data
57
54
  const listData = [
58
55
  {