@planningcenter/chat-react-native 3.42.3-qa-staging.1 → 3.42.3-qa-staging.3
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.
- package/build/components/display/platform_modal_header_buttons.d.ts +47 -7
- package/build/components/display/platform_modal_header_buttons.d.ts.map +1 -1
- package/build/components/display/platform_modal_header_buttons.js +119 -9
- package/build/components/display/platform_modal_header_buttons.js.map +1 -1
- package/build/contexts/chat_context.d.ts +1 -0
- package/build/contexts/chat_context.d.ts.map +1 -1
- package/build/contexts/chat_context.js +7 -1
- package/build/contexts/chat_context.js.map +1 -1
- package/build/hooks/index.d.ts +1 -1
- package/build/hooks/index.d.ts.map +1 -1
- package/build/hooks/index.js +1 -1
- package/build/hooks/index.js.map +1 -1
- package/build/hooks/use_current_person.d.ts.map +1 -1
- package/build/hooks/use_current_person.js +1 -0
- package/build/hooks/use_current_person.js.map +1 -1
- package/build/hooks/use_direct_messages_eligible.d.ts +2 -0
- package/build/hooks/use_direct_messages_eligible.d.ts.map +1 -0
- package/build/hooks/use_direct_messages_eligible.js +9 -0
- package/build/hooks/use_direct_messages_eligible.js.map +1 -0
- package/build/hooks/use_features.d.ts +0 -1
- package/build/hooks/use_features.d.ts.map +1 -1
- package/build/hooks/use_features.js +0 -1
- package/build/hooks/use_features.js.map +1 -1
- package/build/navigation/index.d.ts +69 -173
- package/build/navigation/index.d.ts.map +1 -1
- package/build/navigation/index.js +61 -72
- package/build/navigation/index.js.map +1 -1
- package/build/screens/bug_report_screen.d.ts.map +1 -1
- package/build/screens/bug_report_screen.js +10 -13
- package/build/screens/bug_report_screen.js.map +1 -1
- package/build/screens/conversation_details_screen.d.ts.map +1 -1
- package/build/screens/conversation_details_screen.js +11 -12
- package/build/screens/conversation_details_screen.js.map +1 -1
- package/build/screens/conversation_filters/components/conversation_filters.js +3 -3
- package/build/screens/conversation_filters/components/conversation_filters.js.map +1 -1
- package/build/screens/conversation_filters_screen.js +2 -2
- package/build/screens/conversation_filters_screen.js.map +1 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +9 -2
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/screens/conversation_select_type_screen.d.ts.map +1 -1
- package/build/screens/conversation_select_type_screen.js +4 -7
- package/build/screens/conversation_select_type_screen.js.map +1 -1
- package/build/screens/conversations/components/list_header_component.js +3 -3
- package/build/screens/conversations/components/list_header_component.js.map +1 -1
- package/build/screens/message_report_screen.d.ts.map +1 -1
- package/build/screens/message_report_screen.js +14 -18
- package/build/screens/message_report_screen.js.map +1 -1
- package/build/screens/notification_settings_screen.d.ts.map +1 -1
- package/build/screens/notification_settings_screen.js +0 -14
- package/build/screens/notification_settings_screen.js.map +1 -1
- package/build/screens/settings_screen.js +3 -3
- package/build/screens/settings_screen.js.map +1 -1
- package/build/types/resources/person.d.ts +1 -0
- package/build/types/resources/person.d.ts.map +1 -1
- package/build/types/resources/person.js.map +1 -1
- package/build/utils/index.d.ts +1 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +1 -0
- package/build/utils/index.js.map +1 -1
- package/build/utils/liquid_glass.d.ts +3 -0
- package/build/utils/liquid_glass.d.ts.map +1 -0
- package/build/utils/liquid_glass.js +9 -0
- package/build/utils/liquid_glass.js.map +1 -0
- package/package.json +3 -3
- package/src/__tests__/hooks/use_direct_messages_eligible.test.tsx +77 -0
- package/src/__tests__/utils/liquid_glass.ts +40 -0
- package/src/components/display/platform_modal_header_buttons.tsx +254 -13
- package/src/contexts/chat_context.tsx +8 -0
- package/src/hooks/index.ts +1 -1
- package/src/hooks/use_current_person.ts +1 -0
- package/src/hooks/use_direct_messages_eligible.ts +11 -0
- package/src/hooks/use_features.ts +0 -1
- package/src/navigation/index.tsx +149 -166
- package/src/screens/bug_report_screen.tsx +16 -15
- package/src/screens/conversation_details_screen.tsx +11 -17
- package/src/screens/conversation_filters/components/conversation_filters.tsx +3 -3
- package/src/screens/conversation_filters_screen.tsx +2 -2
- package/src/screens/conversation_screen.tsx +10 -2
- package/src/screens/conversation_select_type_screen.tsx +4 -7
- package/src/screens/conversations/components/list_header_component.tsx +3 -3
- package/src/screens/message_report_screen.tsx +20 -21
- package/src/screens/notification_settings_screen.tsx +1 -21
- package/src/screens/settings_screen.tsx +3 -3
- package/src/types/resources/person.ts +1 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/liquid_glass.ts +12 -0
- package/build/hooks/use_direct_messages_enabled.d.ts +0 -2
- package/build/hooks/use_direct_messages_enabled.d.ts.map +0 -1
- package/build/hooks/use_direct_messages_enabled.js +0 -16
- package/build/hooks/use_direct_messages_enabled.js.map +0 -1
- package/src/hooks/use_direct_messages_enabled.ts +0 -23
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"liquid_glass.d.ts","sourceRoot":"","sources":["../../src/utils/liquid_glass.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,wBAAwB,GAAI,SAAS,OAAO,SAExD,CAAA;AAID,eAAO,MAAM,oBAAoB,eACgE,CAAA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
let hostLiquidGlassEnabled = false;
|
|
3
|
+
export const setLiquidGlassCapability = (enabled) => {
|
|
4
|
+
hostLiquidGlassEnabled = enabled;
|
|
5
|
+
};
|
|
6
|
+
// The host flag is required because JS cannot detect a binary built with a
|
|
7
|
+
// pre-iOS-26 SDK or opted out via UIDesignRequiresCompatibility.
|
|
8
|
+
export const isLiquidGlassEnabled = () => hostLiquidGlassEnabled && Platform.OS === 'ios' && parseInt(String(Platform.Version), 10) >= 26;
|
|
9
|
+
//# sourceMappingURL=liquid_glass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"liquid_glass.js","sourceRoot":"","sources":["../../src/utils/liquid_glass.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAEvC,IAAI,sBAAsB,GAAG,KAAK,CAAA;AAElC,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,OAAgB,EAAE,EAAE;IAC3D,sBAAsB,GAAG,OAAO,CAAA;AAClC,CAAC,CAAA;AAED,2EAA2E;AAC3E,iEAAiE;AACjE,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,EAAE,CACvC,sBAAsB,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,CAAA","sourcesContent":["import { Platform } from 'react-native'\n\nlet hostLiquidGlassEnabled = false\n\nexport const setLiquidGlassCapability = (enabled: boolean) => {\n hostLiquidGlassEnabled = enabled\n}\n\n// The host flag is required because JS cannot detect a binary built with a\n// pre-iOS-26 SDK or opted out via UIDesignRequiresCompatibility.\nexport const isLiquidGlassEnabled = () =>\n hostLiquidGlassEnabled && Platform.OS === 'ios' && parseInt(String(Platform.Version), 10) >= 26\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/chat-react-native",
|
|
3
|
-
"version": "3.42.3-qa-staging.
|
|
3
|
+
"version": "3.42.3-qa-staging.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "./src/index.tsx",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
|
28
28
|
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
|
29
|
-
"@planningcenter/emoji-keyboard": "3.42.3-qa-staging.
|
|
29
|
+
"@planningcenter/emoji-keyboard": "3.42.3-qa-staging.3",
|
|
30
30
|
"lodash-inflection": "^1.5.0",
|
|
31
31
|
"react-compiler-runtime": "^1.0.0"
|
|
32
32
|
},
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
"react-native-url-polyfill": "^2.0.0",
|
|
73
73
|
"typescript": "~6.0.3"
|
|
74
74
|
},
|
|
75
|
-
"gitHead": "
|
|
75
|
+
"gitHead": "b1cd8076ea900ce230b4c652b2175ce0f59d9f97"
|
|
76
76
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
+
import { act, renderHook, waitFor } from '@testing-library/react-native'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { buildTestQueryClient } from '../../__utils__/query_client'
|
|
5
|
+
import { ChatContext, ChatContextValue } from '../../contexts/chat_context'
|
|
6
|
+
import { ApiClient, useApiClient } from '../../hooks/use_api_client'
|
|
7
|
+
import { currentPersonQueryKey } from '../../hooks/use_current_person'
|
|
8
|
+
import { useDirectMessagesEligible } from '../../hooks/use_direct_messages_eligible'
|
|
9
|
+
import { Session } from '../../utils'
|
|
10
|
+
|
|
11
|
+
jest.mock('../../hooks/use_api_client')
|
|
12
|
+
|
|
13
|
+
const mockedUseApiClient = useApiClient as jest.MockedFunction<typeof useApiClient>
|
|
14
|
+
|
|
15
|
+
let get: jest.Mock
|
|
16
|
+
|
|
17
|
+
const session = new Session({ token: { access_token: 'a-real-token' } })
|
|
18
|
+
|
|
19
|
+
const renderUseDirectMessagesEligible = (queryClient = buildTestQueryClient()) => {
|
|
20
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
21
|
+
<QueryClientProvider client={queryClient}>
|
|
22
|
+
<ChatContext.Provider value={{ session } as unknown as ChatContextValue}>
|
|
23
|
+
{children}
|
|
24
|
+
</ChatContext.Provider>
|
|
25
|
+
</QueryClientProvider>
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return { ...renderHook(() => useDirectMessagesEligible(), { wrapper }), queryClient }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const flushMicrotasks = () =>
|
|
32
|
+
act(async () => {
|
|
33
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
get = jest.fn().mockResolvedValue({ data: { id: 1, directMessagesEligible: true } })
|
|
38
|
+
mockedUseApiClient.mockReturnValue({ chat: { get } } as unknown as ApiClient)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
jest.clearAllMocks()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('useDirectMessagesEligible', () => {
|
|
46
|
+
it('returns true once the person is fetched as eligible', async () => {
|
|
47
|
+
get.mockResolvedValue({
|
|
48
|
+
data: { id: 1, directMessagesEligible: true, directMessagesDisabled: true },
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const { result } = renderUseDirectMessagesEligible()
|
|
52
|
+
|
|
53
|
+
await waitFor(() => expect(result.current).toBe(true))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('returns false when the attribute is absent from the response', async () => {
|
|
57
|
+
get.mockResolvedValue({ data: { id: 1 } })
|
|
58
|
+
|
|
59
|
+
const { result } = renderUseDirectMessagesEligible()
|
|
60
|
+
|
|
61
|
+
await waitFor(() => expect(get).toHaveBeenCalled())
|
|
62
|
+
await flushMicrotasks()
|
|
63
|
+
|
|
64
|
+
expect(result.current).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('reads the same /me cache entry as useCurrentPerson', () => {
|
|
68
|
+
const queryClient = buildTestQueryClient()
|
|
69
|
+
queryClient.setQueryData(currentPersonQueryKey, {
|
|
70
|
+
data: { id: 1, directMessagesEligible: true },
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const { result } = renderUseDirectMessagesEligible(queryClient)
|
|
74
|
+
|
|
75
|
+
expect(result.current).toBe(true)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Platform } from 'react-native'
|
|
2
|
+
import { isLiquidGlassEnabled, setLiquidGlassCapability } from '../../utils/liquid_glass'
|
|
3
|
+
|
|
4
|
+
const mockPlatform = (OS: 'ios' | 'android', Version: string | number) => {
|
|
5
|
+
Object.defineProperty(Platform, 'OS', { value: OS, configurable: true })
|
|
6
|
+
Object.defineProperty(Platform, 'Version', { value: Version, configurable: true })
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('isLiquidGlassEnabled', () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
setLiquidGlassCapability(false)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('is false until the host enables it', () => {
|
|
15
|
+
mockPlatform('ios', '26.0')
|
|
16
|
+
|
|
17
|
+
expect(isLiquidGlassEnabled()).toBe(false)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('is true when the host enables it on iOS 26 or later', () => {
|
|
21
|
+
mockPlatform('ios', '26.0')
|
|
22
|
+
setLiquidGlassCapability(true)
|
|
23
|
+
|
|
24
|
+
expect(isLiquidGlassEnabled()).toBe(true)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('is false below iOS 26 even when the host enables it', () => {
|
|
28
|
+
mockPlatform('ios', '18.5')
|
|
29
|
+
setLiquidGlassCapability(true)
|
|
30
|
+
|
|
31
|
+
expect(isLiquidGlassEnabled()).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('is false on android even when the host enables it', () => {
|
|
35
|
+
mockPlatform('android', 36)
|
|
36
|
+
setLiquidGlassCapability(true)
|
|
37
|
+
|
|
38
|
+
expect(isLiquidGlassEnabled()).toBe(false)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import { HeaderButton } from '@react-navigation/elements'
|
|
2
|
-
import
|
|
1
|
+
import { HeaderBackButton, HeaderButton, PlatformPressable } from '@react-navigation/elements'
|
|
2
|
+
import { useNavigation } from '@react-navigation/native'
|
|
3
|
+
import type {
|
|
4
|
+
NativeStackHeaderItemProps,
|
|
5
|
+
NativeStackNavigationOptions,
|
|
6
|
+
} from '@react-navigation/native-stack'
|
|
7
|
+
import { useLayoutEffect } from 'react'
|
|
3
8
|
import { ColorValue, Platform, StyleSheet } from 'react-native'
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
9
|
+
import LinearGradient from 'react-native-linear-gradient'
|
|
10
|
+
import { useFontScale, useTheme } from '../../hooks'
|
|
11
|
+
import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../../utils'
|
|
12
|
+
import { isLiquidGlassEnabled } from '../../utils/liquid_glass'
|
|
13
|
+
import { tokens } from '../../vendor/tapestry/tokens'
|
|
14
|
+
import { Icon, type IconString } from './icon'
|
|
6
15
|
import { TextButton } from './text_button'
|
|
16
|
+
import { getColorKey, useGradientColorMap } from './utils/button_colors'
|
|
7
17
|
|
|
8
18
|
interface HeaderTextButtonProps {
|
|
9
19
|
onPress: () => void
|
|
@@ -63,19 +73,250 @@ export const HeaderDismissButton = ({
|
|
|
63
73
|
})
|
|
64
74
|
}
|
|
65
75
|
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
const createTextHeaderRight =
|
|
77
|
+
({ onPress, title, disabled }: HeaderTextButtonProps) =>
|
|
78
|
+
(props: NativeStackHeaderItemProps) => (
|
|
79
|
+
<HeaderTextButton {...props} onPress={onPress} title={title} disabled={disabled} />
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
export type OptionsDecorator = (
|
|
83
|
+
options: NativeStackNavigationOptions
|
|
84
|
+
) => NativeStackNavigationOptions
|
|
85
|
+
|
|
86
|
+
export const composeOptions = (
|
|
87
|
+
base: NativeStackNavigationOptions,
|
|
88
|
+
...decorators: OptionsDecorator[]
|
|
89
|
+
): NativeStackNavigationOptions => decorators.reduce((options, decorate) => decorate(options), base)
|
|
90
|
+
|
|
91
|
+
interface TextButtonDecoratorProps {
|
|
92
|
+
text: string
|
|
93
|
+
onPress: () => void
|
|
94
|
+
disabled?: boolean
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Native bar items pick up liquid glass on iOS 26; hosts on an older
|
|
98
|
+
// react-navigation ignore the unstable option and keep using the
|
|
99
|
+
// headerRight/headerLeft fallbacks, as does Android.
|
|
100
|
+
export const withRightText =
|
|
101
|
+
({ text, onPress, disabled }: TextButtonDecoratorProps): OptionsDecorator =>
|
|
102
|
+
options => ({
|
|
103
|
+
...options,
|
|
104
|
+
headerRight: createTextHeaderRight({ onPress, title: text, disabled }),
|
|
105
|
+
unstable_headerRightItems: () => [{ type: 'button' as const, label: text, onPress, disabled }],
|
|
106
|
+
})
|
|
107
|
+
|
|
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
|
+
)
|
|
68
182
|
}
|
|
69
183
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 =>
|
|
228
|
+
options => ({
|
|
229
|
+
...options,
|
|
230
|
+
headerRight: createHeaderIconElement(props),
|
|
231
|
+
unstable_headerRightItems: () => [createHeaderIconItem(props)],
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
export const withLeftIcon =
|
|
235
|
+
(props: HeaderIconProps): OptionsDecorator =>
|
|
236
|
+
options => ({
|
|
237
|
+
...options,
|
|
238
|
+
headerLeft: createHeaderIconElement(props),
|
|
239
|
+
unstable_headerLeftItems: () => [createHeaderIconItem(props)],
|
|
240
|
+
})
|
|
241
|
+
|
|
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
|
+
// The default back icon reserves trailing space for a label that
|
|
252
|
+
// displayMode="minimal" never shows; a custom image skips it so the glass
|
|
253
|
+
// capsule hugs the glyph.
|
|
254
|
+
const renderGlassBackImage = ({ tintColor }: { tintColor: string }) => (
|
|
255
|
+
<Icon name="general.leftChevron" size={18} color={tintColor} />
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
// The iOS 26 glass capsule manages its own insets — negative margins clip
|
|
259
|
+
// the button against the capsule edge.
|
|
260
|
+
export const createMinimalHeaderBackButton =
|
|
261
|
+
({ onPress }: { onPress: () => void }) =>
|
|
262
|
+
(props: NativeStackHeaderItemProps) => (
|
|
263
|
+
<HeaderBackButton
|
|
264
|
+
style={
|
|
265
|
+
isLiquidGlassEnabled()
|
|
266
|
+
? undefined
|
|
267
|
+
: {
|
|
268
|
+
marginLeft: Platform.select({ ios: -8, default: -3 }),
|
|
269
|
+
marginRight: Platform.select({ ios: 0, default: 30 }),
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
backImage={isLiquidGlassEnabled() ? renderGlassBackImage : undefined}
|
|
273
|
+
displayMode="minimal"
|
|
274
|
+
onPress={onPress}
|
|
275
|
+
{...props}
|
|
276
|
+
/>
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
// Under glass the native minimal back button suffices; pre-26 needs the
|
|
280
|
+
// custom button and its layout nudges.
|
|
281
|
+
export const withMinimalBack =
|
|
282
|
+
({ onPress }: { onPress: () => void }): OptionsDecorator =>
|
|
283
|
+
options => ({
|
|
284
|
+
...options,
|
|
285
|
+
...(isLiquidGlassEnabled() ? {} : { headerLeft: createMinimalHeaderBackButton({ onPress }) }),
|
|
286
|
+
})
|
|
287
|
+
|
|
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 = {
|
|
291
|
+
onPress: () => void
|
|
292
|
+
disabled?: boolean
|
|
293
|
+
enabled?: boolean
|
|
294
|
+
} & (
|
|
295
|
+
| (HeaderIconProps & { title?: never })
|
|
296
|
+
| { title: string; icon?: never; accessibilityLabel?: never; variant?: never }
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
// For screens whose header button depends on live state (form validity,
|
|
300
|
+
// mutation callbacks). Static buttons belong in the navigator options.
|
|
301
|
+
export const useHeaderRight = (options: UseHeaderRightOptions) => {
|
|
302
|
+
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
|
+
|
|
308
|
+
useLayoutEffect(() => {
|
|
309
|
+
if (!enabled) {
|
|
310
|
+
navigation.setOptions({ headerRight: () => null, unstable_headerRightItems: undefined })
|
|
311
|
+
return
|
|
312
|
+
}
|
|
73
313
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
314
|
+
const decorate = icon
|
|
315
|
+
? withRightIcon({ icon, accessibilityLabel: label, onPress, disabled, variant })
|
|
316
|
+
: withRightText({ text: label, onPress, disabled })
|
|
77
317
|
|
|
78
|
-
|
|
318
|
+
navigation.setOptions(decorate({}))
|
|
319
|
+
}, [disabled, enabled, icon, label, navigation, onPress, variant])
|
|
79
320
|
}
|
|
80
321
|
|
|
81
322
|
const useStyles = () => {
|
|
@@ -7,6 +7,7 @@ import { ApiResource, DeepPartial } from '../types'
|
|
|
7
7
|
import { FailedResponse } from '../types'
|
|
8
8
|
import { ProductAnalyticsConfig } from '../types/product_analytics'
|
|
9
9
|
import { Client, ENV, OauthType, PartialToken, Session, Uri } from '../utils'
|
|
10
|
+
import { setLiquidGlassCapability } from '../utils/liquid_glass'
|
|
10
11
|
import { ChatTheme, defaultTheme, DefaultTheme } from '../utils/theme'
|
|
11
12
|
|
|
12
13
|
export interface ChatProviderProps {
|
|
@@ -18,6 +19,7 @@ export interface ChatProviderProps {
|
|
|
18
19
|
tokenType?: OauthType
|
|
19
20
|
theme: CreateChatThemeProps
|
|
20
21
|
edgeToEdge?: boolean
|
|
22
|
+
liquidGlassEnabled?: boolean
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export interface ChatContextValue extends Omit<ChatProviderProps, 'theme'> {
|
|
@@ -36,6 +38,7 @@ export const ChatContext = createContext<ChatContextValue>({
|
|
|
36
38
|
theme: defaultTheme('light'),
|
|
37
39
|
token: undefined,
|
|
38
40
|
edgeToEdge: true,
|
|
41
|
+
liquidGlassEnabled: false,
|
|
39
42
|
productAnalyticsConfig: undefined,
|
|
40
43
|
})
|
|
41
44
|
|
|
@@ -48,7 +51,11 @@ export function ChatProvider({ children, value }: { children: any; value: ChatPr
|
|
|
48
51
|
giphyApiKey,
|
|
49
52
|
tokenType,
|
|
50
53
|
edgeToEdge = true,
|
|
54
|
+
liquidGlassEnabled = false,
|
|
51
55
|
} = value
|
|
56
|
+
// Set during render, not in an effect — navigator options factories read it
|
|
57
|
+
// before effects would run.
|
|
58
|
+
setLiquidGlassCapability(liquidGlassEnabled)
|
|
52
59
|
const theme = useCreateChatTheme(value.theme || {})
|
|
53
60
|
const session = useMemo(
|
|
54
61
|
() => new Session({ token, env, type: tokenType }),
|
|
@@ -59,6 +66,7 @@ export function ChatProvider({ children, value }: { children: any; value: ChatPr
|
|
|
59
66
|
const contextValue: ChatContextValue = {
|
|
60
67
|
...value,
|
|
61
68
|
edgeToEdge,
|
|
69
|
+
liquidGlassEnabled,
|
|
62
70
|
env,
|
|
63
71
|
giphyApiKey,
|
|
64
72
|
onAgeDisqualification,
|
package/src/hooks/index.ts
CHANGED
|
@@ -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 './
|
|
11
|
+
export * from './use_direct_messages_eligible'
|
|
12
12
|
export * from './use_font_scale'
|
|
13
13
|
export * from './use_has_direct_messages'
|
|
14
14
|
export * from './use_groups_groups'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { CurrentPersonResource } from '../types'
|
|
2
|
+
import { useApiGet } from './use_api'
|
|
3
|
+
import { currentPersonRequestArgs } from './use_current_person'
|
|
4
|
+
|
|
5
|
+
export const useDirectMessagesEligible = (): boolean => {
|
|
6
|
+
const { data: person, isFetched } = useApiGet<CurrentPersonResource>(currentPersonRequestArgs)
|
|
7
|
+
|
|
8
|
+
if (!isFetched) return false
|
|
9
|
+
|
|
10
|
+
return person?.directMessagesEligible ?? false
|
|
11
|
+
}
|
|
@@ -43,7 +43,6 @@ 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',
|
|
47
46
|
dm_search_clear_fix: 'ROLLOUT_MOBILE_dm_search_clear_fix',
|
|
48
47
|
} as const satisfies Record<string, `ROLLOUT_${string}`>
|
|
49
48
|
|