mattermost-mobile-upstream/app/hooks/gallery.ts
JG Heithcock d12bd1d770
[MM-69455] Fix channel bookmark taps with React Native New Architecture (#9904)
* [MM-69455] Fix channel bookmark taps broken by the switch to React Native New Architecture

useGalleryItem returned a Reanimated worklet as the onPress handler.
With React Native's New Architecture enabled, standard touch handlers
do not reliably invoke worklets, so file and image bookmark taps
silently failed to open the gallery. Replace the worklet with a plain
JS callback.

Also fix swapped openLink arguments for link bookmarks, replace @rneui
Button with Pressable on bookmark rows, and add Detox regression
coverage for file tap and link long-press behavior.

* Address CodeRabbit review feedback for channel bookmark fix.

Add accessibilityRole to bookmark Pressables, use nullish coalescing for
optional bookmark index, and rename the Detox test to MM-T69455_1.

* Revert changes for scheduleOnRN
2026-07-02 13:55:27 -07:00

139 lines
3.4 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useCallback} from 'react';
import {
Easing, useAnimatedRef, useAnimatedStyle,
useSharedValue,
withTiming, type WithTimingConfig,
} from 'react-native-reanimated';
import {scheduleOnRN} from 'react-native-worklets';
import {useGallery} from '@context/gallery';
import useDidMount from './did_mount';
interface DiffEntry {
stash: number;
prev: number | null;
}
export interface DiffContext {
___diffs?: Record<string, DiffEntry>;
}
export function diff(context: DiffContext, name: string, value: number) {
'worklet';
if (!context.___diffs) {
context.___diffs = {};
}
if (!context.___diffs[name]) {
context.___diffs[name] = {
stash: 0,
prev: null,
};
}
const d = context.___diffs[name];
d.stash = d.prev === null ? 0 : value - d.prev;
d.prev = value;
return d.stash;
}
export const translateYConfig: WithTimingConfig = {
duration: 400,
easing: Easing.bezier(0.33, 0.01, 0, 1),
};
export function useGalleryControls(bottomInset = 0) {
const headerAndFooterHidden = useSharedValue(false);
const headerStyles = useAnimatedStyle(() => ({
opacity: headerAndFooterHidden.value ? withTiming(0) : withTiming(1),
transform: [
{
translateY: headerAndFooterHidden.value ? withTiming(-100, translateYConfig) : withTiming(0, translateYConfig),
},
],
position: 'absolute',
top: 0,
width: '100%',
zIndex: 1,
}));
const footerStyles = useAnimatedStyle(() => ({
backgroundColor: '#000',
opacity: headerAndFooterHidden.value ? withTiming(0) : withTiming(1),
transform: [
{
translateY: headerAndFooterHidden.value ? withTiming(100, translateYConfig) : withTiming(0, translateYConfig),
},
],
position: 'absolute',
bottom: bottomInset,
width: '100%',
zIndex: 1,
}), [bottomInset]);
const hideHeaderAndFooter = useCallback((hidden?: boolean) => {
'worklet';
if (hidden == null) {
// if we don't pass hidden, then we toggle the current value
headerAndFooterHidden.value = !headerAndFooterHidden.value;
return;
}
if (headerAndFooterHidden.value === hidden) {
return;
}
headerAndFooterHidden.value = hidden;
}, [headerAndFooterHidden]);
return {
headerAndFooterHidden,
headerStyles,
footerStyles,
hideHeaderAndFooter,
};
}
export function useGalleryItem(
identifier: string,
index: number,
onPress: (identifier: string, itemIndex: number) => void,
) {
const gallery = useGallery(identifier);
const ref = useAnimatedRef<any>();
const {opacity, activeIndex} = gallery.sharedValues;
const styles = useAnimatedStyle(() => {
return {
opacity: activeIndex.value === index ? opacity.value : 1,
};
}, []);
useDidMount(() => {
gallery.registerItem(index, ref);
});
const onGestureEvent = () => {
'worklet';
activeIndex.value = index;
scheduleOnRN(onPress, identifier, index);
};
return {
ref,
styles,
onGestureEvent,
};
}