@planningcenter/chat-react-native 3.42.3-qa-staging.6 → 3.42.3-qa-856.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 (118) hide show
  1. package/build/components/conversation/messages_disabled_banners.d.ts +3 -2
  2. package/build/components/conversation/messages_disabled_banners.d.ts.map +1 -1
  3. package/build/components/conversation/messages_disabled_banners.js +7 -2
  4. package/build/components/conversation/messages_disabled_banners.js.map +1 -1
  5. package/build/components/display/adult_requirement_notice.js +1 -1
  6. package/build/components/display/adult_requirement_notice.js.map +1 -1
  7. package/build/components/display/platform_modal_header_buttons.d.ts +7 -29
  8. package/build/components/display/platform_modal_header_buttons.d.ts.map +1 -1
  9. package/build/components/display/platform_modal_header_buttons.js +24 -76
  10. package/build/components/display/platform_modal_header_buttons.js.map +1 -1
  11. package/build/hooks/index.d.ts +1 -1
  12. package/build/hooks/index.d.ts.map +1 -1
  13. package/build/hooks/index.js +1 -1
  14. package/build/hooks/index.js.map +1 -1
  15. package/build/hooks/use_conversation.d.ts.map +1 -1
  16. package/build/hooks/use_conversation.js +0 -1
  17. package/build/hooks/use_conversation.js.map +1 -1
  18. package/build/hooks/use_conversation_jolt_events.d.ts.map +1 -1
  19. package/build/hooks/use_conversation_jolt_events.js +2 -10
  20. package/build/hooks/use_conversation_jolt_events.js.map +1 -1
  21. package/build/hooks/use_current_person.d.ts.map +1 -1
  22. package/build/hooks/use_current_person.js +0 -1
  23. package/build/hooks/use_current_person.js.map +1 -1
  24. package/build/hooks/use_direct_messages_enabled.d.ts +2 -0
  25. package/build/hooks/use_direct_messages_enabled.d.ts.map +1 -0
  26. package/build/hooks/use_direct_messages_enabled.js +16 -0
  27. package/build/hooks/use_direct_messages_enabled.js.map +1 -0
  28. package/build/hooks/use_features.d.ts +1 -0
  29. package/build/hooks/use_features.d.ts.map +1 -1
  30. package/build/hooks/use_features.js +1 -0
  31. package/build/hooks/use_features.js.map +1 -1
  32. package/build/navigation/index.d.ts +84 -50
  33. package/build/navigation/index.d.ts.map +1 -1
  34. package/build/navigation/index.js +36 -27
  35. package/build/navigation/index.js.map +1 -1
  36. package/build/screens/bug_report_screen.d.ts.map +1 -1
  37. package/build/screens/bug_report_screen.js +3 -9
  38. package/build/screens/bug_report_screen.js.map +1 -1
  39. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  40. package/build/screens/conversation_details_screen.js +1 -2
  41. package/build/screens/conversation_details_screen.js.map +1 -1
  42. package/build/screens/conversation_filters/components/conversation_filters.js +3 -3
  43. package/build/screens/conversation_filters/components/conversation_filters.js.map +1 -1
  44. package/build/screens/conversation_filters/components/rows.js +1 -1
  45. package/build/screens/conversation_filters/components/rows.js.map +1 -1
  46. package/build/screens/conversation_filters_screen.d.ts.map +1 -1
  47. package/build/screens/conversation_filters_screen.js +14 -1
  48. package/build/screens/conversation_filters_screen.js.map +1 -1
  49. package/build/screens/conversation_screen.d.ts.map +1 -1
  50. package/build/screens/conversation_screen.js +3 -10
  51. package/build/screens/conversation_screen.js.map +1 -1
  52. package/build/screens/conversation_select_recipients/components/groups_recipient_row.js +1 -1
  53. package/build/screens/conversation_select_recipients/components/groups_recipient_row.js.map +1 -1
  54. package/build/screens/conversation_select_recipients/components/restricted_group_row.js +1 -1
  55. package/build/screens/conversation_select_recipients/components/restricted_group_row.js.map +1 -1
  56. package/build/screens/conversation_select_type_screen.d.ts.map +1 -1
  57. package/build/screens/conversation_select_type_screen.js +7 -4
  58. package/build/screens/conversation_select_type_screen.js.map +1 -1
  59. package/build/screens/conversations/components/list_header_component.js +3 -3
  60. package/build/screens/conversations/components/list_header_component.js.map +1 -1
  61. package/build/screens/message_report_screen.js +3 -5
  62. package/build/screens/message_report_screen.js.map +1 -1
  63. package/build/screens/notification_settings_screen.d.ts.map +1 -1
  64. package/build/screens/notification_settings_screen.js +2 -0
  65. package/build/screens/notification_settings_screen.js.map +1 -1
  66. package/build/screens/settings_screen.js +3 -3
  67. package/build/screens/settings_screen.js.map +1 -1
  68. package/build/types/jolt_events/conversation_events.d.ts +2 -2
  69. package/build/types/jolt_events/conversation_events.d.ts.map +1 -1
  70. package/build/types/jolt_events/conversation_events.js.map +1 -1
  71. package/build/types/resources/conversation.d.ts +2 -2
  72. package/build/types/resources/conversation.d.ts.map +1 -1
  73. package/build/types/resources/conversation.js.map +1 -1
  74. package/build/types/resources/person.d.ts +0 -1
  75. package/build/types/resources/person.d.ts.map +1 -1
  76. package/build/types/resources/person.js.map +1 -1
  77. package/build/utils/conversation_safety_constants.d.ts +1 -1
  78. package/build/utils/conversation_safety_constants.d.ts.map +1 -1
  79. package/build/utils/conversation_safety_constants.js +1 -1
  80. package/build/utils/conversation_safety_constants.js.map +1 -1
  81. package/build/utils/request/conversation.d.ts.map +1 -1
  82. package/build/utils/request/conversation.js +0 -1
  83. package/build/utils/request/conversation.js.map +1 -1
  84. package/package.json +4 -4
  85. package/src/components/conversation/messages_disabled_banners.tsx +11 -3
  86. package/src/components/display/adult_requirement_notice.tsx +1 -1
  87. package/src/components/display/platform_modal_header_buttons.tsx +50 -163
  88. package/src/hooks/index.ts +1 -1
  89. package/src/hooks/use_conversation.ts +0 -1
  90. package/src/hooks/use_conversation_jolt_events.ts +3 -16
  91. package/src/hooks/use_current_person.ts +0 -1
  92. package/src/hooks/use_direct_messages_enabled.ts +23 -0
  93. package/src/hooks/use_features.ts +1 -0
  94. package/src/navigation/index.tsx +42 -36
  95. package/src/screens/bug_report_screen.tsx +3 -9
  96. package/src/screens/conversation_details_screen.tsx +1 -2
  97. package/src/screens/conversation_filters/components/conversation_filters.tsx +3 -3
  98. package/src/screens/conversation_filters/components/rows.tsx +2 -2
  99. package/src/screens/conversation_filters_screen.tsx +21 -1
  100. package/src/screens/conversation_screen.tsx +3 -11
  101. package/src/screens/conversation_select_recipients/components/groups_recipient_row.tsx +1 -1
  102. package/src/screens/conversation_select_recipients/components/restricted_group_row.tsx +2 -2
  103. package/src/screens/conversation_select_type_screen.tsx +7 -4
  104. package/src/screens/conversations/components/list_header_component.tsx +3 -3
  105. package/src/screens/message_report_screen.tsx +3 -5
  106. package/src/screens/notification_settings_screen.tsx +4 -1
  107. package/src/screens/settings_screen.tsx +3 -3
  108. package/src/types/jolt_events/conversation_events.ts +2 -2
  109. package/src/types/resources/conversation.ts +3 -2
  110. package/src/types/resources/person.ts +0 -1
  111. package/src/utils/conversation_safety_constants.ts +1 -1
  112. package/src/utils/request/conversation.ts +0 -1
  113. package/build/hooks/use_direct_messages_eligible.d.ts +0 -2
  114. package/build/hooks/use_direct_messages_eligible.d.ts.map +0 -1
  115. package/build/hooks/use_direct_messages_eligible.js +0 -9
  116. package/build/hooks/use_direct_messages_eligible.js.map +0 -1
  117. package/src/__tests__/hooks/use_direct_messages_eligible.test.tsx +0 -77
  118. package/src/hooks/use_direct_messages_eligible.ts +0 -11
