@planningcenter/chat-react-native 3.44.0-rc.0 → 3.44.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 (63) hide show
  1. package/build/components/display/platform_modal_header_buttons.d.ts +42 -14
  2. package/build/components/display/platform_modal_header_buttons.d.ts.map +1 -1
  3. package/build/components/display/platform_modal_header_buttons.js +107 -27
  4. package/build/components/display/platform_modal_header_buttons.js.map +1 -1
  5. package/build/components/primitive/form_sheet.d.ts +8 -2
  6. package/build/components/primitive/form_sheet.d.ts.map +1 -1
  7. package/build/components/primitive/form_sheet.js +10 -4
  8. package/build/components/primitive/form_sheet.js.map +1 -1
  9. package/build/navigation/index.d.ts +76 -149
  10. package/build/navigation/index.d.ts.map +1 -1
  11. package/build/navigation/index.js +67 -76
  12. package/build/navigation/index.js.map +1 -1
  13. package/build/screens/attachment_actions/attachment_actions_screen.d.ts +1 -1
  14. package/build/screens/attachment_actions/attachment_actions_screen.d.ts.map +1 -1
  15. package/build/screens/avatar_picker/avatar_picker_screen.d.ts +2 -2
  16. package/build/screens/avatar_picker/avatar_picker_screen.d.ts.map +1 -1
  17. package/build/screens/bug_report_screen.d.ts.map +1 -1
  18. package/build/screens/bug_report_screen.js +10 -13
  19. package/build/screens/bug_report_screen.js.map +1 -1
  20. package/build/screens/conversation/message_read_receipts_screen.d.ts +1 -1
  21. package/build/screens/conversation/message_read_receipts_screen.d.ts.map +1 -1
  22. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  23. package/build/screens/conversation_details_screen.js +11 -12
  24. package/build/screens/conversation_details_screen.js.map +1 -1
  25. package/build/screens/conversation_filter_recipients/conversation_filter_recipients_screen.d.ts +1 -1
  26. package/build/screens/conversation_filter_recipients/conversation_filter_recipients_screen.d.ts.map +1 -1
  27. package/build/screens/conversation_filters_screen.d.ts +1 -1
  28. package/build/screens/conversation_filters_screen.d.ts.map +1 -1
  29. package/build/screens/conversation_filters_screen.js +18 -2
  30. package/build/screens/conversation_filters_screen.js.map +1 -1
  31. package/build/screens/conversation_notification_level_select_screen.d.ts +1 -1
  32. package/build/screens/conversation_notification_level_select_screen.d.ts.map +1 -1
  33. package/build/screens/conversation_screen.d.ts.map +1 -1
  34. package/build/screens/conversation_screen.js +6 -2
  35. package/build/screens/conversation_screen.js.map +1 -1
  36. package/build/screens/conversation_select_type_screen.d.ts +1 -1
  37. package/build/screens/conversation_select_type_screen.d.ts.map +1 -1
  38. package/build/screens/group_notification_level_select_screen.d.ts +1 -1
  39. package/build/screens/group_notification_level_select_screen.d.ts.map +1 -1
  40. package/build/screens/message_actions_screen.d.ts +1 -1
  41. package/build/screens/message_actions_screen.d.ts.map +1 -1
  42. package/build/screens/message_report_screen.d.ts.map +1 -1
  43. package/build/screens/message_report_screen.js +14 -18
  44. package/build/screens/message_report_screen.js.map +1 -1
  45. package/build/screens/notification_settings_screen.d.ts.map +1 -1
  46. package/build/screens/notification_settings_screen.js +0 -14
  47. package/build/screens/notification_settings_screen.js.map +1 -1
  48. package/build/screens/reactions_screen.d.ts +1 -1
  49. package/build/screens/reactions_screen.d.ts.map +1 -1
  50. package/build/screens/send_giphy_screen.d.ts +1 -1
  51. package/build/screens/send_giphy_screen.d.ts.map +1 -1
  52. package/build/screens/system_message_people_screen.d.ts +1 -1
  53. package/build/screens/system_message_people_screen.d.ts.map +1 -1
  54. package/package.json +3 -2
  55. package/src/components/display/platform_modal_header_buttons.tsx +196 -45
  56. package/src/components/primitive/form_sheet.tsx +19 -15
  57. package/src/navigation/index.tsx +147 -151
  58. package/src/screens/bug_report_screen.tsx +16 -15
  59. package/src/screens/conversation_details_screen.tsx +11 -17
  60. package/src/screens/conversation_filters_screen.tsx +25 -2
  61. package/src/screens/conversation_screen.tsx +7 -2
  62. package/src/screens/message_report_screen.tsx +20 -21
  63. package/src/screens/notification_settings_screen.tsx +1 -21
