mattermost-mobile-upstream/app/components/option_box/animated.tsx
Elias Nahum b6fc85385e
Migrate from RNN to Expo router (#9402)
* Expo-Router partial migration

* Full migration to ExpoRouter

* i18n

* add tests

* Merge branch 'main' into expo-router

* align package-lock.json

* i18n

* feedback review

* feedback review

* android 16kb patch

* Refactored emoji picker keyboard using a state machine

* set android launch_screen in drawables instead of layout

* no conditional hooks

* remove duplicate call in test

* address feedback

* fix tests and address more feedback

* small corrections

* small corrections

* small corrections

* fix draft test

* fix draft test 2

* remove navigateToScreen in draff test

* fix nit in draft test

* remove doc file

* Feedback review

* fix snapshots

* improve test coverage

* fix i18n

* Upgrade t o RN 0.83, other dependencies and new architecture (#9737)

* library hardware-keyboard android new arch support fix

* library rnshare android new arch support fix

* library rnutils android new arch support fix

* library secure-pdf-viewer add new arch support

* Enable new arch on iOS and Android

* update emm and network client with new-arch

* update intune to use new-arch

* performance improvements

* fix tests

* fixes post input on new-arch

* fix android emoji picker

* remove doc

* run tests in parallel

* upgrade to React Native 0.83.9, Expo 55, React 19, and New Architecture

Bumps core dependencies to their latest versions:
- React Native 0.77.3 → 0.83.9 (New Architecture enabled)
- React 18.3.1 → 19.2.5
- Expo SDK 52 → 55
- TypeScript 5.8.3 → 6.0.3
- Node.js 22.16.0 → 24.15.0
- react-native-reanimated 3.17.3 → 4.3.0
- Various other dependency upgrades

Migrates iOS AppDelegate from Objective-C to Swift, updates Android
build tooling and Gradle wrapper, refreshes all patches and font
assets, and adapts app code, tests, and CI workflows throughout for
API changes introduced by these upgrades.

* remove unecessary dismissKeyboard in notifications

* feedback review

* update intune to use new-arch

* update @mattermost deps

* missing i18n string

* additional fixes

* update fastlane
2026-05-13 17:44:55 +08:00

163 lines
5.4 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useState} from 'react';
import {Pressable, StyleSheet, Text, View} from 'react-native';
import Animated, {Easing, interpolate, interpolateColor, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import {scheduleOnRN} from 'react-native-worklets';
import CompassIcon, {type CompassIconName} from '@components/compass_icon';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {OPTIONS_HEIGHT} from '.';
type OptionBoxProps = {
animatedBackgroundColor: string;
animatedColor: string;
animatedIconName: CompassIconName;
animatedText: string;
iconName: CompassIconName;
onAnimationEnd?: () => void;
onPress: () => void;
testID?: string;
text: string;
}
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
borderRadius: 4,
flex: 1,
maxHeight: OPTIONS_HEIGHT,
minWidth: 60,
},
background: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
},
center: {
alignItems: 'center',
justifyContent: 'center',
},
text: {
color: changeOpacity(theme.centerChannelColor, 0.56),
paddingHorizontal: 5,
textTransform: 'capitalize',
...typography('Body', 50, 'SemiBold'),
},
}));
const AnimatedOptionBox = ({
animatedBackgroundColor, animatedColor, animatedIconName, animatedText,
iconName, onAnimationEnd, onPress, testID, text,
}: OptionBoxProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const [activated, setActivated] = useState(false);
const animate = useSharedValue(0);
const handleOnPress = useCallback(() => {
animate.value = withTiming(1, {duration: 150, easing: Easing.out(Easing.linear)});
setActivated(true);
onPress();
}, [animate, onPress]);
const backgroundStyle = useAnimatedStyle(() => ({
backgroundColor: interpolateColor(
animate.value,
[0, 1],
['transparent', animatedBackgroundColor],
),
}));
const optionStyle = useAnimatedStyle(() => ({
opacity: interpolate(
animate.value,
[0, 1],
[1, 0],
),
}));
const scaleStyle = useAnimatedStyle(() => ({
transform: [{
scale: interpolate(
animate.value,
[0, 1],
[0.25, 1],
),
}],
opacity: animate.value,
}), [activated]);
useEffect(() => {
let t: NodeJS.Timeout|undefined;
const callback = () => {
'worklet';
if (onAnimationEnd) {
scheduleOnRN(onAnimationEnd);
}
};
if (activated) {
t = setTimeout(() => {
setActivated(false);
animate.value = withTiming(0, {duration: 150, easing: Easing.out(Easing.linear)}, callback);
}, 1200);
}
return () => {
if (t) {
clearTimeout(t);
}
};
}, [activated, animate, onAnimationEnd]);
return (
<AnimatedPressable
onPress={handleOnPress}
disabled={activated}
style={styles.container}
testID={testID}
>
{({pressed}) => (
<View style={[styles.container, styles.background, pressed && {backgroundColor: changeOpacity(theme.buttonBg, 0.16)}]}>
<Animated.View style={[styles.container, backgroundStyle]}>
<Animated.View style={[StyleSheet.absoluteFill, styles.center, optionStyle]}>
<CompassIcon
color={pressed ? theme.buttonBg : changeOpacity(theme.centerChannelColor, 0.56)}
name={iconName}
size={24}
/>
<Text
numberOfLines={1}
style={[styles.text, {color: pressed ? theme.buttonBg : changeOpacity(theme.centerChannelColor, 0.56)}]}
testID={`${testID}.label`}
>
{text}
</Text>
</Animated.View>
<Animated.View style={[StyleSheet.absoluteFill, styles.center, scaleStyle]}>
<CompassIcon
color={animatedColor}
name={animatedIconName}
size={24}
/>
<Text
numberOfLines={1}
style={[styles.text, {color: animatedColor}]}
testID={`${testID}.animated`}
>
{animatedText}
</Text>
</Animated.View>
</Animated.View>
</View>
)}
</AnimatedPressable>
);
};
export default AnimatedOptionBox;