Skip to content

Commit

Permalink
[NEW] Action Sheet (#2114)
Browse files Browse the repository at this point in the history
* [WIP] New Action Sheet

* [NEW] Header Indicator

* [IMPROVEMENT] Header Logic

* [NEW] Use EventEmitter to show ActionSheet for while

* [FIX] Animation

* [IMPROVEMENT] Use provider

* [FIX] Add callback

* [FIX] Message Actions

* [FIX] Add MessageActions icons

* [NEW] MessageErrorActions

* [IMPROVEMENT] OnClose

* [FIX] Adjust height

* [FIX] Close/Reopen

* [CHORE] Remove react-native-action-sheet

* [CHORE] Move ActionSheet

* [FIX] Reply Message

* [IMPROVEMENT] Hide ActionSheet logic

* [WIP] Custom MessageActions Header

* [IMPROVEMENT] MessageActions Header

* [IMPROVEMENT] Enable Scroll

* [FIX] Scroll on Android

* Move to react-native-scroll-bottom-sheet

* Stash

* Refactor actions

* Revert some changes

* Trying react-native-modalize

* Back to HOC

* ActionSheet style

* HOC Header

* Reaction actionSheet

* Fix messageBox actions

* Fix add reaction

* Change to flatListProps

* fix modalize android scroll

* Use react-native-scroll-bottom-sheet

* [NEW] BottomSheet dismissable & [FIX] Android not opening

* [NEW] Show emojis based on screen width

* [WIP] Adjust to content height

* [IMPROVEMENT] Responsible

* [IMPROVEMENT] Use alert instead actionSheet at NewServerView

* [FIX] Handle tablet cases

* [IMPROVEMENT] Remove actionSheet of RoomMembersView

* [IMPROVEMENT] Min snap distance when its portrait

* [CHORE] Remove unused Components

* [IMPROVEMENT] Remove duplicated add-reaction

* [IMPROVEMENT] Refactor Icon Package

* [IMPROVEMENT] Use new icons

* [FIX] Select message at MessageActions before getOptions

* [FIX] Custom header height

* [CHORE] Remove patch & [FIX] Tablet bottom sheet

* [FIX] Use ListItem height to BottomSheet Height

* Some fixes

* [FIX] Custom MessageActions header

* [FIX] Android height adjust

* [IMPROVEMENT] Item touchable & [FIX] Respect pin permission

* [IMPROVEMENT] More than one snap point

* some size fixes

* improve code

* hide horizontal scroll indicator

* [FIX] Focus MessageBox on edit message

* [FIX] Ripple color

* [IMPROVEMENT] Backdrop must keep same opacity after 50% of the screen

* [TEST] Change animation config

* [IMPROVEMENT] BackHandler should close the ActionSheet

* [CHORE] Add react-native-safe-area-context

* [FIX] Provide a bottom padding at notch devices

* [IMPROVEMENT] Improve backdrop input/output range

* [FIX] Weird Android Snap behavior

* [PATCH] React-native-scroll-bottom-sheet

* [CI] Re-run build

* [FIX] Landscape android

* [IMPROVEMENT] Cover 50% of the screen at the landscape mode

* [FIX] Adjust emoji content to width size

* [IMPROVEMENT] Use hooks library

* [IMPROVEMENT] Close the actionSheet when orientation change

* deactivate safe-area-context for while

* [REVERT] Re-add react-native-safe-area-context (3.0.2)

* [IMPROVEMENT] Use focused background

* [TESTS] E2E Tests updated to new BottomSheet

* [NEW] Add cancel button

* [FIX] Cancel button at android

* [IMPROVEMENT] Use cancelable bottom sheet at room members view

* [IMPROVEMENT] Use better function names

* [IMPROVEMENT] Use getItemLayout

* [FIX][TEMP] Animation

* Review

* Build

* Header keyExtractor

* Rename function

* Tweak animation

* Refactoring

* useTheme

* Refactoring

* TestIDs

* Refactor

* Remove old lib

Co-authored-by: Diego Mello <[email protected]>
  • Loading branch information
djorkaeffalexandre and diegolmello authored Jun 15, 2020
1 parent 98ed84b commit 893acdc
Show file tree
Hide file tree
Showing 26 changed files with 1,336 additions and 827 deletions.
97 changes: 50 additions & 47 deletions app/AppContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navi
import {
ROOT_LOADING, ROOT_OUTSIDE, ROOT_NEW_SERVER, ROOT_INSIDE, ROOT_SET_USERNAME, ROOT_BACKGROUND
} from './actions/app';
import { ActionSheetProvider } from './containers/ActionSheet';

// Stacks
import AuthLoadingView from './views/AuthLoadingView';
Expand Down Expand Up @@ -53,53 +54,55 @@ const App = React.memo(({ root, isMasterDetail }) => {

return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<NavigationContainer
theme={navTheme}
ref={Navigation.navigationRef}
onStateChange={(state) => {
const previousRouteName = Navigation.routeNameRef.current;
const currentRouteName = getActiveRouteName(state);
if (previousRouteName !== currentRouteName) {
setCurrentScreen(currentRouteName);
}
Navigation.routeNameRef.current = currentRouteName;
}}
>
<Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}>
<>
{root === ROOT_LOADING || root === ROOT_BACKGROUND ? (
<Stack.Screen
name='AuthLoading'
component={AuthLoadingView}
/>
) : null}
{root === ROOT_OUTSIDE || root === ROOT_NEW_SERVER ? (
<Stack.Screen
name='OutsideStack'
component={OutsideStack}
/>
) : null}
{root === ROOT_INSIDE && isMasterDetail ? (
<Stack.Screen
name='MasterDetailStack'
component={MasterDetailStack}
/>
) : null}
{root === ROOT_INSIDE && !isMasterDetail ? (
<Stack.Screen
name='InsideStack'
component={InsideStack}
/>
) : null}
{root === ROOT_SET_USERNAME ? (
<Stack.Screen
name='SetUsernameStack'
component={SetUsernameStack}
/>
) : null}
</>
</Stack.Navigator>
</NavigationContainer>
<ActionSheetProvider>
<NavigationContainer
theme={navTheme}
ref={Navigation.navigationRef}
onStateChange={(state) => {
const previousRouteName = Navigation.routeNameRef.current;
const currentRouteName = getActiveRouteName(state);
if (previousRouteName !== currentRouteName) {
setCurrentScreen(currentRouteName);
}
Navigation.routeNameRef.current = currentRouteName;
}}
>
<Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}>
<>
{root === ROOT_LOADING || root === ROOT_BACKGROUND ? (
<Stack.Screen
name='AuthLoading'
component={AuthLoadingView}
/>
) : null}
{root === ROOT_OUTSIDE || root === ROOT_NEW_SERVER ? (
<Stack.Screen
name='OutsideStack'
component={OutsideStack}
/>
) : null}
{root === ROOT_INSIDE && isMasterDetail ? (
<Stack.Screen
name='MasterDetailStack'
component={MasterDetailStack}
/>
) : null}
{root === ROOT_INSIDE && !isMasterDetail ? (
<Stack.Screen
name='InsideStack'
component={InsideStack}
/>
) : null}
{root === ROOT_SET_USERNAME ? (
<Stack.Screen
name='SetUsernameStack'
component={SetUsernameStack}
/>
) : null}
</>
</Stack.Navigator>
</NavigationContainer>
</ActionSheetProvider>
</SafeAreaProvider>
);
});
Expand Down
214 changes: 214 additions & 0 deletions app/containers/ActionSheet/ActionSheet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import React, {
useRef,
useState,
useEffect,
forwardRef,
useImperativeHandle,
useCallback,
isValidElement
} from 'react';
import PropTypes from 'prop-types';
import { Keyboard, Text } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { TapGestureHandler, State } from 'react-native-gesture-handler';
import ScrollBottomSheet from 'react-native-scroll-bottom-sheet';
import Animated, {
Extrapolate,
interpolate,
Value,
Easing
} from 'react-native-reanimated';
import * as Haptics from 'expo-haptics';
import {
useDimensions,
useBackHandler,
useDeviceOrientation
} from '@react-native-community/hooks';

