@planningcenter/chat-react-native 3.43.1-qa-staging.1 → 3.44.0-rc.1

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 +18 -20
  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 +4 -3
  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 +24 -23
  63. package/src/screens/notification_settings_screen.tsx +1 -21
@@ -1,12 +1,15 @@
1
- import { HeaderBackButton, HeaderButton } from '@react-navigation/elements'
1
+ import { HeaderBackButton, HeaderButton, PlatformPressable } from '@react-navigation/elements'
2
+ import { useNavigation } from '@react-navigation/native'
2
3
  import type {
3
- NativeStackHeaderLeftProps,
4
- NativeStackHeaderRightProps,
4
+ NativeStackHeaderItemProps,
5
+ NativeStackNavigationOptions,
5
6
  } from '@react-navigation/native-stack'
7
+ import { useLayoutEffect } from 'react'
6
8
  import { ColorValue, Platform, StyleSheet } from 'react-native'
7
- import { useTheme } from '../../hooks'
8
- import { isLiquidGlassEnabled } from '../../utils/liquid_glass'
9
- 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'
10
13
  import { TextButton } from './text_button'
11
14
 
12
15
  interface HeaderTextButtonProps {
@@ -67,61 +70,209 @@ export const HeaderDismissButton = ({
67
70
  })
68
71
  }
69
72
 
70
- interface HeaderDoneButtonProps extends NativeStackHeaderRightProps {
71
- navigation: { goBack: () => void; getState: () => any }
73
+ const createTextHeaderRight =
74
+ ({ onPress, title, disabled }: HeaderTextButtonProps) =>
75
+ (props: NativeStackHeaderItemProps) => (
76
+ <HeaderTextButton {...props} onPress={onPress} title={title} disabled={disabled} />
77
+ )
78
+
79
+ export type OptionsDecorator = (
80
+ options: NativeStackNavigationOptions
81
+ ) => NativeStackNavigationOptions
82
+
83
+ export const composeOptions = (
84
+ base: NativeStackNavigationOptions,
85
+ ...decorators: OptionsDecorator[]
86
+ ): NativeStackNavigationOptions => decorators.reduce((options, decorate) => decorate(options), base)
87
+
88
+ interface TextButtonDecoratorProps {
89
+ text: string
90
+ onPress: () => void
91
+ disabled?: boolean
72
92
  }
73
93
 
74
- export const HeaderDoneButton = ({ navigation, ...props }: HeaderDoneButtonProps) => {
75
- const state = navigation.getState()
76
- const isFirstScreen = state.index === 0
94
+ export const withRightText =
95
+ ({ text, onPress, disabled }: TextButtonDecoratorProps): OptionsDecorator =>
96
+ options => ({
97
+ ...options,
98
+ headerRight: createTextHeaderRight({ onPress, title: text, disabled }),
99
+ unstable_headerRightItems: () => [{ type: 'button' as const, label: text, onPress, disabled }],
100
+ })
77
101
 
78
- if (!isFirstScreen) {
79
- return null
80
- }
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
81
106
 
82
- return <HeaderTextButton {...props} onPress={navigation.goBack} title="Done" />
107
+ export const useHeaderSlotReserve = (): number => {
108
+ const fontScale = useFontScale({ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER_LANDMARK })
109
+
110
+ return HEADER_BAR_INSET + HEADER_GLASS_BUTTON_SIZE * fontScale + HEADER_SLOT_GAP
83
111
  }
84
112
 
85
- // The default back icon reserves trailing space for a label that
86
- // displayMode="minimal" never shows; a custom image skips it so the glass
87
- // capsule hugs the glyph.
88
- const renderGlassBackImage = ({ tintColor }: { tintColor: string }) => (
89
- <Icon name="general.leftChevron" size={18} color={tintColor} />
90
- )
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>
188
+ )
189
+
190
+ export const withRightIcon =
191
+ (props: HeaderIconProps): OptionsDecorator =>
192
+ options => ({
193
+ ...options,
194
+ headerRight: createHeaderIconElement('right')(props),
195
+ unstable_headerRightItems: () => [createHeaderIconItem(props)],
196
+ })
197
+
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 })
211
+
212
+ export const withRightDone = ({ onPress }: { onPress: () => void }) =>
213
+ withRightIcon({ icon: 'general.check', accessibilityLabel: 'Done', onPress })
91
214
 
92
- // The iOS 26 glass capsule manages its own insets — negative margins clip
93
- // the button against the capsule edge.
94
- export const createMinimalHeaderBackButton =
215
+ const createMinimalHeaderBackElement =
95
216
  ({ onPress }: { onPress: () => void }) =>
96
- (props: NativeStackHeaderLeftProps) => (
217
+ (props: NativeStackHeaderItemProps) => (
97
218
  <HeaderBackButton
98
- style={
99
- isLiquidGlassEnabled()
100
- ? undefined
101
- : {
102
- marginLeft: Platform.select({ ios: -8, default: -3 }),
103
- marginRight: Platform.select({ ios: 0, default: 30 }),
104
- }
105
- }
106
- backImage={isLiquidGlassEnabled() ? renderGlassBackImage : undefined}
219
+ style={Platform.select({
220
+ ios: { marginLeft: -8 },
221
+ default: { marginLeft: -3, marginRight: 30 },
222
+ })}
107
223
  displayMode="minimal"
108
224
  onPress={onPress}
109
225
  {...props}
110
226
  />
111
227
  )
112
228
 
113
- // Under glass the native minimal back button suffices; pre-26 needs the
114
- // custom button and its layout nudges.
115
- export const composeHeaderOptions = ({
116
- title,
117
- onPress,
118
- }: {
119
- title: string
229
+ export const withMinimalBack =
230
+ ({ onPress }: { onPress: () => void }): OptionsDecorator =>
231
+ options => ({
232
+ ...options,
233
+ headerLeft: createMinimalHeaderBackElement({ onPress }),
234
+ unstable_headerLeftItems: () => [
235
+ createHeaderIconItem({ icon: 'general.leftChevron', accessibilityLabel: 'Back', onPress }),
236
+ ],
237
+ })
238
+
239
+ type UseHeaderRightOptions = {
120
240
  onPress: () => void
121
- }) => ({
122
- title,
123
- ...(isLiquidGlassEnabled() ? {} : { headerLeft: createMinimalHeaderBackButton({ onPress }) }),
124
- })
241
+ disabled?: boolean
242
+ enabled?: boolean
243
+ } & (
244
+ | (Omit<HeaderIconProps, 'tintColor'> & { title?: never })
245
+ | { title: string; icon?: never; accessibilityLabel?: never; prominent?: never }
246
+ )
247
+
248
+ export const useHeaderRight = (options: UseHeaderRightOptions) => {
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
255
+
256
+ useLayoutEffect(() => {
257
+ if (!enabled) {
258
+ navigation.setOptions({ headerRight: () => null, unstable_headerRightItems: undefined })
259
+ return
260
+ }
261
+
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])
275
+ }
125
276
 
126
277
  const useStyles = () => {
127
278
  const { colors } = useTheme()
@@ -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,