From 658a352bdb2f5052b8c475cedf63c14f58c6efd9 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 4 Oct 2024 09:19:51 -0700 Subject: [PATCH] fix event not firing (#5603) Co-authored-by: Samuel Newman --- modules/bottom-sheet/ios/SheetView.swift | 6 +- .../ios/SheetViewController.swift | 2 + modules/bottom-sheet/src/BottomSheet.tsx | 16 +--- modules/bottom-sheet/src/BottomSheet.types.ts | 19 ++-- package.json | 1 - patches/react-native+0.74.1.patch | 80 +++++++++++++---- src/App.native.tsx | 4 +- src/alf/util/useColorModeTheme.ts | 6 +- src/components/Dialog/index.tsx | 89 +++++++++++++++---- src/components/Dialog/sheet-wrapper.ts | 20 +++++ src/components/Menu/index.tsx | 2 +- src/screens/Onboarding/StepProfile/index.tsx | 36 +++++--- src/state/dialogs/index.tsx | 37 ++++++-- .../com/composer/photos/SelectPhotoBtn.tsx | 14 +-- src/view/com/util/UserAvatar.tsx | 12 ++- src/view/com/util/UserBanner.tsx | 6 +- src/view/shell/Composer.ios.tsx | 77 +++++----------- src/view/shell/index.tsx | 13 ++- yarn.lock | 8 -- 19 files changed, 282 insertions(+), 166 deletions(-) create mode 100644 src/components/Dialog/sheet-wrapper.ts diff --git a/modules/bottom-sheet/ios/SheetView.swift b/modules/bottom-sheet/ios/SheetView.swift index 0899eae6a0..f91cb43b45 100644 --- a/modules/bottom-sheet/ios/SheetView.swift +++ b/modules/bottom-sheet/ios/SheetView.swift @@ -129,11 +129,7 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate { if let sheet = sheetVc.sheetPresentationController { sheet.delegate = self sheet.preferredCornerRadius = self.cornerRadius - if sheet.detents.count == 1 { - self.selectedDetentIdentifier = .large - } else { - self.selectedDetentIdentifier = sheet.selectedDetentIdentifier - } + self.selectedDetentIdentifier = sheet.selectedDetentIdentifier } sheetVc.view.addSubview(innerView) diff --git a/modules/bottom-sheet/ios/SheetViewController.swift b/modules/bottom-sheet/ios/SheetViewController.swift index 28585eec02..8deef3723e 100644 --- a/modules/bottom-sheet/ios/SheetViewController.swift +++ b/modules/bottom-sheet/ios/SheetViewController.swift @@ -31,6 +31,7 @@ class SheetViewController: UIViewController { sheet.detents = [ .large() ] + sheet.selectedDetentIdentifier = .large } else { if #available(iOS 16.0, *) { sheet.detents = [ @@ -47,6 +48,7 @@ class SheetViewController: UIViewController { if !preventExpansion { sheet.detents.append(.large()) } + sheet.selectedDetentIdentifier = .medium } } diff --git a/modules/bottom-sheet/src/BottomSheet.tsx b/modules/bottom-sheet/src/BottomSheet.tsx index ccba8b35ae..489b76d2b6 100644 --- a/modules/bottom-sheet/src/BottomSheet.tsx +++ b/modules/bottom-sheet/src/BottomSheet.tsx @@ -1,11 +1,9 @@ import * as React from 'react' import { - ColorValue, Dimensions, NativeSyntheticEvent, Platform, StyleProp, - StyleSheet, View, ViewStyle, } from 'react-native' @@ -57,17 +55,6 @@ export class BottomSheet extends React.Component< this.props.onStateChange?.(event) } - private getBackgroundColor = (): ColorValue | undefined => { - const parent = React.Children.toArray( - this.props.children, - )[0] as React.ReactElement - if (parent?.props?.style) { - const parentStyle = StyleSheet.flatten(parent.props.style) as ViewStyle - return parentStyle.backgroundColor ?? 'transparent' - } - return undefined - } - private updateLayout = () => { this.ref.current?.updateLayout() } @@ -77,7 +64,7 @@ export class BottomSheet extends React.Component< } render() { - const {children, ...rest} = this.props + const {children, backgroundColor, ...rest} = this.props const topInset = rest.topInset ?? 0 const bottomInset = rest.bottomInset ?? 0 const cornerRadius = rest.cornerRadius ?? 0 @@ -86,7 +73,6 @@ export class BottomSheet extends React.Component< return null } - const backgroundColor = this.getBackgroundColor() return ( +export type BottomSheetSnapPointChangeEvent = NativeSyntheticEvent<{ + snapPoint: BottomSheetSnapPoint +}> +export type BottomSheetStateChangeEvent = NativeSyntheticEvent<{ + state: BottomSheetState +}> + export interface BottomSheetViewProps { children: React.ReactNode cornerRadius?: number preventDismiss?: boolean preventExpansion?: boolean + backgroundColor?: ColorValue containerBackgroundColor?: ColorValue topInset?: number bottomInset?: number @@ -21,11 +30,7 @@ export interface BottomSheetViewProps { minHeight?: number maxHeight?: number - onAttemptDismiss?: (event: NativeSyntheticEvent) => void - onSnapPointChange?: ( - event: NativeSyntheticEvent<{snapPoint: BottomSheetSnapPoint}>, - ) => void - onStateChange?: ( - event: NativeSyntheticEvent<{state: BottomSheetState}>, - ) => void + onAttemptDismiss?: (event: BottomSheetAttemptDismissEvent) => void + onSnapPointChange?: (event: BottomSheetSnapPointChangeEvent) => void + onStateChange?: (event: BottomSheetStateChangeEvent) => void } diff --git a/package.json b/package.json index 5250a14254..5aeaa8a42b 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,6 @@ "expo-sharing": "^12.0.1", "expo-splash-screen": "~0.27.4", "expo-status-bar": "~1.12.1", - "expo-system-ui": "~3.0.4", "expo-task-manager": "~11.8.1", "expo-updates": "~0.25.14", "expo-web-browser": "~13.0.3", diff --git a/patches/react-native+0.74.1.patch b/patches/react-native+0.74.1.patch index aee3da1ecc..ea6161e2ff 100644 --- a/patches/react-native+0.74.1.patch +++ b/patches/react-native+0.74.1.patch @@ -38,37 +38,65 @@ index b0d71dc..41b9a0e 100644 - (void)reactBlur diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h -index e9b330f..1ecdf0a 100644 +index e9b330f..ec5f58c 100644 --- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h +++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h -@@ -16,4 +16,6 @@ +@@ -15,5 +15,8 @@ + @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) RCTDirectEventBlock onRefresh; @property (nonatomic, weak) UIScrollView *scrollView; - -+- (void)forwarderBeginRefreshing; ++@property (nonatomic, copy) UIColor *customTintColor; + ++- (void)forwarderBeginRefreshing; + @end diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m -index b09e653..f93cb46 100644 +index b09e653..288e60c 100644 --- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m +++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m -@@ -198,9 +198,53 @@ - (void)refreshControlValueChanged - [self setCurrentRefreshingState:super.refreshing]; - _refreshingProgrammatically = NO; +@@ -22,6 +22,7 @@ @implementation RCTRefreshControl { + NSString *_title; + UIColor *_titleColor; + CGFloat _progressViewOffset; ++ UIColor *_customTintColor; + } -+ if (@available(iOS 17.4, *)) { -+ if (_currentRefreshingState) { -+ UIImpactFeedbackGenerator *feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight]; -+ [feedbackGenerator prepare]; -+ [feedbackGenerator impactOccurred]; -+ } -+ } + - (instancetype)init +@@ -56,6 +57,12 @@ - (void)layoutSubviews + _isInitialRender = false; + } + ++- (void)didMoveToSuperview ++{ ++ [super didMoveToSuperview]; ++ [self setTintColor:_customTintColor]; ++} + - if (_onRefresh) { - _onRefresh(nil); + - (void)beginRefreshingProgrammatically + { + UInt64 beginRefreshingTimestamp = _currentRefreshingStateTimestamp; +@@ -203,4 +210,58 @@ - (void)refreshControlValueChanged } } ++- (void)setCustomTintColor:(UIColor *)customTintColor ++{ ++ _customTintColor = customTintColor; ++ [self setTintColor:customTintColor]; ++} ++ ++// Fix for https://github.com/facebook/react-native/issues/43388 ++// A bug in iOS 17.4 causes the haptic to not play when refreshing if the tintColor ++// is set before the refresh control gets added to the scrollview. We'll call this ++// function whenever the superview changes. We'll also call it if the value of customTintColor ++// changes. ++- (void)setTintColor:(UIColor *)tintColor ++{ ++ if ([self.superview isKindOfClass:[UIScrollView class]] && self.tintColor != tintColor) { ++ [super setTintColor:tintColor]; ++ } ++} ++ +/* + This method is used by Bluesky's ExpoScrollForwarder. This allows other React Native + libraries to perform a refresh of a scrollview and access the refresh control's onRefresh @@ -106,6 +134,24 @@ index b09e653..f93cb46 100644 +} + @end +diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m +index 40aaf9c..1c60164 100644 +--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m ++++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m +@@ -22,11 +22,12 @@ - (UIView *)view + + RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL) +-RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) + RCT_EXPORT_VIEW_PROPERTY(title, NSString) + RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor) + RCT_EXPORT_VIEW_PROPERTY(progressViewOffset, CGFloat) + ++RCT_REMAP_VIEW_PROPERTY(tintColor, customTintColor, UIColor) ++ + RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing) + { + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java index 5f5e1ab..aac00b6 100644 --- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java diff --git a/src/App.native.tsx b/src/App.native.tsx index e2fcd6d2ec..c6334379f7 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -1,6 +1,6 @@ import 'react-native-url-polyfill/auto' -import 'lib/sentry' // must be near top -import 'view/icons' +import '#/lib/sentry' // must be near top +import '#/view/icons' import React, {useEffect, useState} from 'react' import {GestureHandlerRootView} from 'react-native-gesture-handler' diff --git a/src/alf/util/useColorModeTheme.ts b/src/alf/util/useColorModeTheme.ts index 12840c7062..561a504b2f 100644 --- a/src/alf/util/useColorModeTheme.ts +++ b/src/alf/util/useColorModeTheme.ts @@ -1,9 +1,8 @@ import React from 'react' import {ColorSchemeName, useColorScheme} from 'react-native' -import * as SystemUI from 'expo-system-ui' -import {isWeb} from 'platform/detection' -import {useThemePrefs} from 'state/shell' +import {isWeb} from '#/platform/detection' +import {useThemePrefs} from '#/state/shell' import {dark, dim, light} from '#/alf/themes' import {ThemeName} from '#/alf/types' @@ -12,7 +11,6 @@ export function useColorModeTheme(): ThemeName { React.useLayoutEffect(() => { updateDocument(theme) - SystemUI.setBackgroundColorAsync(getBackgroundColor(theme)) }, [theme]) return theme diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index 368cb2dc02..6bf2f4522c 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -7,12 +7,17 @@ import { View, ViewStyle, } from 'react-native' -import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' +import { + KeyboardAwareScrollView, + useKeyboardHandler, +} from 'react-native-keyboard-controller' +import {runOnJS} from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {logger} from '#/logger' +import {isIOS} from '#/platform/detection' import {useA11y} from '#/state/a11y' import {useDialogStateControlContext} from '#/state/dialogs' import {List, ListMethods, ListProps} from '#/view/com/util/List' @@ -26,6 +31,10 @@ import { import {createInput} from '#/components/forms/TextField' import {Portal as DefaultPortal} from '#/components/Portal' import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet' +import { + BottomSheetSnapPointChangeEvent, + BottomSheetStateChangeEvent, +} from '../../../modules/bottom-sheet/src/BottomSheet.types' export {useDialogContext, useDialogControl} from '#/components/Dialog/context' export * from '#/components/Dialog/types' @@ -45,7 +54,12 @@ export function Outer({ const ref = React.useRef(null) const insets = useSafeAreaInsets() const closeCallbacks = React.useRef<(() => void)[]>([]) - const {setDialogIsOpen} = useDialogStateControlContext() + const {setDialogIsOpen, setFullyExpandedCount} = + useDialogStateControlContext() + + const prevSnapPoint = React.useRef( + BottomSheetSnapPoint.Hidden, + ) const [snapPoint, setSnapPoint] = React.useState( BottomSheetSnapPoint.Partial, @@ -88,6 +102,36 @@ export function Outer({ onClose?.() }, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen]) + const onSnapPointChange = (e: BottomSheetSnapPointChangeEvent) => { + const {snapPoint} = e.nativeEvent + setSnapPoint(snapPoint) + console.log(e.nativeEvent) + + if ( + snapPoint === BottomSheetSnapPoint.Full && + prevSnapPoint.current !== BottomSheetSnapPoint.Full + ) { + setFullyExpandedCount(c => c + 1) + } else if ( + snapPoint !== BottomSheetSnapPoint.Full && + prevSnapPoint.current === BottomSheetSnapPoint.Full + ) { + setFullyExpandedCount(c => c - 1) + } + prevSnapPoint.current = snapPoint + } + + const onStateChange = (e: BottomSheetStateChangeEvent) => { + if (e.nativeEvent.state === 'closed') { + onCloseAnimationComplete() + + if (prevSnapPoint.current === BottomSheetSnapPoint.Full) { + setFullyExpandedCount(c => c - 1) + } + prevSnapPoint.current = BottomSheetSnapPoint.Hidden + } + } + useImperativeHandle( control.ref, () => ({ @@ -107,21 +151,14 @@ export function Outer({ { - setSnapPoint(e.nativeEvent.snapPoint) - }} - onStateChange={e => { - if (e.nativeEvent.state === 'closed') { - onCloseAnimationComplete() - } - }} + onSnapPointChange={onSnapPointChange} + onStateChange={onStateChange} cornerRadius={20} topInset={insets.top} bottomInset={insets.bottom} + backgroundColor={t.atoms.bg.backgroundColor} {...nativeOptions}> - - {children} - + {children} @@ -149,14 +186,28 @@ export const ScrollableInner = React.forwardRef( function ScrollableInner({children, style, ...props}, ref) { const {nativeSnapPoint} = useDialogContext() const insets = useSafeAreaInsets() + const [keyboardHeight, setKeyboardHeight] = React.useState(0) + useKeyboardHandler({ + onEnd: e => { + 'worklet' + runOnJS(setKeyboardHeight)(e.height) + }, + }) + + console.log('kb:', keyboardHeight) + + const basePading = + (isIOS ? 30 : 50) + (isIOS ? keyboardHeight / 4 : keyboardHeight) + const fullPaddingBase = insets.bottom + insets.top + basePading + const fullPadding = isIOS ? fullPaddingBase : fullPaddingBase + 50 + + const paddingBottom = + nativeSnapPoint === BottomSheetSnapPoint.Full ? fullPadding : basePading + return ( + close()} diff --git a/src/components/Dialog/sheet-wrapper.ts b/src/components/Dialog/sheet-wrapper.ts new file mode 100644 index 0000000000..37c6633837 --- /dev/null +++ b/src/components/Dialog/sheet-wrapper.ts @@ -0,0 +1,20 @@ +import {useCallback} from 'react' + +import {useDialogStateControlContext} from '#/state/dialogs' + +/** + * If we're calling a system API like the image picker that opens a sheet + * wrap it in this function to make sure the status bar is the correct color. + */ +export function useSheetWrapper() { + const {setFullyExpandedCount} = useDialogStateControlContext() + return useCallback( + async (promise: Promise): Promise => { + setFullyExpandedCount(c => c + 1) + const res = await promise + setFullyExpandedCount(c => c - 1) + return res + }, + [setFullyExpandedCount], + ) +} diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index b5ce6f583e..2796d793c0 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -90,7 +90,7 @@ export function Outer({ {/* Re-wrap with context since Dialogs are portal-ed to root */} - + {children} {isNative && showCancel && } diff --git a/src/screens/Onboarding/StepProfile/index.tsx b/src/screens/Onboarding/StepProfile/index.tsx index c6a39ab45e..73472ec332 100644 --- a/src/screens/Onboarding/StepProfile/index.tsx +++ b/src/screens/Onboarding/StepProfile/index.tsx @@ -32,6 +32,7 @@ import { import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' +import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' import {IconCircle} from '#/components/IconCircle' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo' @@ -89,15 +90,18 @@ export function StepProfile() { requestNotificationsPermission('StartOnboarding') }, [gate, requestNotificationsPermission]) + const sheetWrapper = useSheetWrapper() const openPicker = React.useCallback( async (opts?: ImagePickerOptions) => { - const response = await launchImageLibraryAsync({ - exif: false, - mediaTypes: MediaTypeOptions.Images, - quality: 1, - ...opts, - legacy: true, - }) + const response = await sheetWrapper( + launchImageLibraryAsync({ + exif: false, + mediaTypes: MediaTypeOptions.Images, + quality: 1, + ...opts, + legacy: true, + }), + ) return (response.assets ?? []) .slice(0, 1) @@ -121,7 +125,7 @@ export function StepProfile() { size: getDataUriSize(image.uri), })) }, - [_, setError], + [_, setError, sheetWrapper], ) const onContinue = React.useCallback(async () => { @@ -168,9 +172,11 @@ export function StepProfile() { setError('') - const items = await openPicker({ - aspect: [1, 1], - }) + const items = await sheetWrapper( + openPicker({ + aspect: [1, 1], + }), + ) let image = items[0] if (!image) return @@ -196,7 +202,13 @@ export function StepProfile() { image, useCreatedAvatar: false, })) - }, [requestPhotoAccessIfNeeded, setAvatar, openPicker, setError]) + }, [ + requestPhotoAccessIfNeeded, + setAvatar, + openPicker, + setError, + sheetWrapper, + ]) const onSecondaryPress = React.useCallback(() => { if (avatar.useCreatedAvatar) { diff --git a/src/state/dialogs/index.tsx b/src/state/dialogs/index.tsx index 315ab412c5..80893190fc 100644 --- a/src/state/dialogs/index.tsx +++ b/src/state/dialogs/index.tsx @@ -19,15 +19,22 @@ interface IDialogContext { openDialogs: React.MutableRefObject> } -const DialogContext = React.createContext({} as IDialogContext) - -const DialogControlContext = React.createContext<{ +interface IDialogControlContext { closeAllDialogs(): boolean setDialogIsOpen(id: string, isOpen: boolean): void -}>({ - closeAllDialogs: () => false, - setDialogIsOpen: () => {}, -}) + /** + * The number of dialogs that are fully expanded. This is used to determine the backgground color of the status bar + * on iOS. + */ + fullyExpandedCount: number + setFullyExpandedCount: React.Dispatch> +} + +const DialogContext = React.createContext({} as IDialogContext) + +const DialogControlContext = React.createContext( + {} as IDialogControlContext, +) export function useDialogStateContext() { return React.useContext(DialogContext) @@ -38,6 +45,8 @@ export function useDialogStateControlContext() { } export function Provider({children}: React.PropsWithChildren<{}>) { + const [fullyExpandedCount, setFullyExpandedCount] = React.useState(0) + const activeDialogs = React.useRef< Map> >(new Map()) @@ -73,8 +82,18 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [activeDialogs, openDialogs], ) const controls = React.useMemo( - () => ({closeAllDialogs, setDialogIsOpen}), - [closeAllDialogs, setDialogIsOpen], + () => ({ + closeAllDialogs, + setDialogIsOpen, + fullyExpandedCount, + setFullyExpandedCount, + }), + [ + closeAllDialogs, + setDialogIsOpen, + fullyExpandedCount, + setFullyExpandedCount, + ], ) return ( diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx index 34ead3d9a9..37bfbafe60 100644 --- a/src/view/com/composer/photos/SelectPhotoBtn.tsx +++ b/src/view/com/composer/photos/SelectPhotoBtn.tsx @@ -9,6 +9,7 @@ import {isNative} from '#/platform/detection' import {ComposerImage, createComposerImage} from '#/state/gallery' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' +import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' import {Image_Stroke2_Corner0_Rounded as Image} from '#/components/icons/Image' type Props = { @@ -21,23 +22,26 @@ export function SelectPhotoBtn({size, disabled, onAdd}: Props) { const {_} = useLingui() const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() const t = useTheme() + const sheetWrapper = useSheetWrapper() const onPressSelectPhotos = useCallback(async () => { if (isNative && !(await requestPhotoAccessIfNeeded())) { return } - const images = await openPicker({ - selectionLimit: 4 - size, - allowsMultipleSelection: true, - }) + const images = await sheetWrapper( + openPicker({ + selectionLimit: 4 - size, + allowsMultipleSelection: true, + }), + ) const results = await Promise.all( images.map(img => createComposerImage(img)), ) onAdd(results) - }, [requestPhotoAccessIfNeeded, size, onAdd]) + }, [requestPhotoAccessIfNeeded, size, onAdd, sheetWrapper]) return (