@@ -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
 
@@ -22,24 +22,28 @@ import { Button, Heading, Icon, IconString, Text, TextButton } from '../display'
22
22
 
23
23
  /**
24
24
  * Screen options for the formsheet's NativeStackNavigation route
25
+ *
26
+ * Returns a factory rather than an object because callers assign it at module
27
+ * scope, which runs before ChatProvider reports the host's liquid glass
28
+ * capability. React Navigation calls the factory per render, so the sheet
29
+ * corner radius is resolved once that capability is known.
30
+ *
25
31
  * @param {string} options.headerTitle - Doesn't show in UI but good to have for semantic reasons.
26
32
  * @param {number[]} [options.sheetAllowedDetents] - Controls the height increments the sheet grows to. To prevent bugs in Android, do not specify more then two values and use [0.94] instead of [1] to go full screen.
27
33
  * @param {NativeStackNavigationOptions} [options.rest] - Adds or overrides any valid NativeStackNavigationOptions
28
- * @returns {NativeStackNavigationOptions} Merged NativeStackNavigationOptions
34
+ * @returns {() => NativeStackNavigationOptions} Factory producing merged NativeStackNavigationOptions
29
35
  */
30
- export const getFormSheetScreenOptions = ({
31
- headerTitle,
32
- sheetAllowedDetents = [0.25],
33
- ...rest
34
- }: NativeStackNavigationOptions = {}): NativeStackNavigationOptions => ({
35
- presentation: 'formSheet',
36
- headerShown: false,
37
- sheetAllowedDetents,
38
- sheetCornerRadius: 16,
39
- sheetGrabberVisible: true,
40
- headerTitle,
41
- ...rest,
42
- })
36
+ export const getFormSheetScreenOptions =
37
+ ({ headerTitle, sheetAllowedDetents = [0.25], ...rest }: NativeStackNavigationOptions = {}) =>
38
+ (): NativeStackNavigationOptions => ({
39
+ presentation: 'formSheet',
40
+ headerShown: false,
41
+ sheetAllowedDetents,
42
+ sheetCornerRadius: isLiquidGlassEnabled() ? undefined : 16,
43
+ sheetGrabberVisible: true,
44
+ headerTitle,
45
+ ...rest,
46
+ })
43
47
 