import { Item } from './Item';
import { Handle } from './Handle';
import { Button } from './Button';
import { themes } from '../../constants/colors';
import styles, { ITEM_HEIGHT } from './styles';
import { isTablet, isIOS } from '../../utils/deviceInfo';
import Separator from '../Separator';
import I18n from '../../i18n';

const getItemLayout = (data, index) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index });

const HANDLE_HEIGHT = isIOS ? 40 : 56;
const MAX_SNAP_HEIGHT = 16;
const CANCEL_HEIGHT = 64;

const ANIMATION_DURATION = 250;

const ANIMATION_CONFIG = {
duration: ANIMATION_DURATION,
// https://easings.net/#easeInOutCubic
easing: Easing.bezier(0.645, 0.045, 0.355, 1.0)
};

const ActionSheet = React.memo(forwardRef(({ children, theme }, ref) => {
const bottomSheetRef = useRef();
const [data, setData] = useState({});
const [isVisible, setVisible] = useState(false);
const orientation = useDeviceOrientation();
const { height } = useDimensions().window;
const insets = useSafeAreaInsets();
const { landscape } = orientation;

const maxSnap = Math.max(
(
height
// Items height
- (ITEM_HEIGHT * (data?.options?.length || 0))
// Handle height
- HANDLE_HEIGHT
// Custom header height
- (data?.headerHeight || 0)
// Insets bottom height (Notch devices)
- insets.bottom
// Cancel button height
- (data?.hasCancel ? CANCEL_HEIGHT : 0)
),
MAX_SNAP_HEIGHT
);

/*
* if the action sheet cover more
* than 60% of the whole screen
* and it's not at the landscape mode
* we'll provide more one snap
* that point 50% of the whole screen
*/
const snaps = (height - maxSnap > height * 0.6) && !landscape ? [maxSnap, height * 0.5, height] : [maxSnap, height];
const openedSnapIndex = snaps.length > 2 ? 1 : 0;
const closedSnapIndex = snaps.length - 1;

const toggleVisible = () => setVisible(!isVisible);

const hide = () => {
bottomSheetRef.current?.snapTo(closedSnapIndex);
};

const show = (options) => {
setData(options);
toggleVisible();
};

const onBackdropPressed = ({ nativeEvent }) => {
if (nativeEvent.oldState === State.ACTIVE) {
hide();
}
};

useBackHandler(() => {
if (isVisible) {
hide();
}
return isVisible;
});

useEffect(() => {
if (isVisible) {
Keyboard.dismiss();
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
bottomSheetRef.current?.snapTo(openedSnapIndex);
}
}, [isVisible]);

// Hides action sheet when orientation changes
useEffect(() => {
setVisible(false);
}, [orientation.landscape]);

useImperativeHandle(ref, () => ({
showActionSheet: show,
hideActionSheet: hide
}));

const renderHandle = useCallback(() => (
<>
<Handle theme={theme} />
{isValidElement(data?.customHeader) ? data.customHeader : null}
</>
));

const renderFooter = useCallback(() => (data?.hasCancel ? (
<Button
onPress={hide}
style={[styles.button, { backgroundColor: themes[theme].auxiliaryBackground }]}
theme={theme}
>
<Text style={[styles.text, { color: themes[theme].bodyText }]}>
{I18n.t('Cancel')}
</Text>
</Button>
) : null));

const renderSeparator = useCallback(() => <Separator theme={theme} style={styles.separator} />);

const renderItem = useCallback(({ item }) => <Item item={item} hide={hide} theme={theme} />);

const animatedPosition = React.useRef(new Value(0));
const opacity = interpolate(animatedPosition.current, {
inputRange: [0, 1],
outputRange: [0, 0.7],
extrapolate: Extrapolate.CLAMP
});

return (
<>
{children}
{isVisible && (
<>
<TapGestureHandler onHandlerStateChange={onBackdropPressed}>
<Animated.View
testID='action-sheet-backdrop'
style={[
styles.backdrop,
{
backgroundColor: themes[theme].backdropColor,
opacity
}
]}
/>
</TapGestureHandler>
<ScrollBottomSheet
testID='action-sheet'
ref={bottomSheetRef}
componentType='FlatList'
snapPoints={snaps}
initialSnapIndex={closedSnapIndex}
renderHandle={renderHandle}
onSettle={index => (index === closedSnapIndex) && toggleVisible()}
animatedPosition={animatedPosition.current}
containerStyle={[
styles.container,
{ backgroundColor: themes[theme].focusedBackground },
(landscape || isTablet) && styles.bottomSheet
]}
animationConfig={ANIMATION_CONFIG}
// FlatList props
data={data?.options}
renderItem={renderItem}
keyExtractor={item => item.title}
style={{ backgroundColor: themes[theme].focusedBackground }}
contentContainerStyle={styles.content}
ItemSeparatorComponent={renderSeparator}
ListHeaderComponent={renderSeparator}
ListFooterComponent={renderFooter}
getItemLayout={getItemLayout}
removeClippedSubviews={isIOS}
/>
</>
)}
</>
);
}));
ActionSheet.propTypes = {
children: PropTypes.node,
theme: PropTypes.string
};