@@ -1,6 +1,8 @@
1
1
  import { StyleSheet, View, type ViewStyle } from 'react-native'
2
2
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
3
3
  import { useTheme } from '../../hooks'
4
+ import type { ConversationDisabledReason } from '../../types'
5
+ import { ADULT_REQUIREMENT_BODY } from '../../utils'
4
6
  import { Button, Text } from '../display'
5
7
 
6
8
  export const LeaderMessagesDisabledBanner = () => {
@@ -20,19 +22,25 @@ export const MemberMessagesDisabledBanner = () => {
20
22
  )
21
23
  }
22
24
 
25
+ const DISABLED_REASON_TEXT: Record<ConversationDisabledReason, string> = {
26
+ safety_check: ADULT_REQUIREMENT_BODY,
27
+ direct_messages_disabled: 'Direct messages have been turned off by someone in this conversation.',
28
+ }
29
+
23
30
  const DEFAULT_DISABLED_TEXT = 'This conversation has been disabled.'
24
31
 
25
32
  interface ConversationDisabledBannerProps {
26
- message: string | null
33
+ disabledReason?: ConversationDisabledReason
27
34
  messageLink?: { label: string; onPress: () => void }
28
35
  }
29
36
 
30
37
  export const ConversationDisabledBanner = ({
31
- message,
38
+ disabledReason,
32
39
  messageLink,
33
40
  }: ConversationDisabledBannerProps) => {
34
41
  const styles = useStyles()
35
- const description = message ?? DEFAULT_DISABLED_TEXT
42
+ const description =
43
+ (disabledReason && DISABLED_REASON_TEXT[disabledReason]) ?? DEFAULT_DISABLED_TEXT
36
44
 
37
45
  return (
38
46
  <MessagesDisabledBanner
@@ -13,7 +13,7 @@ export function AdultRequirementNotice({ style }: AdultRequirementNoticeProps) {
13
13
  <BannerCollapsible
14
14
  appearance="warning"
15
15
  iconName="general.shieldExclamation"
16
- heading="Two adults required to chat"
16
+ heading="2 adults required to chat"
17
17
  style={style}
18
18
  >
19
19
  <BannerPrimitive.Text>{ADULT_REQUIREMENT_BODY}</BannerPrimitive.Text>
@@ -1,19 +1,16 @@
1
- import { HeaderBackButton, HeaderButton, PlatformPressable } from '@react-navigation/elements'
1
+ import { HeaderBackButton, HeaderButton } from '@react-navigation/elements'
2
2
  import { useNavigation } from '@react-navigation/native'
3
3
  import type {
4
- NativeStackHeaderItemProps,
4
+ NativeStackHeaderLeftProps,
5
+ NativeStackHeaderRightProps,
5
6
  NativeStackNavigationOptions,
6
7
  } from '@react-navigation/native-stack'
7
8
  import { useLayoutEffect } from 'react'
8
9
  import { ColorValue, Platform, StyleSheet } from 'react-native'
9
- import LinearGradient from 'react-native-linear-gradient'
10
- import { useFontScale, useTheme } from '../../hooks'
11
- import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../../utils'
10
+ import { useTheme } from '../../hooks'
12
11
  import { isLiquidGlassEnabled } from '../../utils/liquid_glass'
13
- import { tokens } from '../../vendor/tapestry/tokens'
14
- import { Icon, type IconString } from './icon'
12
+ import { Icon } from './icon'
15
13
  import { TextButton } from './text_button'
16
- import { getColorKey, useGradientColorMap } from './utils/button_colors'
17
14
 
18
15
  interface HeaderTextButtonProps {
19
16
  onPress: () => void
@@ -75,10 +72,14 @@ export const HeaderDismissButton = ({
75
72
 
76
73
  const createTextHeaderRight =
77
74
  ({ onPress, title, disabled }: HeaderTextButtonProps) =>
78
- (props: NativeStackHeaderItemProps) => (
75
+ (props: NativeStackHeaderRightProps) => (
79
76
  <HeaderTextButton {...props} onPress={onPress} title={title} disabled={disabled} />
80
77
  )
81
78
 
79
+ const createDismissHeaderLeft =
80
+ ({ onPress, title }: { onPress: () => void; title: string }) =>
81
+ () => <HeaderDismissButton title={title} onPress={onPress} />
82
+
82
83
  export type OptionsDecorator = (
83
84
  options: NativeStackNavigationOptions
84
85
  ) => NativeStackNavigationOptions
@@ -105,149 +106,42 @@ export const withRightText =
105
106
  unstable_headerRightItems: () => [{ type: 'button' as const, label: text, onPress, disabled }],
106
107
  })
107
108
 
108
- const HEADER_ICON_SIZE = 17
109
- const HEADER_FILL_ICON_SIZE = 16
110
- const HEADER_FILL_PADDING = 14
111
- const HEADER_BAR_INSET = 16
112
- const HEADER_SLOT_GAP = 12
113
-
114
- const headerFillSize = (fontScale: number) => {
115
- const icon = HEADER_FILL_ICON_SIZE * fontScale
116
-
117
- return { icon, circle: icon + HEADER_FILL_PADDING * 2 }
118
- }
119
-
120
- // Liquid glass icons don't communicate
121
- export const useHeaderSlotReserve = (): number => {
122
- const fontScale = useFontScale({ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER_LANDMARK })
123
-
124
- return HEADER_BAR_INSET + headerFillSize(fontScale).circle + HEADER_SLOT_GAP
125
- }
126
-
127
- const HEADER_FALLBACK_HIT_SLOP = 14
128
-
129
- const iconStyles = StyleSheet.create({
130
- disabled: {
131
- opacity: 0.4,
132
- },
133
- fallbackButton: {
134
- alignItems: 'center',
135
- justifyContent: 'center',
136
- paddingHorizontal: 8,
137
- },
138
- fillCircle: {
139
- alignItems: 'center',
140
- justifyContent: 'center',
141
- },
142
- })
143
-
144
- export interface HeaderIconProps {
145
- icon: IconString
146
- accessibilityLabel: string
147
- onPress: () => void
148
- disabled?: boolean
149
- variant?: 'plain' | 'fill'
150
- }
151
-
152
- const HeaderIcon = ({
153
- icon,
154
- disabled,
155
- variant,
156
- }: Pick<HeaderIconProps, 'icon' | 'disabled' | 'variant'>) => {
157
- const { colors } = useTheme()
158
- const gradients = useGradientColorMap()
159
- const fontScale = useFontScale({ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER_LANDMARK })
160
-
161
- if (variant !== 'fill') return <Icon name={icon} size={HEADER_ICON_SIZE} />
162
-
163
- const { icon: iconSize, circle: circleSize } = headerFillSize(fontScale)
164
-
165
- return (
166
- <LinearGradient
167
- start={{ x: 0.1, y: 0.1 }}
168
- end={{ x: 0.9, y: 0.9 }}
169
- colors={gradients[getColorKey({ disabled: Boolean(disabled), appearance: 'interaction' })]}
170
- style={[
171
- iconStyles.fillCircle,
172
- { borderRadius: circleSize, height: circleSize, width: circleSize },
173
- ]}
174
- >
175
- <Icon
176
- name={icon}
177
- size={iconSize}
178
- color={disabled ? colors.iconColorDefaultDisabled : tokens.colorNeutral100White}
179
- />
180
- </LinearGradient>
181
- )
182
- }
183
-
184
- // Native bar items take SF Symbols, images, or any element, so both paths
185
- // render the same icon: iOS 26 puts this one in the glass circle, and the
186
- // element below covers Android and older iOS.
187
- const createHeaderIconItem = ({
188
- icon,
189
- accessibilityLabel,
190
- onPress,
191
- disabled,
192
- variant,
193
- }: HeaderIconProps) => ({
194
- type: 'custom' as const,
195
- // The gradient circle replaces the glass one rather than sitting on it.
196
- hidesSharedBackground: variant === 'fill',
197
- element: (
198
- <PlatformPressable
199
- onPress={onPress}
200
- disabled={disabled}
201
- hitSlop={12}
202
- accessibilityRole="button"
203
- accessibilityLabel={accessibilityLabel}
204
- style={disabled && variant !== 'fill' && iconStyles.disabled}
205
- >
206
- <HeaderIcon icon={icon} disabled={disabled} variant={variant} />
207
- </PlatformPressable>
208
- ),
209
- })
210
-
211
- const createHeaderIconElement =
212
- ({ icon, accessibilityLabel, onPress, disabled, variant }: HeaderIconProps) =>
213
- () => (
214
- <PlatformPressable
215
- onPress={onPress}
216
- disabled={disabled}
217
- hitSlop={HEADER_FALLBACK_HIT_SLOP}
218
- accessibilityRole="button"
219
- accessibilityLabel={accessibilityLabel}
220
- style={[iconStyles.fallbackButton, disabled && variant !== 'fill' && iconStyles.disabled]}
221
- >
222
- <HeaderIcon icon={icon} disabled={disabled} variant={variant} />
223
- </PlatformPressable>
224
- )
225
-
226
- export const withRightIcon =
227
- (props: HeaderIconProps): OptionsDecorator =>
109
+ export const withLeftText =
110
+ ({ text, onPress }: TextButtonDecoratorProps): OptionsDecorator =>
228
111
  options => ({
229
112
  ...options,
230
- headerRight: createHeaderIconElement(props),
231
- unstable_headerRightItems: () => [createHeaderIconItem(props)],
113
+ headerLeft: createDismissHeaderLeft({ title: text, onPress }),
114
+ unstable_headerLeftItems: () => [{ type: 'button' as const, label: text, onPress }],
232
115
  })
233
116
 
234
- export const withLeftIcon =
235
- (props: HeaderIconProps): OptionsDecorator =>
117
+ const renderCloseImage = ({ tintColor }: { tintColor: string }) => (
118
+ <Icon name="general.x" size={18} color={tintColor} />
119
+ )
120
+
121
+ const createCloseHeaderRight =
122
+ ({ onPress }: { onPress: () => void }) =>
123
+ (props: NativeStackHeaderRightProps) => (
124
+ <HeaderBackButton backImage={renderCloseImage} onPress={onPress} {...props} />
125
+ )
126
+
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 =>
236
132
  options => ({
237
133
  ...options,
238
- headerLeft: createHeaderIconElement(props),
239
- unstable_headerLeftItems: () => [createHeaderIconItem(props)],
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
+ ],
240
143
  })
241
144
 
242
- export const withRightClose = ({ onPress }: { onPress: () => void }) =>
243
- withRightIcon({ icon: 'general.x', accessibilityLabel: 'Close', onPress })
244
-
245
- export const withLeftClose = ({ onPress }: { onPress: () => void }) =>
246
- withLeftIcon({ icon: 'general.x', accessibilityLabel: 'Close', onPress })
247
-
248
- export const withRightDone = ({ onPress }: { onPress: () => void }) =>
249
- withRightIcon({ icon: 'general.check', accessibilityLabel: 'Done', onPress })
250
-
251
145
  // The default back icon reserves trailing space for a label that
252
146
  // displayMode="minimal" never shows; a custom image skips it so the glass
253
147
  // capsule hugs the glyph.
@@ -259,7 +153,7 @@ const renderGlassBackImage = ({ tintColor }: { tintColor: string }) => (
259
153
  // the button against the capsule edge.
260
154
  export const createMinimalHeaderBackButton =
261
155
  ({ onPress }: { onPress: () => void }) =>
262
- (props: NativeStackHeaderItemProps) => (
156
+ (props: NativeStackHeaderLeftProps) => (
263
157
  <HeaderBackButton
264
158
  style={
265
159
  isLiquidGlassEnabled()
@@ -285,25 +179,22 @@ export const withMinimalBack =
285
179
  ...(isLiquidGlassEnabled() ? {} : { headerLeft: createMinimalHeaderBackButton({ onPress }) }),
286
180
  })
287
181
 
288
- // An icon or a title, never both — passing both is a compile error rather
289
- // than a question of which one wins.
290
- type UseHeaderRightOptions = {
182
+ interface UseHeaderRightOptions {
183
+ title: string
291
184
  onPress: () => void
292
185
  disabled?: boolean
293
186
  enabled?: boolean
294
- } & (
295
- | (HeaderIconProps & { title?: never })
296
- | { title: string; icon?: never; accessibilityLabel?: never; variant?: never }
297
- )
187
+ }
298
188
 
299
189
  // For screens whose header button depends on live state (form validity,
300
190
  // mutation callbacks). Static buttons belong in the navigator options.
301
- export const useHeaderRight = (options: UseHeaderRightOptions) => {
191
+ export const useHeaderRight = ({
192
+ title,
193
+ onPress,
194
+ disabled,
195
+ enabled = true,
196
+ }: UseHeaderRightOptions) => {
302
197
  const navigation = useNavigation()
303
- const { onPress, disabled, enabled = true } = options
304
- const icon = options.icon
305
- const label = options.icon !== undefined ? options.accessibilityLabel : options.title
306
- const variant = options.variant
307
198
 
308
199
  useLayoutEffect(() => {
309
200
  if (!enabled) {
@@ -311,12 +202,8 @@ export const useHeaderRight = (options: UseHeaderRightOptions) => {
311
202
  return
312
203
  }
313
204
 
314
- const decorate = icon
315
- ? withRightIcon({ icon, accessibilityLabel: label, onPress, disabled, variant })
316
- : withRightText({ text: label, onPress, disabled })
317
-
318
- navigation.setOptions(decorate({}))
319
- }, [disabled, enabled, icon, label, navigation, onPress, variant])
205
+ navigation.setOptions(withRightText({ text: title, onPress, disabled })({}))
206
+ }, [disabled, enabled, navigation, onPress, title])
320
207
  }
321
208
 
322
209
  const useStyles = () => {
@@ -8,7 +8,7 @@ export * from './use_chat_permissions'
8
8
  export * from './use_create_android_ripple_color'
9
9
  export * from './use_current_person'
10
10
  export * from './use_deleting_ids'
11
- export * from './use_direct_messages_eligible'
11
+ export * from './use_direct_messages_enabled'
12
12
  export * from './use_font_scale'
13
13
  export * from './use_has_direct_messages'
14
14
  export * from './use_groups_groups'
@@ -32,7 +32,6 @@ export const getConversationRequestArgs = ({ conversation_id }: { conversation_i
32
32
  'direct_message',
33
33
  'disabled',
34
34
  'disabled_reason',
35
- 'disabled_message',
36
35
  'member_ability',
37
36
  'muted',
38
37
  'replies_disabled',
@@ -26,13 +26,8 @@ export function useConversationJoltEvents({ conversationId }: Props) {
26
26
 
27
27
  const handleUpdatedConversation = useCallback(
28
28
  (e: JoltConversationEvent) => {
29
- const {
30
- last_message_idempotent_key,
31
- last_message_author_id,
32
- disabled,
33
- disabled_reason,
34
- disabled_message,
35
- } = e.data.data
29
+ const { last_message_idempotent_key, last_message_author_id, disabled, disabled_reason } =
30
+ e.data.data
36
31
 
37
32
  if (last_message_idempotent_key && last_message_author_id === currentPersonId) {
38
33
  completeMessageCreationConversationTracking({
@@ -43,15 +38,7 @@ export function useConversationJoltEvents({ conversationId }: Props) {
43
38
 
44
39
  queryClient.setQueryData<ApiResource<ConversationResource>>(queryKey, prev => {
45
40
  if (!prev?.data) return prev
46
- return {
47
- ...prev,
48
- data: {
49
- ...prev.data,
50
- disabled,
51
- disabledReason: disabled_reason,
52
- disabledMessage: disabled_message,
53
- },
54
- }
41
+ return { ...prev, data: { ...prev.data, disabled, disabledReason: disabled_reason } }
55
42
  })
56
43
 
57
44
  queryClient.invalidateQueries({ queryKey })
@@ -17,7 +17,6 @@ export const currentPersonRequestArgs = {
17
17
  'pco_chat_enabled',
18
18
  'age_qualification_status',
19
19
  'direct_messages_disabled',
20
- 'direct_messages_eligible',
21
20
  'under_18',
22
21
  ],
23
22
  },
@@ -0,0 +1,23 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { ApiCollection } from '../types'
3
+ import type { FeatureResource } from '../types/resources/feature_resource'
4
+ import { getFeaturesRequestArgs, getFeaturesQueryKey } from '../utils/request/get_features'
5
+ import { useApiClient } from './use_api_client'
6
+ import { availableFeatures } from './use_features'
7
+
8
+ export function useDirectMessagesEnabled(): boolean {
9
+ const apiClient = useApiClient()
10
+ const { data, isFetched } = useQuery<ApiCollection<FeatureResource>>({
11
+ queryKey: getFeaturesQueryKey(),
12
+ queryFn: () => apiClient.chat.get<ApiCollection<FeatureResource>>(getFeaturesRequestArgs()),
13
+ staleTime: 1000 * 60 * 5,
14
+ })
15
+
16
+ if (!isFetched) return false
17
+
18
+ return (
19
+ data?.data.some(
20
+ feature => feature.name === availableFeatures.direct_messages_eap && feature.enabled
21
+ ) ?? false
22
+ )
23
+ }
@@ -43,6 +43,7 @@ export const availableFeatures = {
43
43
  custom_conversation_avatars: 'ROLLOUT_custom_conversation_avatars',
44
44
  conversation_safety_lock: 'ROLLOUT_conversation_safety_lock',
45
45
  video_moderation: 'ROLLOUT_MOBILE_video_moderation',
46
+ direct_messages_eap: 'ROLLOUT_direct_messages_eap',
46
47
  dm_search_clear_fix: 'ROLLOUT_MOBILE_dm_search_clear_fix',
47
48
  } as const satisfies Record<string, `ROLLOUT_${string}`>
48
49
 
@@ -2,7 +2,6 @@ import { StaticParamList } from '@react-navigation/native'
2
2
  import {
3
3
  createNativeStackNavigator,
4
4
  NativeStackHeaderRightProps,
5
- NativeStackNavigationOptions,
6
5
  } from '@react-navigation/native-stack'
7
6
  import { CardStyleInterpolators } from '@react-navigation/stack'
8
7
  import React from 'react'
@@ -11,9 +10,9 @@ import {
11
10
  createMinimalHeaderBackButton,
12
11
  withMinimalBack,
13
12
  withRightClose,
14
- withRightDone,
13
+ withRightText,
15
14
  } from '../components/display/platform_modal_header_buttons'
16
- import { useDirectMessagesEligible, useQualifiedByAge } from '../hooks'
15
+ import { useDirectMessagesEnabled, useQualifiedByAge } from '../hooks'
17
16
  import {
18
17
  AttachmentActionsScreen,
19
18
  AttachmentActionsScreenOptions,
@@ -80,17 +79,13 @@ import {
80
79
  SystemMessagePeopleScreenOptions,
81
80
  } from '../screens/system_message_people_screen'
82
81
  import { TeamConversationScreen } from '../screens/team_conversation_screen'
83
- import { isLiquidGlassEnabled } from '../utils/liquid_glass'
84
82
  import { ScreenLayoutWithChatAccessGate } from './screenLayout'
85
83
 
86
- const globalScreenOptions: () => NativeStackNavigationOptions = () => ({
87
- headerBackButtonDisplayMode: 'minimal',
88
- ...(isLiquidGlassEnabled() ? { headerShadowVisible: false } : {}),
89
- })
90
-
91
84
  export const NewConversationStack = createNativeStackNavigator({
92
85
  initialRouteName: 'ConversationNewEntry',
93
- screenOptions: globalScreenOptions,
86
+ screenOptions: {
87
+ headerBackButtonDisplayMode: 'minimal',
88
+ },
94
89
  screenLayout: ScreenLayoutWithChatAccessGate,
95
90
  screens: {
96
91
  ConversationNewEntry: {
@@ -101,7 +96,7 @@ export const NewConversationStack = createNativeStackNavigator({
101
96
  title: 'New conversation',
102
97
  headerBackVisible: false,
103
98
  },
104
- withRightClose({ onPress: navigation.goBack })
99
+ withRightText({ text: 'Cancel', onPress: navigation.goBack })
105
100
  ),
106
101
  },
107
102
  ConversationSelectGroupRecipients: {
@@ -111,7 +106,8 @@ export const NewConversationStack = createNativeStackNavigator({
111
106
  {
112
107
  title: 'New conversation',
113
108
  },
114
- withRightClose({
109
+ withRightText({
110
+ text: 'Cancel',
115
111
  onPress: () => navigation.popTo('Conversations', route.params),
116
112
  })
117
113
  ),
@@ -123,20 +119,22 @@ export const NewConversationStack = createNativeStackNavigator({
123
119
  {
124
120
  title: 'New conversation',
125
121
  },
126
- withRightClose({
122
+ withRightText({
123
+ text: 'Cancel',
127
124
  onPress: () => navigation.popTo('Conversations', route.params),
128
125
  })
129
126
  ),
130
127
  },
131
128
  ConversationSelectDirectMessageRecipients: {
132
129
  screen: ConversationSelectDirectMessageRecipientsScreen,
133
- if: useDirectMessagesEligible,
130
+ if: useDirectMessagesEnabled,
134
131
  options: ({ navigation, route }) =>
135
132
  composeOptions(
136
133
  {
137
134
  title: 'New conversation',
138
135
  },
139
- withRightClose({
136
+ withRightText({
137
+ text: 'Cancel',
140
138
  onPress: () => navigation.popTo('Conversations', route.params),
141
139
  })
142
140
  ),
@@ -153,7 +151,8 @@ export const NewConversationStack = createNativeStackNavigator({
153
151
  title: 'New conversation',
154
152
  headerLeft: () => null,
155
153
  },
156
- withRightClose({
154
+ withRightText({
155
+ text: 'Cancel',
157
156
  onPress: () => {
158
157
  const { chat_group_graph_id, group_source_app_name } =
159
158
  route.params as ConversationSelectRecipientsParams
@@ -170,7 +169,9 @@ export const NewConversationStack = createNativeStackNavigator({
170
169
  })
171
170
 
172
171
  export const ChatStack = createNativeStackNavigator({
173
- screenOptions: globalScreenOptions,
172
+ screenOptions: {
173
+ headerBackButtonDisplayMode: 'minimal',
174
+ },
174
175
  screenLayout: ScreenLayoutWithChatAccessGate,
175
176
  screens: {
176
177
  Conversations: {
@@ -250,7 +251,7 @@ export const ChatStack = createNativeStackNavigator({
250
251
  presentation: 'modal',
251
252
  title: 'Conversation details',
252
253
  },
253
- withRightDone({ onPress: navigation.goBack })
254
+ withRightText({ text: 'Done', onPress: navigation.goBack })
254
255
  ),
255
256
  },
256
257
  AvatarPicker: {
@@ -260,41 +261,46 @@ export const ChatStack = createNativeStackNavigator({
260
261
  ChatSettings: {
261
262
  screen: SettingsScreen,
262
263
  options: ({ navigation }) =>
263
- composeOptions({ title: 'Chat settings' }, withMinimalBack({ onPress: navigation.goBack })),
264
+ composeOptions(
265
+ {
266
+ title: 'Chat settings',
267
+ headerBackVisible: false,
268
+ },
269
+ withRightText({ text: 'Done', onPress: navigation.goBack })
270
+ ),
264
271
  },
265
272
  NotificationSettings: {
266
273
  screen: NotificationSettingsScreen,
267
274
  options: ({ navigation }) =>
268
- navigation.getState().index === 0
269
- ? composeOptions(
270
- { title: 'Chat notifications' },
271
- withRightClose({ onPress: navigation.goBack })
272
- )
273
- : composeOptions(
274
- { title: 'Chat notifications' },
275
- withMinimalBack({ onPress: navigation.goBack })
276
- ),
275
+ composeOptions(
276
+ {
277
+ title: 'Chat',
278
+ headerBackVisible: false,
279
+ },
280
+ withRightText({ text: 'Done', onPress: navigation.goBack })
281
+ ),
277
282
  },
278
283
  PreferredAppSelection: {
279
284
  screen: PreferredAppSelectionScreen,
280
- options: ({ navigation }) =>
281
- composeOptions({ title: 'Preferred app' }, withMinimalBack({ onPress: navigation.goBack })),
285
+ options: {
286
+ title: 'Preferred app',
287
+ },
282
288
  },
283
289
  GroupNotificationSettings: {
284
290
  screen: GroupNotificationSettingsScreen,
285
291
  options: ({ route, navigation }) => {
286
292
  const title = (route.params as { title?: string })?.title || 'Group Settings'
287
293
 
288
- // The dismiss button only shows when this is the entry screen (e.g.
289
- // deep links); deeper in the stack the back button covers dismissal.
294
+ // Done only shows when this is the entry screen (e.g. deep links);
295
+ // deeper in the stack the back button covers dismissal.
290
296
  return navigation.getState().index === 0
291
297
  ? composeOptions(
292
298
  {
293
299
  title,
294
300
  },
295
- withRightClose({ onPress: navigation.goBack })
301
+ withRightText({ text: 'Done', onPress: navigation.goBack })
296
302
  )
297
- : composeOptions({ title }, withMinimalBack({ onPress: navigation.goBack }))
303
+ : { title }
298
304
  },
299
305
  },
300
306
  GroupNotificationLevelSelect: {
@@ -322,7 +328,7 @@ export const ChatStack = createNativeStackNavigator({
322
328
  title: 'New conversation',
323
329
  headerLeft: () => null,
324
330
  },
325
- withRightClose({ onPress: () => navigation.getParent()?.goBack() })
331
+ withRightText({ text: 'Cancel', onPress: () => navigation.getParent()?.goBack() })
326
332
  ),
327
333
  },
328
334
  SendGiphy: {
@@ -371,7 +377,7 @@ export const ChatStack = createNativeStackNavigator({
371
377
  headerBackVisible: false,
372
378
  presentation: 'modal',
373
379
  },
374
- withRightClose({ onPress: navigation.goBack })
380
+ withRightText({ text: 'Close', onPress: navigation.goBack })
375
381
  ),
376
382
  },
377
383
  NotFound: {
@@ -28,7 +28,7 @@ import {
28
28
  composeOptions,
29
29
  HeaderDismissButton,
30
30
  useHeaderRight,
31
- withLeftClose,
31
+ withLeftText,
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
- withLeftClose({ onPress: () => navigation.goBack() })
63
+ withLeftText({ text: 'Cancel', onPress: () => navigation.goBack() })
64
64
  )
65
65
 
66
66
  interface Attachment {
@@ -158,13 +158,7 @@ export function BugReportScreen() {
158
158
  return ImagePicker.openImageLibraryAsync().then(result => uploadImagePickerResult(result))
159
159
  }
160
160
 
161
- useHeaderRight({
162
- icon: 'general.message',
163
- accessibilityLabel: 'Submit',
164
- variant: 'fill',
165
- onPress: handleSubmit,
166
- disabled: !formValid,
167
- })
161
+ useHeaderRight({ title: 'Submit', onPress: handleSubmit, disabled: !formValid })
168
162
 
169
163
  if (status === 'pending') {
170
164
  return (
@@ -223,8 +223,7 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
223
223
  )
224
224
 
225
225
  useHeaderRight({
226
- icon: 'general.check',
227
- accessibilityLabel: 'Done',
226
+ title: 'Done',
228
227
  onPress: () => {
229
228
  saveTitle({ title: trimmedTitle || conversation.title })
230
229
  navigation.goBack()
@@ -4,7 +4,7 @@ import { FlatList } from 'react-native-gesture-handler'
4
4
  import { Heading } from '../../../components'
5
5
  import {
6
6
  useAtFontScaleBreakpoint,
7
- useDirectMessagesEligible,
7
+ useDirectMessagesEnabled,
8
8
  useHasDirectMessages,
9
9
  useTheme,
10
10
  } from '../../../hooks'
@@ -59,9 +59,9 @@ export const ConversationFilters = () => {
59
59
  const { setScrollOffset, params } = useContext(FilterContext)
60
60
  const { chat_group_graph_id, group_source_app_name = '', show_direct_messages } = params
61
61
 
62
- const directMessagesEligible = useDirectMessagesEligible()
62
+ const directMessagesEnabled = useDirectMessagesEnabled()
63
63
  const hasDirectMessages = useHasDirectMessages()
64
- const canFilterByDMs = directMessagesEligible && hasDirectMessages
64
+ const canFilterByDMs = directMessagesEnabled && hasDirectMessages
65
65
 
66
66
  const activeFilter: FilterTypes = useMemo(() => {
67
67
  if (chat_group_graph_id) {