44
48
  const FormSheet = {
45
49
  Root: FormSheetRoot,
@@ -1,18 +1,16 @@
1
- import { HeaderBackButton } from '@react-navigation/elements'
2
1
  import { StaticParamList } from '@react-navigation/native'
3
2
  import {
4
3
  createNativeStackNavigator,
5
4
  NativeStackHeaderRightProps,
5
+ NativeStackNavigationOptions,
6
6
  } from '@react-navigation/native-stack'
7
7
  import { CardStyleInterpolators } from '@react-navigation/stack'
8
8
  import React from 'react'
9
- import { Platform } from 'react-native'
10
- import { Icon } from '../components'
11
9
  import {
12
- composeHeaderOptions,
13
- createMinimalHeaderBackButton,
14
- HeaderDoneButton,
15
- HeaderTextButton,
10
+ composeOptions,
11
+ withMinimalBack,
12
+ withRightClose,
13
+ withRightDone,
16
14
  } from '../components/display/platform_modal_header_buttons'
17
15
  import { useDirectMessagesEligible, useQualifiedByAge } from '../hooks'
18
16
  import {
@@ -81,64 +79,66 @@ import {
81
79
  SystemMessagePeopleScreenOptions,
82
80
  } from '../screens/system_message_people_screen'
83
81
  import { TeamConversationScreen } from '../screens/team_conversation_screen'
82
+ import { isLiquidGlassEnabled } from '../utils/liquid_glass'
84
83
  import { ScreenLayoutWithChatAccessGate } from './screenLayout'
85
84
 
85
+ const globalScreenOptions: () => NativeStackNavigationOptions = () => ({
86
+ headerBackButtonDisplayMode: 'minimal',
87
+ ...(isLiquidGlassEnabled() ? { headerShadowVisible: false } : {}),
88
+ })
89
+
86
90
  export const NewConversationStack = createNativeStackNavigator({
87
91
  initialRouteName: 'ConversationNewEntry',
88
- screenOptions: {
89
- headerBackButtonDisplayMode: 'minimal',
90
- },
92
+ screenOptions: globalScreenOptions,
91
93
  screenLayout: ScreenLayoutWithChatAccessGate,
92
94
  screens: {
93
95
  ConversationNewEntry: {
94
96
  screen: ConversationNewEntryScreen,
95
- options: ({ navigation }) => ({
96
- title: 'New conversation',
97
- headerRight: (props: NativeStackHeaderRightProps) => (
98
- <HeaderTextButton {...props} onPress={navigation.goBack} title="Cancel" />
97
+ options: ({ navigation }) =>
98
+ composeOptions(
99
+ {
100
+ title: 'New conversation',
101
+ headerBackVisible: false,
102
+ },
103
+ withRightClose({ onPress: navigation.goBack })
99
104
  ),
100
- headerBackVisible: false,
101
- }),
102
105
  },
103
106
  ConversationSelectGroupRecipients: {
104
107
  screen: ConversationSelectGroupRecipientsScreen,
105
- options: ({ navigation, route }) => ({
106
- title: 'New conversation',
107
- headerRight: (props: NativeStackHeaderRightProps) => (
108
- <HeaderTextButton
109
- {...props}
110
- onPress={() => navigation.popTo('Conversations', route.params)}
111
- title="Cancel"
112
- />
108
+ options: ({ navigation, route }) =>
109
+ composeOptions(
110
+ {
111
+ title: 'New conversation',
112
+ },
113
+ withRightClose({
114
+ onPress: () => navigation.popTo('Conversations', route.params),
115
+ })
113
116
  ),
114
- }),
115
117
  },
116
118
  ConversationSelectTeamsILeadRecipients: {
117
119
  screen: ConversationSelectTeamsILeadRecipientsScreen,
118
- options: ({ navigation, route }) => ({
119
- title: 'New conversation',
120
- headerRight: (props: NativeStackHeaderRightProps) => (
121
- <HeaderTextButton
122
- {...props}
123
- onPress={() => navigation.popTo('Conversations', route.params)}
124
- title="Cancel"
125
- />
120
+ options: ({ navigation, route }) =>
121
+ composeOptions(
122
+ {
123
+ title: 'New conversation',
124
+ },
125
+ withRightClose({
126
+ onPress: () => navigation.popTo('Conversations', route.params),
127
+ })
126
128
  ),
127
- }),
128
129
  },
129
130
  ConversationSelectDirectMessageRecipients: {
130
131
  screen: ConversationSelectDirectMessageRecipientsScreen,
131
132
  if: useDirectMessagesEligible,
132
- options: ({ navigation, route }) => ({
133
- title: 'New conversation',
134
- headerRight: (props: NativeStackHeaderRightProps) => (
135
- <HeaderTextButton
136
- {...props}
137
- onPress={() => navigation.popTo('Conversations', route.params)}
138
- title="Cancel"
139
- />
133
+ options: ({ navigation, route }) =>
134
+ composeOptions(
135
+ {
136
+ title: 'New conversation',
137
+ },
138
+ withRightClose({
139
+ onPress: () => navigation.popTo('Conversations', route.params),
140
+ })
140
141
  ),
141
- }),
142
142
  },
143
143
  ConversationFilterRecipients: {
144
144
  screen: ConversationFilterRecipientsScreen,
@@ -146,21 +146,20 @@ export const NewConversationStack = createNativeStackNavigator({
146
146
  },
147
147
  ConversationNew: {
148
148
  screen: ConversationNewScreen,
149
- options: ({ navigation, route }) => ({
150
- title: 'New conversation',
151
- headerLeft: () => null,
152
- headerRight: (props: NativeStackHeaderRightProps) => (
153
- <HeaderTextButton
154
- {...props}
155
- onPress={() => {
149
+ options: ({ navigation, route }) =>
150
+ composeOptions(
151
+ {
152
+ title: 'New conversation',
153
+ headerLeft: () => null,
154
+ },
155
+ withRightClose({
156
+ onPress: () => {
156
157
  const { chat_group_graph_id, group_source_app_name } =
157
158
  route.params as ConversationSelectRecipientsParams
158
159
  navigation.popTo('Conversations', { chat_group_graph_id, group_source_app_name })
159
- }}
160
- title="Cancel"
161
- />
160
+ },
161
+ })
162
162
  ),
163
- }),
164
163
  },
165
164
  AvatarPicker: {
166
165
  screen: AvatarPickerScreen,
@@ -170,24 +169,19 @@ export const NewConversationStack = createNativeStackNavigator({
170
169
  })
171
170
 
172
171
  export const ChatStack = createNativeStackNavigator({
173
- screenOptions: {
174
- headerBackButtonDisplayMode: 'minimal',
175
- },
172
+ screenOptions: globalScreenOptions,
176
173
  screenLayout: ScreenLayoutWithChatAccessGate,
177
174
  screens: {
178
175
  Conversations: {
179
176
  screen: ConversationsScreen,
180
- options: ({ route, navigation }) => ({
181
- headerTitle: (route.params as { title?: string })?.title ?? 'Chat',
182
- headerBackVisible: false,
183
- headerRight: (props: NativeStackHeaderRightProps) => (
184
- <HeaderBackButton
185
- backImage={() => <Icon name="general.x" size={18} color={props.tintColor} />}
186
- onPress={navigation.goBack}
187
- {...props}
188
- />
177
+ options: ({ route, navigation }) =>
178
+ composeOptions(
179
+ {
180
+ headerTitle: (route.params as { title?: string })?.title ?? 'Chat',
181
+ headerBackVisible: false,
182
+ },
183
+ withRightClose({ onPress: navigation.goBack })
189
184
  ),
190
- }),
191
185
  },
192
186
  ConversationFilters: {
193
187
  screen: ConversationFiltersScreen,
@@ -195,50 +189,48 @@ export const ChatStack = createNativeStackNavigator({
195
189
  },
196
190
  Conversation: {
197
191
  screen: ConversationScreen,
198
- options: ({ route, navigation }) => ({
199
- headerTitle: (props: NativeStackHeaderRightProps) => {
200
- const { conversation_id, title, badge, deleted, muted } =
201
- route.params as ConversationRouteProps
192
+ options: ({ route, navigation }) =>
193
+ composeOptions(
194
+ {
195
+ headerTitle: (props: NativeStackHeaderRightProps) => {
196
+ const { conversation_id, title, badge, deleted, muted } =
197
+ route.params as ConversationRouteProps
202
198
 
203
- return (
204
- <ConversationScreenTitle
205
- conversation_id={conversation_id}
206
- badge={badge}
207
- deleted={deleted}
208
- muted={muted}
209
- {...props}
210
- >
211
- {title ?? 'Conversation'}
212
- </ConversationScreenTitle>
213
- )
214
- },
215
- headerRight: (props: NativeStackHeaderRightProps) => (
216
- <HeaderBackButton
217
- backImage={() => <Icon name="general.x" size={18} color={props.tintColor} />}
218
- onPress={() => navigation.getParent()?.goBack()}
219
- />
220
- ),
221
- headerLeft: createMinimalHeaderBackButton({
222
- onPress: () => {
223
- const params = route.params as ConversationRouteProps
224
- if ((params as ConversationFiltersParams)?.chat_group_graph_id) {
225
- // Ensure that conversations with a graph id pass them back to the conversation list
226
- const { chat_group_graph_id } = params
227
- navigation.popTo('Conversations', { chat_group_graph_id })
228
- } else {
229
- navigation.goBack()
230
- }
199
+ return (
200
+ <ConversationScreenTitle
201
+ conversation_id={conversation_id}
202
+ badge={badge}
203
+ deleted={deleted}
204
+ muted={muted}
205
+ {...props}
206
+ >
207
+ {title ?? 'Conversation'}
208
+ </ConversationScreenTitle>
209
+ )
210
+ },
231
211
  },
232
- }),
233
- }),
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
+ }),
224
+ withRightClose({ onPress: () => navigation.getParent()?.goBack() })
225
+ ),
234
226
  },
235
227
  ConversationReply: {
236
228
  screen: ConversationScreen,
237
229
  options: ({ route, navigation }) =>
238
- composeHeaderOptions({
239
- title: (route.params as ConversationRouteProps)?.title ?? 'Reply',
240
- onPress: navigation.goBack,
241
- }),
230
+ composeOptions(
231
+ { title: (route.params as ConversationRouteProps)?.title ?? 'Reply' },
232
+ withMinimalBack({ onPress: navigation.goBack })
233
+ ),
242
234
  },
243
235
  TeamConversation: {
244
236
  screen: TeamConversationScreen,
@@ -251,13 +243,14 @@ export const ChatStack = createNativeStackNavigator({
251
243
  },
252
244
  ConversationDetails: {
253
245
  screen: ConversationDetailsScreen,
254
- options: ({ navigation }) => ({
255
- presentation: 'modal',
256
- title: 'Conversation details',
257
- headerRight: (props: NativeStackHeaderRightProps) => (
258
- <HeaderTextButton {...props} onPress={navigation.goBack} title="Done" />
246
+ options: ({ navigation }) =>
247
+ composeOptions(
248
+ {
249
+ presentation: 'modal',
250
+ title: 'Conversation details',
251
+ },
252
+ withRightDone({ onPress: navigation.goBack })
259
253
  ),
260
- }),
261
254
  },
262
255
  AvatarPicker: {
263
256
  screen: AvatarPickerScreen,
@@ -265,38 +258,43 @@ export const ChatStack = createNativeStackNavigator({
265
258
  },
266
259
  ChatSettings: {
267
260
  screen: SettingsScreen,
268
- options: ({ navigation }) => ({
269
- title: 'Chat settings',
270
- headerBackVisible: false,
271
- headerRight: (props: NativeStackHeaderRightProps) => (
272
- <HeaderTextButton {...props} onPress={navigation.goBack} title="Done" />
273
- ),
274
- }),
261
+ options: ({ navigation }) =>
262
+ composeOptions({ title: 'Chat settings' }, withMinimalBack({ onPress: navigation.goBack })),
275
263
  },
276
264
  NotificationSettings: {
277
265
  screen: NotificationSettingsScreen,
278
- options: ({ navigation }) => ({
279
- title: 'Chat',
280
- headerBackVisible: false,
281
- headerRight: (props: NativeStackHeaderRightProps) => (
282
- <HeaderTextButton {...props} onPress={navigation.goBack} title="Done" />
283
- ),
284
- }),
266
+ options: ({ navigation }) =>
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
+ ),
285
276
  },
286
277
  PreferredAppSelection: {
287
278
  screen: PreferredAppSelectionScreen,
288
- options: {
289
- title: 'Preferred app',
290
- },
279
+ options: ({ navigation }) =>
280
+ composeOptions({ title: 'Preferred app' }, withMinimalBack({ onPress: navigation.goBack })),
291
281
  },
292
282
  GroupNotificationSettings: {
293
283
  screen: GroupNotificationSettingsScreen,
294
- options: ({ route, navigation }) => ({
295
- title: (route.params as { title?: string })?.title || 'Group Settings',
296
- headerRight: (props: NativeStackHeaderRightProps) => (
297
- <HeaderDoneButton {...props} navigation={navigation} />
298
- ),
299
- }),
284
+ options: ({ route, navigation }) => {
285
+ const title = (route.params as { title?: string })?.title || 'Group Settings'
286
+
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.
289
+ return navigation.getState().index === 0
290
+ ? composeOptions(
291
+ {
292
+ title,
293
+ },
294
+ withRightClose({ onPress: navigation.goBack })
295
+ )
296
+ : composeOptions({ title }, withMinimalBack({ onPress: navigation.goBack }))
297
+ },
300
298
  },
301
299
  GroupNotificationLevelSelect: {
302
300
  screen: GroupNotificationLevelSelectScreen,
@@ -317,17 +315,14 @@ export const ChatStack = createNativeStackNavigator({
317
315
  GroupConversationNew: {
318
316
  screen: ConversationNewScreen,
319
317
  if: useQualifiedByAge,
320
- options: ({ navigation }) => ({
321
- title: 'New conversation',
322
- headerLeft: () => null,
323
- headerRight: props => (
324
- <HeaderTextButton
325
- {...props}
326
- onPress={() => navigation.getParent()?.goBack()}
327
- title="Cancel"
328
- />
318
+ options: ({ navigation }) =>
319
+ composeOptions(
320
+ {
321
+ title: 'New conversation',
322
+ headerLeft: () => null,
323
+ },
324
+ withRightClose({ onPress: () => navigation.getParent()?.goBack() })
329
325
  ),
330
- }),
331
326
  },
332
327
  SendGiphy: {
333
328
  screen: SendGiphyScreen,
@@ -368,14 +363,15 @@ export const ChatStack = createNativeStackNavigator({
368
363
  },
369
364
  GetHelp: {
370
365
  screen: GetHelpScreen,
371
- options: ({ navigation }) => ({
372
- headerTitle: 'Get help',
373
- headerBackVisible: false,
374
- presentation: 'modal',
375
- headerRight: props => (
376
- <HeaderTextButton {...props} onPress={navigation.goBack} title="Close" />
366
+ options: ({ navigation }) =>
367
+ composeOptions(
368
+ {
369
+ headerTitle: 'Get help',
370
+ headerBackVisible: false,
371
+ presentation: 'modal',
372
+ },
373
+ withRightClose({ onPress: navigation.goBack })
377
374
  ),
378
- }),
379
375
  },
380
376
  NotFound: {
381
377
  screen: NotFound,
@@ -4,7 +4,7 @@ import type {
4
4
  NativeStackScreenProps,
5
5
  } from '@react-navigation/native-stack'
6
6
  import { startsWith } from 'lodash'
7
- import React, { useCallback, useLayoutEffect, useState } from 'react'
7
+ import React, { useCallback, useState } from 'react'
8
8
  import {
9
9
  View,
10
10
  StyleSheet,
@@ -25,8 +25,10 @@ import {
25
25
  KeyboardView,
26
26
  } from '../components'
27
27
  import {
28
+ composeOptions,
28
29
  HeaderDismissButton,
29
- HeaderTextButton,
30
+ useHeaderRight,
31
+ withLeftClose,
30
32
  } from '../components/display/platform_modal_header_buttons'
31
33
  import { VideoAttachmentPreview } from '../components/display/video_attachment_preview'
32
34
  import { DefaultLoading } from '../components/page/loading'
@@ -55,11 +57,11 @@ enum BUG_TYPE_OPTIONS {
55
57
 
56
58
  export const BugReportScreenOptions = ({
57
59
  navigation,
58
- }: NativeStackScreenProps<any>): NativeStackNavigationOptions => ({
59
- presentation: 'modal',
60
- title: 'Report a bug',
61
- headerLeft: () => <HeaderDismissButton title="Cancel" onPress={() => navigation.goBack()} />,
62
- })
60
+ }: NativeStackScreenProps<any>): NativeStackNavigationOptions =>
61
+ composeOptions(
62
+ { presentation: 'modal', title: 'Report a bug' },
63
+ withLeftClose({ onPress: () => navigation.goBack() })
64
+ )
63
65
 
64
66
  interface Attachment {
65
67
  id: string
@@ -156,14 +158,13 @@ export function BugReportScreen() {
156
158
  return ImagePicker.openImageLibraryAsync().then(result => uploadImagePickerResult(result))
157
159
  }
158
160
 
159
- useLayoutEffect(() => {
160
- navigation.setOptions({
161
- // eslint-disable-next-line react/no-unstable-nested-components
162
- headerRight: () => (
163
- <HeaderTextButton title="Submit" onPress={handleSubmit} disabled={!formValid} />
164
- ),
165
- })
166
- }, [formValid, handleSubmit, navigation])
161
+ useHeaderRight({
162
+ icon: 'general.upArrow',
163
+ accessibilityLabel: 'Submit',
164
+ prominent: true,
165
+ onPress: handleSubmit,
166
+ disabled: !formValid,
167
+ })
167
168
 
168
169
  if (status === 'pending') {
169
170
  return (
@@ -38,7 +38,7 @@ import {
38
38
  type TextStyle,
39
39
  } from '../components'
40
40
  import { ConversationAvatar } from '../components/display/conversation_avatar'
41
- import { HeaderTextButton } from '../components/display/platform_modal_header_buttons'
41
+ import { useHeaderRight } from '../components/display/platform_modal_header_buttons'
42
42
  import { ButtonAppearanceUnion } from '../components/display/utils/button_colors'
43
43
  import { useSuspensePaginator, useTheme } from '../hooks'
44
44
  import {
@@ -222,24 +222,18 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
222
222
  [name, productName, resourceType, styles.headerTitleBadge, styles.headerTitleContainer]
223
223
  )
224
224
 
225
- const HeaderRight = useCallback(() => {
226
- return (
227
- <HeaderTextButton
228
- onPress={() => {
229
- saveTitle({ title: trimmedTitle || conversation.title })
230
- navigation.goBack()
231
- }}
232
- title="Done"
233
- />
234
- )
235
- }, [conversation.title, navigation, saveTitle, trimmedTitle])
225
+ useHeaderRight({
226
+ icon: 'general.check',
227
+ accessibilityLabel: 'Done',
228
+ onPress: () => {
229
+ saveTitle({ title: trimmedTitle || conversation.title })
230
+ navigation.goBack()
231
+ },
232
+ })
236
233
 
237
234
  useEffect(() => {
238
- navigation.setOptions({
239
- headerRight: HeaderRight,
240
- headerTitle: HeaderTitle,
241
- })
242
- }, [HeaderRight, HeaderTitle, navigation])
235
+ navigation.setOptions({ headerTitle: HeaderTitle })
236
+ }, [HeaderTitle, navigation])
243
237
 
244
238
  const listData = [
245
239
  {
@@ -1,5 +1,6 @@
1
1
  import { useHeaderHeight } from '@react-navigation/elements'
2
- import { createComponentForStaticNavigation, RouteProp, useRoute } from '@react-navigation/native'
2
+ import * as ReactNavigationNative from '@react-navigation/native'
3
+ import { RouteProp, useRoute } from '@react-navigation/native'
3
4
  import { createNativeStackNavigator } from '@react-navigation/native-stack'
4
5
  import React, { useCallback } from 'react'
5
6
  import { LayoutChangeEvent, Platform, StyleSheet, useWindowDimensions, View } from 'react-native'
@@ -58,7 +59,26 @@ const FilterNavigator = createNativeStackNavigator<ConversationFilterStackParamL
58
59
  },
59
60
  })
60
61
 
61
- const Filters = createComponentForStaticNavigation(FilterNavigator, 'Filters')
62
+ // tree.getComponent() doesn't exist on every host's installed
63
+ // react-navigation/core — hosts on an older version would throw here.
64
+ // Where it does exist, it's the real implementation (the "deprecated"
65
+ // createComponentForStaticNavigation is just a console.warn wrapper around
66
+ // the same call on current versions), so prefer it and only reach for the
67
+ // old function on cores where getComponent() genuinely isn't there yet.
68
+ function createStaticNavigationComponent<P extends object>(
69
+ tree: { getComponent?: () => React.ComponentType<P> },
70
+ name: string
71
+ ): React.ComponentType<P> {
72
+ if (typeof tree.getComponent === 'function') return tree.getComponent()
73
+
74
+ const { createComponentForStaticNavigation } = ReactNavigationNative as {
75
+ createComponentForStaticNavigation: (tree: unknown, name: string) => React.ComponentType<P>
76
+ }
77
+
78
+ return createComponentForStaticNavigation(tree, name)
79
+ }
80
+
81
+ const Filters = createStaticNavigationComponent(FilterNavigator, 'Filters')
62
82
 
63
83
  export const ConversationFiltersScreen = (_props: ConversationFiltersScreenProps) => {
64
84
  const styles = useStyles()
@@ -117,6 +137,9 @@ const useStyles = () => {
117
137
  ios: 20,
118
138
  android: 20 + top,
119
139
  }),
140
+ // The filter list scrolls under it, so the sheet header doesn't need a
141
+ // separator the way the non-scrolling reaction sheet does.
142
+ borderBottomWidth: 0,
120
143
  },
121
144
  })
122
145
  }
@@ -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',