export default ActionSheet;
7 changes: 7 additions & 0 deletions app/containers/ActionSheet/Button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { TouchableOpacity } from 'react-native';

import { isAndroid } from '../../utils/deviceInfo';
import Touch from '../../utils/touch';

// Taken from https://github.com/rgommezz/react-native-scroll-bottom-sheet#touchables
export const Button = isAndroid ? Touch : TouchableOpacity;
15 changes: 15 additions & 0 deletions app/containers/ActionSheet/Handle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';

import styles from './styles';
import { themes } from '../../constants/colors';

export const Handle = React.memo(({ theme }) => (
<View style={[styles.handle, { backgroundColor: themes[theme].focusedBackground }]} testID='action-sheet-handle'>
<View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} />
</View>
));
Handle.propTypes = {
theme: PropTypes.string
};
41 changes: 41 additions & 0 deletions app/containers/ActionSheet/Item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text } from 'react-native';

import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
import { Button } from './Button';

export const Item = React.memo(({ item, hide, theme }) => {
const onPress = () => {
hide();
item?.onPress();
};

return (
<Button
onPress={onPress}
style={[styles.item, { backgroundColor: themes[theme].focusedBackground }]}
theme={theme}
>
<CustomIcon name={item.icon} size={20} color={item.danger ? themes[theme].dangerColor : themes[theme].bodyText} />
<Text
numberOfLines={1}
style={[styles.title, { color: item.danger ? themes[theme].dangerColor : themes[theme].bodyText }]}
>
{item.title}
</Text>
</Button>
);
});
Item.propTypes = {
item: PropTypes.shape({
title: PropTypes.string,
icon: PropTypes.string,
danger: PropTypes.bool,
onPress: PropTypes.func
}),
hide: PropTypes.func,
theme: PropTypes.string
};
Loading

0 comments on commit 893acdc

Please sign in to comment.