[NEW] Action Sheet (#2114)
* [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 <diegolmello@gmail.com>
This commit is contained in:
parent
98ed84ba5c
commit
893acdcd3a
|
@ -10,6 +10,7 @@ import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navi
|
||||||
import {
|
import {
|
||||||
ROOT_LOADING, ROOT_OUTSIDE, ROOT_NEW_SERVER, ROOT_INSIDE, ROOT_SET_USERNAME, ROOT_BACKGROUND
|
ROOT_LOADING, ROOT_OUTSIDE, ROOT_NEW_SERVER, ROOT_INSIDE, ROOT_SET_USERNAME, ROOT_BACKGROUND
|
||||||
} from './actions/app';
|
} from './actions/app';
|
||||||
|
import { ActionSheetProvider } from './containers/ActionSheet';
|
||||||
|
|
||||||
// Stacks
|
// Stacks
|
||||||
import AuthLoadingView from './views/AuthLoadingView';
|
import AuthLoadingView from './views/AuthLoadingView';
|
||||||
|
@ -53,53 +54,55 @@ const App = React.memo(({ root, isMasterDetail }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
||||||
<NavigationContainer
|
<ActionSheetProvider>
|
||||||
theme={navTheme}
|
<NavigationContainer
|
||||||
ref={Navigation.navigationRef}
|
theme={navTheme}
|
||||||
onStateChange={(state) => {
|
ref={Navigation.navigationRef}
|
||||||
const previousRouteName = Navigation.routeNameRef.current;
|
onStateChange={(state) => {
|
||||||
const currentRouteName = getActiveRouteName(state);
|
const previousRouteName = Navigation.routeNameRef.current;
|
||||||
if (previousRouteName !== currentRouteName) {
|
const currentRouteName = getActiveRouteName(state);
|
||||||
setCurrentScreen(currentRouteName);
|
if (previousRouteName !== currentRouteName) {
|
||||||
}
|
setCurrentScreen(currentRouteName);
|
||||||
Navigation.routeNameRef.current = currentRouteName;
|
}
|
||||||
}}
|
Navigation.routeNameRef.current = currentRouteName;
|
||||||
>
|
}}
|
||||||
<Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}>
|
>
|
||||||
<>
|
<Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}>
|
||||||
{root === ROOT_LOADING || root === ROOT_BACKGROUND ? (
|
<>
|
||||||
<Stack.Screen
|
{root === ROOT_LOADING || root === ROOT_BACKGROUND ? (
|
||||||
name='AuthLoading'
|
<Stack.Screen
|
||||||
component={AuthLoadingView}
|
name='AuthLoading'
|
||||||
/>
|
component={AuthLoadingView}
|
||||||
) : null}
|
/>
|
||||||
{root === ROOT_OUTSIDE || root === ROOT_NEW_SERVER ? (
|
) : null}
|
||||||
<Stack.Screen
|
{root === ROOT_OUTSIDE || root === ROOT_NEW_SERVER ? (
|
||||||
name='OutsideStack'
|
<Stack.Screen
|
||||||
component={OutsideStack}
|
name='OutsideStack'
|
||||||
/>
|
component={OutsideStack}
|
||||||
) : null}
|
/>
|
||||||
{root === ROOT_INSIDE && isMasterDetail ? (
|
) : null}
|
||||||
<Stack.Screen
|
{root === ROOT_INSIDE && isMasterDetail ? (
|
||||||
name='MasterDetailStack'
|
<Stack.Screen
|
||||||
component={MasterDetailStack}
|
name='MasterDetailStack'
|
||||||
/>
|
component={MasterDetailStack}
|
||||||
) : null}
|
/>
|
||||||
{root === ROOT_INSIDE && !isMasterDetail ? (
|
) : null}
|
||||||
<Stack.Screen
|
{root === ROOT_INSIDE && !isMasterDetail ? (
|
||||||
name='InsideStack'
|
<Stack.Screen
|
||||||
component={InsideStack}
|
name='InsideStack'
|
||||||
/>
|
component={InsideStack}
|
||||||
) : null}
|
/>
|
||||||
{root === ROOT_SET_USERNAME ? (
|
) : null}
|
||||||
<Stack.Screen
|
{root === ROOT_SET_USERNAME ? (
|
||||||
name='SetUsernameStack'
|
<Stack.Screen
|
||||||
component={SetUsernameStack}
|
name='SetUsernameStack'
|
||||||
/>
|
component={SetUsernameStack}
|
||||||
) : null}
|
/>
|
||||||
</>
|
) : null}
|
||||||
</Stack.Navigator>
|
</>
|
||||||
</NavigationContainer>
|
</Stack.Navigator>
|
||||||
|
</NavigationContainer>
|
||||||
|
</ActionSheetProvider>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
||||||
|
};
|
|
@ -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
|
||||||
|
};
|
|
@ -0,0 +1,50 @@
|
||||||
|
import React, { useRef, useContext } from 'react';
|
||||||
|
import hoistNonReactStatics from 'hoist-non-react-statics';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import ActionSheet from './ActionSheet';
|
||||||
|
import { useTheme } from '../../theme';
|
||||||
|
|
||||||
|
const context = React.createContext({
|
||||||
|
showActionSheet: () => {},
|
||||||
|
hideActionSheet: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useActionSheet = () => useContext(context);
|
||||||
|
|
||||||
|
const { Provider, Consumer } = context;
|
||||||
|
|
||||||
|
export const withActionSheet = (Component) => {
|
||||||
|
const ConnectedActionSheet = props => (
|
||||||
|
<Consumer>
|
||||||
|
{contexts => <Component {...props} {...contexts} />}
|
||||||
|
</Consumer>
|
||||||
|
);
|
||||||
|
hoistNonReactStatics(ConnectedActionSheet, Component);
|
||||||
|
return ConnectedActionSheet;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActionSheetProvider = React.memo(({ children }) => {
|
||||||
|
const ref = useRef();
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
|
const getContext = () => ({
|
||||||
|
showActionSheet: (options) => {
|
||||||
|
ref.current?.showActionSheet(options);
|
||||||
|
},
|
||||||
|
hideActionSheet: () => {
|
||||||
|
ref.current?.hideActionSheet();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Provider value={getContext()}>
|
||||||
|
<ActionSheet ref={ref} theme={theme}>
|
||||||
|
{children}
|
||||||
|
</ActionSheet>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ActionSheetProvider.propTypes = {
|
||||||
|
children: PropTypes.node
|
||||||
|
};
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './Provider';
|
||||||
|
export * from './Button';
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
|
||||||
|
import sharedStyles from '../../views/Styles';
|
||||||
|
|
||||||
|
export const ITEM_HEIGHT = 48;
|
||||||
|
|
||||||
|
export default StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderTopLeftRadius: 16,
|
||||||
|
borderTopRightRadius: 16
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
height: ITEM_HEIGHT,
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'row'
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
marginHorizontal: 16
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
paddingTop: 16
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 16,
|
||||||
|
marginLeft: 16,
|
||||||
|
...sharedStyles.textRegular
|
||||||
|
},
|
||||||
|
handle: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
handleIndicator: {
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
borderRadius: 4,
|
||||||
|
margin: 8
|
||||||
|
},
|
||||||
|
backdrop: {
|
||||||
|
...StyleSheet.absoluteFillObject
|
||||||
|
},
|
||||||
|
bottomSheet: {
|
||||||
|
width: '50%',
|
||||||
|
alignSelf: 'center',
|
||||||
|
left: '25%'
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginHorizontal: 16,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: ITEM_HEIGHT,
|
||||||
|
borderRadius: 2,
|
||||||
|
marginBottom: 12
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
...sharedStyles.textMedium
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,467 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Alert, Clipboard, Share } from 'react-native';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import ActionSheet from 'react-native-action-sheet';
|
|
||||||
import moment from 'moment';
|
|
||||||
import * as Haptics from 'expo-haptics';
|
|
||||||
|
|
||||||
import RocketChat from '../lib/rocketchat';
|
|
||||||
import database from '../lib/database';
|
|
||||||
import I18n from '../i18n';
|
|
||||||
import log from '../utils/log';
|
|
||||||
import Navigation from '../lib/Navigation';
|
|
||||||
import { getMessageTranslation } from './message/utils';
|
|
||||||
import { LISTENER } from './Toast';
|
|
||||||
import EventEmitter from '../utils/events';
|
|
||||||
import { showConfirmationAlert } from '../utils/info';
|
|
||||||
|
|
||||||
class MessageActions extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
actionsHide: PropTypes.func.isRequired,
|
|
||||||
room: PropTypes.object.isRequired,
|
|
||||||
message: PropTypes.object,
|
|
||||||
user: PropTypes.object,
|
|
||||||
editInit: PropTypes.func.isRequired,
|
|
||||||
reactionInit: PropTypes.func.isRequired,
|
|
||||||
replyInit: PropTypes.func.isRequired,
|
|
||||||
isReadOnly: PropTypes.bool,
|
|
||||||
Message_AllowDeleting: PropTypes.bool,
|
|
||||||
Message_AllowDeleting_BlockDeleteInMinutes: PropTypes.number,
|
|
||||||
Message_AllowEditing: PropTypes.bool,
|
|
||||||
Message_AllowEditing_BlockEditInMinutes: PropTypes.number,
|
|
||||||
Message_AllowPinning: PropTypes.bool,
|
|
||||||
Message_AllowStarring: PropTypes.bool,
|
|
||||||
Message_Read_Receipt_Store_Users: PropTypes.bool,
|
|
||||||
isMasterDetail: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.handleActionPress = this.handleActionPress.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
await this.setPermissions();
|
|
||||||
|
|
||||||
const {
|
|
||||||
Message_AllowStarring, Message_AllowPinning, Message_Read_Receipt_Store_Users, user, room, message, isReadOnly
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
// Cancel
|
|
||||||
this.options = [I18n.t('Cancel')];
|
|
||||||
this.CANCEL_INDEX = 0;
|
|
||||||
|
|
||||||
// Reply
|
|
||||||
if (!isReadOnly) {
|
|
||||||
this.options.push(I18n.t('Reply'));
|
|
||||||
this.REPLY_INDEX = this.options.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edit
|
|
||||||
if (this.allowEdit(this.props)) {
|
|
||||||
this.options.push(I18n.t('Edit'));
|
|
||||||
this.EDIT_INDEX = this.options.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Discussion
|
|
||||||
this.options.push(I18n.t('Create_Discussion'));
|
|
||||||
this.CREATE_DISCUSSION_INDEX = this.options.length - 1;
|
|
||||||
|
|
||||||
// Mark as unread
|
|
||||||
if (message.u && message.u._id !== user.id) {
|
|
||||||
this.options.push(I18n.t('Mark_unread'));
|
|
||||||
this.UNREAD_INDEX = this.options.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permalink
|
|
||||||
this.options.push(I18n.t('Permalink'));
|
|
||||||
this.PERMALINK_INDEX = this.options.length - 1;
|
|
||||||
|
|
||||||
// Copy
|
|
||||||
this.options.push(I18n.t('Copy'));
|
|
||||||
this.COPY_INDEX = this.options.length - 1;
|
|
||||||
|
|
||||||
// Share
|
|
||||||
this.options.push(I18n.t('Share'));
|
|
||||||
this.SHARE_INDEX = this.options.length - 1;
|
|
||||||
|
|
||||||
// Quote
|
|
||||||
if (!isReadOnly) {
|
|
||||||
this.options.push(I18n.t('Quote'));
|
|
||||||
this.QUOTE_INDEX = this.options.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Star
|
|
||||||
if (Message_AllowStarring) {
|
|
||||||
this.options.push(I18n.t(message.starred ? 'Unstar' : 'Star'));
|
|
||||||
this.STAR_INDEX = this.options.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pin
|
|
||||||
if (Message_AllowPinning) {
|
|
||||||
this.options.push(I18n.t(message.pinned ? 'Unpin' : 'Pin'));
|
|
||||||
this.PIN_INDEX = this.options.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reaction
|
|
||||||
if (!isReadOnly || this.canReactWhenReadOnly()) {
|
|
||||||
this.options.push(I18n.t('Add_Reaction'));
|
|
||||||
this.REACTION_INDEX = this.options.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read Receipts
|
|
||||||
if (Message_Read_Receipt_Store_Users) {
|
|
||||||
this.options.push(I18n.t('Read_Receipt'));
|
|
||||||
this.READ_RECEIPT_INDEX = this.options.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle Auto-translate
|
|
||||||
if (room.autoTranslate && message.u && message.u._id !== user.id) {
|
|
||||||
this.options.push(I18n.t(message.autoTranslate ? 'View_Original' : 'Translate'));
|
|
||||||
this.TOGGLE_TRANSLATION_INDEX = this.options.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report
|
|
||||||
this.options.push(I18n.t('Report'));
|
|
||||||
this.REPORT_INDEX = this.options.length - 1;
|
|
||||||
|
|
||||||
// Delete
|
|
||||||
if (this.allowDelete(this.props)) {
|
|
||||||
this.options.push(I18n.t('Delete'));
|
|
||||||
this.DELETE_INDEX = this.options.length - 1;
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
this.showActionSheet();
|
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async setPermissions() {
|
|
||||||
try {
|
|
||||||
const { room } = this.props;
|
|
||||||
const permissions = ['edit-message', 'delete-message', 'force-delete-message'];
|
|
||||||
const result = await RocketChat.hasPermission(permissions, room.rid);
|
|
||||||
this.hasEditPermission = result[permissions[0]];
|
|
||||||
this.hasDeletePermission = result[permissions[1]];
|
|
||||||
this.hasForceDeletePermission = result[permissions[2]];
|
|
||||||
} catch (e) {
|
|
||||||
log(e);
|
|
||||||
}
|
|
||||||
Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
showActionSheet = () => {
|
|
||||||
ActionSheet.showActionSheetWithOptions({
|
|
||||||
options: this.options,
|
|
||||||
cancelButtonIndex: this.CANCEL_INDEX,
|
|
||||||
destructiveButtonIndex: this.DELETE_INDEX,
|
|
||||||
title: I18n.t('Message_actions')
|
|
||||||
}, (actionIndex) => {
|
|
||||||
this.handleActionPress(actionIndex);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getPermalink = async(message) => {
|
|
||||||
try {
|
|
||||||
return await RocketChat.getPermalinkMessage(message);
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isOwn = props => props.message.u && props.message.u._id === props.user.id;
|
|
||||||
|
|
||||||
canReactWhenReadOnly = () => {
|
|
||||||
const { room } = this.props;
|
|
||||||
return room.reactWhenReadOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
allowEdit = (props) => {
|
|
||||||
if (props.isReadOnly) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const editOwn = this.isOwn(props);
|
|
||||||
const { Message_AllowEditing: isEditAllowed, Message_AllowEditing_BlockEditInMinutes } = this.props;
|
|
||||||
|
|
||||||
if (!(this.hasEditPermission || (isEditAllowed && editOwn))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const blockEditInMinutes = Message_AllowEditing_BlockEditInMinutes;
|
|
||||||
if (blockEditInMinutes) {
|
|
||||||
let msgTs;
|
|
||||||
if (props.message.ts != null) {
|
|
||||||
msgTs = moment(props.message.ts);
|
|
||||||
}
|
|
||||||
let currentTsDiff;
|
|
||||||
if (msgTs != null) {
|
|
||||||
currentTsDiff = moment().diff(msgTs, 'minutes');
|
|
||||||
}
|
|
||||||
return currentTsDiff < blockEditInMinutes;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
allowDelete = (props) => {
|
|
||||||
if (props.isReadOnly) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent from deleting thread start message when positioned inside the thread
|
|
||||||
if (props.tmid && props.tmid === props.message.id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const deleteOwn = this.isOwn(props);
|
|
||||||
const { Message_AllowDeleting: isDeleteAllowed, Message_AllowDeleting_BlockDeleteInMinutes } = this.props;
|
|
||||||
if (!(this.hasDeletePermission || (isDeleteAllowed && deleteOwn) || this.hasForceDeletePermission)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (this.hasForceDeletePermission) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const blockDeleteInMinutes = Message_AllowDeleting_BlockDeleteInMinutes;
|
|
||||||
if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) {
|
|
||||||
let msgTs;
|
|
||||||
if (props.message.ts != null) {
|
|
||||||
msgTs = moment(props.message.ts);
|
|
||||||
}
|
|
||||||
let currentTsDiff;
|
|
||||||
if (msgTs != null) {
|
|
||||||
currentTsDiff = moment().diff(msgTs, 'minutes');
|
|
||||||
}
|
|
||||||
return currentTsDiff < blockDeleteInMinutes;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDelete = () => {
|
|
||||||
showConfirmationAlert({
|
|
||||||
message: I18n.t('You_will_not_be_able_to_recover_this_message'),
|
|
||||||
callToAction: I18n.t('Delete'),
|
|
||||||
onPress: async() => {
|
|
||||||
const { message } = this.props;
|
|
||||||
try {
|
|
||||||
await RocketChat.deleteMessage(message.id, message.subscription.id);
|
|
||||||
} catch (e) {
|
|
||||||
log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEdit = () => {
|
|
||||||
const { message, editInit } = this.props;
|
|
||||||
editInit(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUnread = async() => {
|
|
||||||
const { message, room, isMasterDetail } = this.props;
|
|
||||||
const { id: messageId, ts } = message;
|
|
||||||
const { rid } = room;
|
|
||||||
try {
|
|
||||||
const db = database.active;
|
|
||||||
const result = await RocketChat.markAsUnread({ messageId });
|
|
||||||
if (result.success) {
|
|
||||||
const subCollection = db.collections.get('subscriptions');
|
|
||||||
const subRecord = await subCollection.find(rid);
|
|
||||||
await db.action(async() => {
|
|
||||||
try {
|
|
||||||
await subRecord.update(sub => sub.lastOpen = ts);
|
|
||||||
} catch {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (isMasterDetail) {
|
|
||||||
Navigation.replace('RoomView');
|
|
||||||
} else {
|
|
||||||
Navigation.navigate('RoomsListView');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCopy = async() => {
|
|
||||||
const { message } = this.props;
|
|
||||||
await Clipboard.setString(message.msg);
|
|
||||||
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleShare = async() => {
|
|
||||||
const { message } = this.props;
|
|
||||||
const permalink = await this.getPermalink(message);
|
|
||||||
if (!permalink) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Share.share({
|
|
||||||
message: permalink
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleStar = async() => {
|
|
||||||
const { message } = this.props;
|
|
||||||
try {
|
|
||||||
await RocketChat.toggleStarMessage(message.id, message.starred);
|
|
||||||
EventEmitter.emit(LISTENER, { message: message.starred ? I18n.t('Message_unstarred') : I18n.t('Message_starred') });
|
|
||||||
} catch (e) {
|
|
||||||
log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePermalink = async() => {
|
|
||||||
const { message } = this.props;
|
|
||||||
const permalink = await this.getPermalink(message);
|
|
||||||
Clipboard.setString(permalink);
|
|
||||||
EventEmitter.emit(LISTENER, { message: I18n.t('Permalink_copied_to_clipboard') });
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePin = async() => {
|
|
||||||
const { message } = this.props;
|
|
||||||
try {
|
|
||||||
await RocketChat.togglePinMessage(message.id, message.pinned);
|
|
||||||
} catch (e) {
|
|
||||||
log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleReply = () => {
|
|
||||||
const { message, replyInit } = this.props;
|
|
||||||
replyInit(message, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleQuote = () => {
|
|
||||||
const { message, replyInit } = this.props;
|
|
||||||
replyInit(message, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleReaction = () => {
|
|
||||||
const { message, reactionInit } = this.props;
|
|
||||||
reactionInit(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleReadReceipt = () => {
|
|
||||||
const { message } = this.props;
|
|
||||||
Navigation.navigate('ReadReceiptsView', { messageId: message.id });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleReport = async() => {
|
|
||||||
const { message } = this.props;
|
|
||||||
try {
|
|
||||||
await RocketChat.reportMessage(message.id);
|
|
||||||
Alert.alert(I18n.t('Message_Reported'));
|
|
||||||
} catch (e) {
|
|
||||||
log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleToggleTranslation = async() => {
|
|
||||||
const { message, room } = this.props;
|
|
||||||
try {
|
|
||||||
const db = database.active;
|
|
||||||
await db.action(async() => {
|
|
||||||
await message.update((m) => {
|
|
||||||
m.autoTranslate = !m.autoTranslate;
|
|
||||||
m._updatedAt = new Date();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const translatedMessage = getMessageTranslation(message, room.autoTranslateLanguage);
|
|
||||||
if (!translatedMessage) {
|
|
||||||
const m = {
|
|
||||||
_id: message.id,
|
|
||||||
rid: message.subscription.id,
|
|
||||||
u: message.u,
|
|
||||||
msg: message.msg
|
|
||||||
};
|
|
||||||
await RocketChat.translateMessage(m, room.autoTranslateLanguage);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCreateDiscussion = () => {
|
|
||||||
const { message, room: channel, isMasterDetail } = this.props;
|
|
||||||
const params = { message, channel, showCloseModal: true };
|
|
||||||
if (isMasterDetail) {
|
|
||||||
Navigation.navigate('ModalStackNavigator', { screen: 'CreateDiscussionView', params });
|
|
||||||
} else {
|
|
||||||
Navigation.navigate('NewMessageStackNavigator', { screen: 'CreateDiscussionView', params });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleActionPress = (actionIndex) => {
|
|
||||||
if (actionIndex) {
|
|
||||||
switch (actionIndex) {
|
|
||||||
case this.REPLY_INDEX:
|
|
||||||
this.handleReply();
|
|
||||||
break;
|
|
||||||
case this.EDIT_INDEX:
|
|
||||||
this.handleEdit();
|
|
||||||
break;
|
|
||||||
case this.UNREAD_INDEX:
|
|
||||||
this.handleUnread();
|
|
||||||
break;
|
|
||||||
case this.PERMALINK_INDEX:
|
|
||||||
this.handlePermalink();
|
|
||||||
break;
|
|
||||||
case this.COPY_INDEX:
|
|
||||||
this.handleCopy();
|
|
||||||
break;
|
|
||||||
case this.SHARE_INDEX:
|
|
||||||
this.handleShare();
|
|
||||||
break;
|
|
||||||
case this.QUOTE_INDEX:
|
|
||||||
this.handleQuote();
|
|
||||||
break;
|
|
||||||
case this.STAR_INDEX:
|
|
||||||
this.handleStar();
|
|
||||||
break;
|
|
||||||
case this.PIN_INDEX:
|
|
||||||
this.handlePin();
|
|
||||||
break;
|
|
||||||
case this.REACTION_INDEX:
|
|
||||||
this.handleReaction();
|
|
||||||
break;
|
|
||||||
case this.REPORT_INDEX:
|
|
||||||
this.handleReport();
|
|
||||||
break;
|
|
||||||
case this.DELETE_INDEX:
|
|
||||||
this.handleDelete();
|
|
||||||
break;
|
|
||||||
case this.READ_RECEIPT_INDEX:
|
|
||||||
this.handleReadReceipt();
|
|
||||||
break;
|
|
||||||
case this.CREATE_DISCUSSION_INDEX:
|
|
||||||
this.handleCreateDiscussion();
|
|
||||||
break;
|
|
||||||
case this.TOGGLE_TRANSLATION_INDEX:
|
|
||||||
this.handleToggleTranslation();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { actionsHide } = this.props;
|
|
||||||
actionsHide();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
Message_AllowDeleting: state.settings.Message_AllowDeleting,
|
|
||||||
Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes,
|
|
||||||
Message_AllowEditing: state.settings.Message_AllowEditing,
|
|
||||||
Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes,
|
|
||||||
Message_AllowPinning: state.settings.Message_AllowPinning,
|
|
||||||
Message_AllowStarring: state.settings.Message_AllowStarring,
|
|
||||||
Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users,
|
|
||||||
isMasterDetail: state.app.isMasterDetail
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(MessageActions);
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
View, Text, FlatList, StyleSheet, Dimensions
|
||||||
|
} from 'react-native';
|
||||||
|
|
||||||
|
import { withTheme } from '../../theme';
|
||||||
|
import { themes } from '../../constants/colors';
|
||||||
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
|
import shortnameToUnicode from '../../utils/shortnameToUnicode';
|
||||||
|
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||||
|
import database from '../../lib/database';
|
||||||
|
import { Button } from '../ActionSheet';
|
||||||
|
|
||||||
|
export const HEADER_HEIGHT = 36;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
alignItems: 'center',
|
||||||
|
marginHorizontal: 8
|
||||||
|
},
|
||||||
|
headerItem: {
|
||||||
|
height: 36,
|
||||||
|
width: 36,
|
||||||
|
borderRadius: 20,
|
||||||
|
marginHorizontal: 8,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
headerIcon: {
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: 20,
|
||||||
|
color: '#fff'
|
||||||
|
},
|
||||||
|
customEmoji: {
|
||||||
|
height: 20,
|
||||||
|
width: 20
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyExtractor = item => item?.id || item;
|
||||||
|
|
||||||
|
const DEFAULT_EMOJIS = ['clap', '+1', 'heart_eyes', 'grinning', 'thinking_face', 'smiley'];
|
||||||
|
|
||||||
|
const HeaderItem = React.memo(({
|
||||||
|
item, onReaction, server, theme
|
||||||
|
}) => (
|
||||||
|
<Button
|
||||||
|
testID={`message-actions-emoji-${ item.content || item }`}
|
||||||
|
onPress={() => onReaction({ emoji: `:${ item.content || item }:` })}
|
||||||
|
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
|
||||||
|
theme={theme}
|
||||||
|
>
|
||||||
|
{item?.isCustom ? (
|
||||||
|
<CustomEmoji style={styles.customEmoji} emoji={item} baseUrl={server} />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.headerIcon}>
|
||||||
|
{shortnameToUnicode(`:${ item.content || item }:`)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
));
|
||||||
|
HeaderItem.propTypes = {
|
||||||
|
item: PropTypes.string,
|
||||||
|
onReaction: PropTypes.func,
|
||||||
|
server: PropTypes.string,
|
||||||
|
theme: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
const HeaderFooter = React.memo(({ onReaction, theme }) => (
|
||||||
|
<Button
|
||||||
|
testID='add-reaction'
|
||||||
|
onPress={onReaction}
|
||||||
|
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
|
||||||
|
theme={theme}
|
||||||
|
>
|
||||||
|
<CustomIcon name='add-reaction' size={24} color={themes[theme].bodyText} />
|
||||||
|
</Button>
|
||||||
|
));
|
||||||
|
HeaderFooter.propTypes = {
|
||||||
|
onReaction: PropTypes.func,
|
||||||
|
theme: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
const Header = React.memo(({
|
||||||
|
handleReaction, server, message, theme
|
||||||
|
}) => {
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
|
||||||
|
const setEmojis = async() => {
|
||||||
|
try {
|
||||||
|
const db = database.active;
|
||||||
|
const freqEmojiCollection = db.collections.get('frequently_used_emojis');
|
||||||
|
let freqEmojis = await freqEmojiCollection.query().fetch();
|
||||||
|
|
||||||
|
const { width, height } = Dimensions.get('window');
|
||||||
|
const isLandscape = width > height;
|
||||||
|
const size = isLandscape ? width / 2 : width;
|
||||||
|
const quantity = (size / 50) - 1;
|
||||||
|
|
||||||
|
freqEmojis = freqEmojis.concat(DEFAULT_EMOJIS).slice(0, quantity);
|
||||||
|
setItems(freqEmojis);
|
||||||
|
} catch {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEmojis();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onReaction = ({ emoji }) => handleReaction(emoji, message);
|
||||||
|
|
||||||
|
const renderItem = useCallback(({ item }) => <HeaderItem item={item} onReaction={onReaction} server={server} theme={theme} />);
|
||||||
|
|
||||||
|
const renderFooter = useCallback(() => <HeaderFooter onReaction={onReaction} theme={theme} />);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}>
|
||||||
|
<FlatList
|
||||||
|
data={items}
|
||||||
|
renderItem={renderItem}
|
||||||
|
ListFooterComponent={renderFooter}
|
||||||
|
style={{ backgroundColor: themes[theme].focusedBackground }}
|
||||||
|
keyExtractor={keyExtractor}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
scrollEnabled={false}
|
||||||
|
horizontal
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Header.propTypes = {
|
||||||
|
handleReaction: PropTypes.func,
|
||||||
|
server: PropTypes.string,
|
||||||
|
message: PropTypes.object,
|
||||||
|
theme: PropTypes.string
|
||||||
|
};
|
||||||
|
export default withTheme(Header);
|
|
@ -0,0 +1,418 @@
|
||||||
|
import React, { forwardRef, useImperativeHandle } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Alert, Clipboard, Share } from 'react-native';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import RocketChat from '../../lib/rocketchat';
|
||||||
|
import database from '../../lib/database';
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
import log from '../../utils/log';
|
||||||
|
import Navigation from '../../lib/Navigation';
|
||||||
|
import { getMessageTranslation } from '../message/utils';
|
||||||
|
import { LISTENER } from '../Toast';
|
||||||
|
import EventEmitter from '../../utils/events';
|
||||||
|
import { showConfirmationAlert } from '../../utils/info';
|
||||||
|
import { useActionSheet } from '../ActionSheet';
|
||||||
|
import Header, { HEADER_HEIGHT } from './Header';
|
||||||
|
|
||||||
|
const MessageActions = React.memo(forwardRef(({
|
||||||
|
room,
|
||||||
|
tmid,
|
||||||
|
user,
|
||||||
|
editInit,
|
||||||
|
reactionInit,
|
||||||
|
onReactionPress,
|
||||||
|
replyInit,
|
||||||
|
isReadOnly,
|
||||||
|
server,
|
||||||
|
Message_AllowDeleting,
|
||||||
|
Message_AllowDeleting_BlockDeleteInMinutes,
|
||||||
|
Message_AllowEditing,
|
||||||
|
Message_AllowEditing_BlockEditInMinutes,
|
||||||
|
Message_AllowPinning,
|
||||||
|
Message_AllowStarring,
|
||||||
|
Message_Read_Receipt_Store_Users
|
||||||
|
}, ref) => {
|
||||||
|
let permissions = {};
|
||||||
|
const { showActionSheet, hideActionSheet } = useActionSheet();
|
||||||
|
|
||||||
|
const getPermissions = async() => {
|
||||||
|
try {
|
||||||
|
const permission = ['edit-message', 'delete-message', 'force-delete-message', 'pin-message'];
|
||||||
|
const result = await RocketChat.hasPermission(permission, room.rid);
|
||||||
|
permissions = {
|
||||||
|
hasEditPermission: result[permission[0]],
|
||||||
|
hasDeletePermission: result[permission[1]],
|
||||||
|
hasForceDeletePermission: result[permission[2]],
|
||||||
|
hasPinPermission: result[permission[3]]
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOwn = message => message.u && message.u._id === user.id;
|
||||||
|
|
||||||
|
const allowEdit = (message) => {
|
||||||
|
if (isReadOnly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const editOwn = isOwn(message);
|
||||||
|
|
||||||
|
if (!(permissions.hasEditPermission || (Message_AllowEditing && editOwn))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const blockEditInMinutes = Message_AllowEditing_BlockEditInMinutes;
|
||||||
|
if (blockEditInMinutes) {
|
||||||
|
let msgTs;
|
||||||
|
if (message.ts != null) {
|
||||||
|
msgTs = moment(message.ts);
|
||||||
|
}
|
||||||
|
let currentTsDiff;
|
||||||
|
if (msgTs != null) {
|
||||||
|
currentTsDiff = moment().diff(msgTs, 'minutes');
|
||||||
|
}
|
||||||
|
return currentTsDiff < blockEditInMinutes;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const allowDelete = (message) => {
|
||||||
|
if (isReadOnly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent from deleting thread start message when positioned inside the thread
|
||||||
|
if (tmid === message.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const deleteOwn = isOwn(message);
|
||||||
|
if (!(permissions.hasDeletePermission || (Message_AllowDeleting && deleteOwn) || permissions.hasForceDeletePermission)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (permissions.hasForceDeletePermission) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const blockDeleteInMinutes = Message_AllowDeleting_BlockDeleteInMinutes;
|
||||||
|
if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) {
|
||||||
|
let msgTs;
|
||||||
|
if (message.ts != null) {
|
||||||
|
msgTs = moment(message.ts);
|
||||||
|
}
|
||||||
|
let currentTsDiff;
|
||||||
|
if (msgTs != null) {
|
||||||
|
currentTsDiff = moment().diff(msgTs, 'minutes');
|
||||||
|
}
|
||||||
|
return currentTsDiff < blockDeleteInMinutes;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPermalink = message => RocketChat.getPermalinkMessage(message);
|
||||||
|
|
||||||
|
const handleReply = message => replyInit(message, true);
|
||||||
|
|
||||||
|
const handleEdit = message => editInit(message);
|
||||||
|
|
||||||
|
const handleCreateDiscussion = (message) => {
|
||||||
|
Navigation.navigate('CreateDiscussionView', { message, channel: room });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnread = async(message) => {
|
||||||
|
const { id: messageId, ts } = message;
|
||||||
|
const { rid } = room;
|
||||||
|
try {
|
||||||
|
const db = database.active;
|
||||||
|
const result = await RocketChat.markAsUnread({ messageId });
|
||||||
|
if (result.success) {
|
||||||
|
const subCollection = db.collections.get('subscriptions');
|
||||||
|
const subRecord = await subCollection.find(rid);
|
||||||
|
await db.action(async() => {
|
||||||
|
try {
|
||||||
|
await subRecord.update(sub => sub.lastOpen = ts);
|
||||||
|
} catch {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Navigation.navigate('RoomsListView');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePermalink = async(message) => {
|
||||||
|
try {
|
||||||
|
const permalink = await getPermalink(message);
|
||||||
|
Clipboard.setString(permalink);
|
||||||
|
EventEmitter.emit(LISTENER, { message: I18n.t('Permalink_copied_to_clipboard') });
|
||||||
|
} catch {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = async(message) => {
|
||||||
|
await Clipboard.setString(message.msg);
|
||||||
|
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShare = async(message) => {
|
||||||
|
try {
|
||||||
|
const permalink = await getPermalink(message);
|
||||||
|
Share.share({ message: permalink });
|
||||||
|
} catch {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuote = message => replyInit(message, false);
|
||||||
|
|
||||||
|
const handleStar = async(message) => {
|
||||||
|
try {
|
||||||
|
await RocketChat.toggleStarMessage(message.id, message.starred);
|
||||||
|
EventEmitter.emit(LISTENER, { message: message.starred ? I18n.t('Message_unstarred') : I18n.t('Message_starred') });
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePin = async(message) => {
|
||||||
|
try {
|
||||||
|
await RocketChat.togglePinMessage(message.id, message.pinned);
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReaction = (shortname, message) => {
|
||||||
|
if (shortname) {
|
||||||
|
onReactionPress(shortname, message.id);
|
||||||
|
} else {
|
||||||
|
reactionInit(message);
|
||||||
|
}
|
||||||
|
// close actionSheet when click at header
|
||||||
|
hideActionSheet();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReadReceipt = message => Navigation.navigate('ReadReceiptsView', { messageId: message.id });
|
||||||
|
|
||||||
|
const handleToggleTranslation = async(message) => {
|
||||||
|
try {
|
||||||
|
const db = database.active;
|
||||||
|
await db.action(async() => {
|
||||||
|
await message.update((m) => {
|
||||||
|
m.autoTranslate = !m.autoTranslate;
|
||||||
|
m._updatedAt = new Date();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const translatedMessage = getMessageTranslation(message, room.autoTranslateLanguage);
|
||||||
|
if (!translatedMessage) {
|
||||||
|
const m = {
|
||||||
|
_id: message.id,
|
||||||
|
rid: message.subscription.id,
|
||||||
|
u: message.u,
|
||||||
|
msg: message.msg
|
||||||
|
};
|
||||||
|
await RocketChat.translateMessage(m, room.autoTranslateLanguage);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReport = async(message) => {
|
||||||
|
try {
|
||||||
|
await RocketChat.reportMessage(message.id);
|
||||||
|
Alert.alert(I18n.t('Message_Reported'));
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (message) => {
|
||||||
|
showConfirmationAlert({
|
||||||
|
message: I18n.t('You_will_not_be_able_to_recover_this_message'),
|
||||||
|
callToAction: I18n.t('Delete'),
|
||||||
|
onPress: async() => {
|
||||||
|
try {
|
||||||
|
await RocketChat.deleteMessage(message.id, message.subscription.id);
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOptions = (message) => {
|
||||||
|
let options = [];
|
||||||
|
|
||||||
|
// Reply
|
||||||
|
if (!isReadOnly) {
|
||||||
|
options = [{
|
||||||
|
title: I18n.t('Reply_in_Thread'),
|
||||||
|
icon: 'threads',
|
||||||
|
onPress: () => handleReply(message)
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quote
|
||||||
|
if (!isReadOnly) {
|
||||||
|
options.push({
|
||||||
|
title: I18n.t('Quote'),
|
||||||
|
icon: 'quote',
|
||||||
|
onPress: () => handleQuote(message)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit
|
||||||
|
if (allowEdit(message)) {
|
||||||
|
options.push({
|
||||||
|
title: I18n.t('Edit'),
|
||||||
|
icon: 'edit',
|
||||||
|
onPress: () => handleEdit(message)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permalink
|
||||||
|
options.push({
|
||||||
|
title: I18n.t('Permalink'),
|
||||||
|
icon: 'link',
|
||||||
|
onPress: () => handlePermalink(message)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create Discussion
|
||||||
|
options.push({
|
||||||
|
title: I18n.t('Start_a_Discussion'),
|
||||||
|
icon: 'chat',
|
||||||
|
onPress: () => handleCreateDiscussion(message)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark as unread
|
||||||
|
if (message.u && message.u._id !== user.id) {
|
||||||
|
options.push({
|
||||||
|
title: I18n.t('Mark_unread'),
|
||||||
|
icon: 'flag',
|
||||||
|
onPress: () => handleUnread(message)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy
|
||||||
|
options.push({
|
||||||
|
title: I18n.t('Copy'),
|
||||||
|
icon: 'copy',
|
||||||
|
onPress: () => handleCopy(message)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Share
|
||||||
|
options.push({
|
||||||
|
title: I18n.t('Share'),
|
||||||
|
icon: 'share',
|
||||||
|
onPress: () => handleShare(message)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Star
|
||||||
|
if (Message_AllowStarring) {
|
||||||
|
options.push({
|
||||||
|
title: I18n.t(message.starred ? 'Unstar' : 'Star'),
|
||||||
|
icon: message.starred ? 'star-filled' : 'star',
|
||||||
|
onPress: () => handleStar(message)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pin
|
||||||
|
if (Message_AllowPinning && permissions?.hasPinPermission) {
|
||||||
|
options.push({
|
||||||
|
title: I18n.t(message.pinned ? 'Unpin' : 'Pin'),
|
||||||
|
icon: 'pin',
|
||||||
|
onPress: () => handlePin(message)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read Receipts
|
||||||
|
if (Message_Read_Receipt_Store_Users) {
|
||||||
|
options.push({
|
||||||
|
title: I18n.t('Read_Receipt'),
|
||||||
|
icon: 'receipt',
|
||||||
|
onPress: () => handleReadReceipt(message)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle Auto-translate
|
||||||
|
if (room.autoTranslate && message.u && message.u._id !== user.id) {
|
||||||
|
options.push({
|
||||||
|
title: I18n.t(message.autoTranslate ? 'View_Original' : 'Translate'),
|
||||||
|
icon: 'language',
|
||||||
|
onPress: () => handleToggleTranslation(message)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report
|
||||||
|
options.push({
|
||||||
|
title: I18n.t('Report'),
|
||||||
|
icon: 'warning',
|
||||||
|
danger: true,
|
||||||
|
onPress: () => handleReport(message)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
if (allowDelete(message)) {
|
||||||
|
options.push({
|
||||||
|
title: I18n.t('Delete'),
|
||||||
|
icon: 'trash',
|
||||||
|
danger: true,
|
||||||
|
onPress: () => handleDelete(message)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showMessageActions = async(message) => {
|
||||||
|
await getPermissions();
|
||||||
|
showActionSheet({
|
||||||
|
options: getOptions(message),
|
||||||
|
headerHeight: HEADER_HEIGHT,
|
||||||
|
customHeader: (!isReadOnly || room.reactWhenReadOnly ? (
|
||||||
|
<Header
|
||||||
|
server={server}
|
||||||
|
handleReaction={handleReaction}
|
||||||
|
message={message}
|
||||||
|
/>
|
||||||
|
) : null)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({ showMessageActions }));
|
||||||
|
}));
|
||||||
|
MessageActions.propTypes = {
|
||||||
|
room: PropTypes.object,
|
||||||
|
tmid: PropTypes.string,
|
||||||
|
user: PropTypes.object,
|
||||||
|
editInit: PropTypes.func,
|
||||||
|
reactionInit: PropTypes.func,
|
||||||
|
onReactionPress: PropTypes.func,
|
||||||
|
replyInit: PropTypes.func,
|
||||||
|
isReadOnly: PropTypes.bool,
|
||||||
|
Message_AllowDeleting: PropTypes.bool,
|
||||||
|
Message_AllowDeleting_BlockDeleteInMinutes: PropTypes.number,
|
||||||
|
Message_AllowEditing: PropTypes.bool,
|
||||||
|
Message_AllowEditing_BlockEditInMinutes: PropTypes.number,
|
||||||
|
Message_AllowPinning: PropTypes.bool,
|
||||||
|
Message_AllowStarring: PropTypes.bool,
|
||||||
|
Message_Read_Receipt_Store_Users: PropTypes.bool,
|
||||||
|
server: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
server: state.server.server,
|
||||||
|
Message_AllowDeleting: state.settings.Message_AllowDeleting,
|
||||||
|
Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes,
|
||||||
|
Message_AllowEditing: state.settings.Message_AllowEditing,
|
||||||
|
Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes,
|
||||||
|
Message_AllowPinning: state.settings.Message_AllowPinning,
|
||||||
|
Message_AllowStarring: state.settings.Message_AllowStarring,
|
||||||
|
Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, null, null, { forwardRef: true })(MessageActions);
|
|
@ -3,6 +3,7 @@ import { View, Text, StyleSheet } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
|
|
||||||
import Markdown from '../markdown';
|
import Markdown from '../markdown';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
|
@ -58,7 +59,7 @@ const ReplyPreview = React.memo(({
|
||||||
>
|
>
|
||||||
<View style={[styles.messageContainer, { backgroundColor: themes[theme].chatComponentBackground }]}>
|
<View style={[styles.messageContainer, { backgroundColor: themes[theme].chatComponentBackground }]}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={[styles.username, { color: themes[theme].tintColor }]}>{message.u.username}</Text>
|
<Text style={[styles.username, { color: themes[theme].tintColor }]}>{message.u?.username}</Text>
|
||||||
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Markdown
|
<Markdown
|
||||||
|
@ -74,7 +75,7 @@ const ReplyPreview = React.memo(({
|
||||||
<CustomIcon name='Cross' color={themes[theme].auxiliaryText} size={20} style={styles.close} onPress={close} />
|
<CustomIcon name='Cross' color={themes[theme].auxiliaryText} size={20} style={styles.close} onPress={close} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}, (prevProps, nextProps) => prevProps.replying === nextProps.replying && prevProps.theme === nextProps.theme);
|
}, (prevProps, nextProps) => prevProps.replying === nextProps.replying && prevProps.theme === nextProps.theme && isEqual(prevProps.message, nextProps.message));
|
||||||
|
|
||||||
ReplyPreview.propTypes = {
|
ReplyPreview.propTypes = {
|
||||||
replying: PropTypes.bool,
|
replying: PropTypes.bool,
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { KeyboardAccessoryView } from 'react-native-keyboard-input';
|
||||||
import ImagePicker from 'react-native-image-crop-picker';
|
import ImagePicker from 'react-native-image-crop-picker';
|
||||||
import equal from 'deep-equal';
|
import equal from 'deep-equal';
|
||||||
import DocumentPicker from 'react-native-document-picker';
|
import DocumentPicker from 'react-native-document-picker';
|
||||||
import ActionSheet from 'react-native-action-sheet';
|
|
||||||
import { Q } from '@nozbe/watermelondb';
|
import { Q } from '@nozbe/watermelondb';
|
||||||
|
|
||||||
import { generateTriggerId } from '../../lib/methods/actions';
|
import { generateTriggerId } from '../../lib/methods/actions';
|
||||||
|
@ -46,6 +45,7 @@ import CommandsPreview from './CommandsPreview';
|
||||||
import { Review } from '../../utils/review';
|
import { Review } from '../../utils/review';
|
||||||
import { getUserSelector } from '../../selectors/login';
|
import { getUserSelector } from '../../selectors/login';
|
||||||
import Navigation from '../../lib/Navigation';
|
import Navigation from '../../lib/Navigation';
|
||||||
|
import { withActionSheet } from '../ActionSheet';
|
||||||
|
|
||||||
const imagePickerConfig = {
|
const imagePickerConfig = {
|
||||||
cropping: true,
|
cropping: true,
|
||||||
|
@ -61,13 +61,6 @@ const videoPickerConfig = {
|
||||||
mediaType: 'video'
|
mediaType: 'video'
|
||||||
};
|
};
|
||||||
|
|
||||||
const FILE_CANCEL_INDEX = 0;
|
|
||||||
const FILE_PHOTO_INDEX = 1;
|
|
||||||
const FILE_VIDEO_INDEX = 2;
|
|
||||||
const FILE_LIBRARY_INDEX = 3;
|
|
||||||
const FILE_DOCUMENT_INDEX = 4;
|
|
||||||
const CREATE_DISCUSSION_INDEX = 5;
|
|
||||||
|
|
||||||
class MessageBox extends Component {
|
class MessageBox extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
rid: PropTypes.string.isRequired,
|
rid: PropTypes.string.isRequired,
|
||||||
|
@ -96,7 +89,8 @@ class MessageBox extends Component {
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
replyCancel: PropTypes.func,
|
replyCancel: PropTypes.func,
|
||||||
isMasterDetail: PropTypes.bool,
|
isMasterDetail: PropTypes.bool,
|
||||||
navigation: PropTypes.object
|
navigation: PropTypes.object,
|
||||||
|
showActionSheet: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -116,14 +110,36 @@ class MessageBox extends Component {
|
||||||
};
|
};
|
||||||
this.text = '';
|
this.text = '';
|
||||||
this.focused = false;
|
this.focused = false;
|
||||||
this.messageBoxActions = [
|
|
||||||
I18n.t('Cancel'),
|
// MessageBox Actions
|
||||||
I18n.t('Take_a_photo'),
|
this.options = [
|
||||||
I18n.t('Take_a_video'),
|
{
|
||||||
I18n.t('Choose_from_library'),
|
title: I18n.t('Take_a_photo'),
|
||||||
I18n.t('Choose_file'),
|
icon: 'image',
|
||||||
I18n.t('Create_Discussion')
|
onPress: this.takePhoto
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: I18n.t('Take_a_video'),
|
||||||
|
icon: 'video-1',
|
||||||
|
onPress: this.takeVideo
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: I18n.t('Choose_from_library'),
|
||||||
|
icon: 'share',
|
||||||
|
onPress: this.chooseFromLibrary
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: I18n.t('Choose_file'),
|
||||||
|
icon: 'folder',
|
||||||
|
onPress: this.chooseFile
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: I18n.t('Create_Discussion'),
|
||||||
|
icon: 'chat',
|
||||||
|
onPress: this.createDiscussion
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const libPickerLabels = {
|
const libPickerLabels = {
|
||||||
cropperChooseText: I18n.t('Choose'),
|
cropperChooseText: I18n.t('Choose'),
|
||||||
cropperCancelText: I18n.t('Cancel'),
|
cropperCancelText: I18n.t('Cancel'),
|
||||||
|
@ -204,6 +220,7 @@ class MessageBox extends Component {
|
||||||
if (this.text) {
|
if (this.text) {
|
||||||
this.setShowSend(true);
|
this.setShowSend(true);
|
||||||
}
|
}
|
||||||
|
this.focus();
|
||||||
} else if (replying !== nextProps.replying && nextProps.replying) {
|
} else if (replying !== nextProps.replying && nextProps.replying) {
|
||||||
this.focus();
|
this.focus();
|
||||||
} else if (!nextProps.message) {
|
} else if (!nextProps.message) {
|
||||||
|
@ -217,7 +234,7 @@ class MessageBox extends Component {
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
roomType, replying, editing, isFocused, theme
|
roomType, replying, editing, isFocused, message, theme
|
||||||
} = this.props;
|
} = this.props;
|
||||||
if (nextProps.theme !== theme) {
|
if (nextProps.theme !== theme) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -252,6 +269,9 @@ class MessageBox extends Component {
|
||||||
if (!equal(nextState.file, file)) {
|
if (!equal(nextState.file, file)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (!equal(nextProps.message, message)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -613,34 +633,8 @@ class MessageBox extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
showMessageBoxActions = () => {
|
showMessageBoxActions = () => {
|
||||||
ActionSheet.showActionSheetWithOptions({
|
const { showActionSheet } = this.props;
|
||||||
options: this.messageBoxActions,
|
showActionSheet({ options: this.options });
|
||||||
cancelButtonIndex: FILE_CANCEL_INDEX
|
|
||||||
}, (actionIndex) => {
|
|
||||||
this.handleMessageBoxActions(actionIndex);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMessageBoxActions = (actionIndex) => {
|
|
||||||
switch (actionIndex) {
|
|
||||||
case FILE_PHOTO_INDEX:
|
|
||||||
this.takePhoto();
|
|
||||||
break;
|
|
||||||
case FILE_VIDEO_INDEX:
|
|
||||||
this.takeVideo();
|
|
||||||
break;
|
|
||||||
case FILE_LIBRARY_INDEX:
|
|
||||||
this.chooseFromLibrary();
|
|
||||||
break;
|
|
||||||
case FILE_DOCUMENT_INDEX:
|
|
||||||
this.chooseFile();
|
|
||||||
break;
|
|
||||||
case CREATE_DISCUSSION_INDEX:
|
|
||||||
this.createDiscussion();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
editCancel = () => {
|
editCancel = () => {
|
||||||
|
@ -939,4 +933,4 @@ const dispatchToProps = ({
|
||||||
typing: (rid, status) => userTypingAction(rid, status)
|
typing: (rid, status) => userTypingAction(rid, status)
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(MessageBox);
|
export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(withActionSheet(MessageBox));
|
||||||
|
|
|
@ -1,41 +1,22 @@
|
||||||
import React from 'react';
|
import { useImperativeHandle, forwardRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ActionSheet from 'react-native-action-sheet';
|
|
||||||
|
|
||||||
import RocketChat from '../lib/rocketchat';
|
import RocketChat from '../lib/rocketchat';
|
||||||
import database from '../lib/database';
|
import database from '../lib/database';
|
||||||
import protectedFunction from '../lib/methods/helpers/protectedFunction';
|
import protectedFunction from '../lib/methods/helpers/protectedFunction';
|
||||||
|
import { useActionSheet } from './ActionSheet';
|
||||||
import I18n from '../i18n';
|
import I18n from '../i18n';
|
||||||
import log from '../utils/log';
|
import log from '../utils/log';
|
||||||
|
|
||||||
class MessageErrorActions extends React.Component {
|
const MessageErrorActions = forwardRef(({ tmid }, ref) => {
|
||||||
static propTypes = {
|
const { showActionSheet } = useActionSheet();
|
||||||
actionsHide: PropTypes.func.isRequired,
|
|
||||||
message: PropTypes.object,
|
|
||||||
tmid: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/sort-comp
|
const handleResend = protectedFunction(async(message) => {
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.handleActionPress = this.handleActionPress.bind(this);
|
|
||||||
this.options = [I18n.t('Cancel'), I18n.t('Delete'), I18n.t('Resend')];
|
|
||||||
this.CANCEL_INDEX = 0;
|
|
||||||
this.DELETE_INDEX = 1;
|
|
||||||
this.RESEND_INDEX = 2;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.showActionSheet();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleResend = protectedFunction(async() => {
|
|
||||||
const { message, tmid } = this.props;
|
|
||||||
await RocketChat.resendMessage(message, tmid);
|
await RocketChat.resendMessage(message, tmid);
|
||||||
});
|
});
|
||||||
|
|
||||||
handleDelete = async() => {
|
const handleDelete = async(message) => {
|
||||||
try {
|
try {
|
||||||
const { message, tmid } = this.props;
|
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const deleteBatch = [];
|
const deleteBatch = [];
|
||||||
const msgCollection = db.collections.get('messages');
|
const msgCollection = db.collections.get('messages');
|
||||||
|
@ -49,7 +30,7 @@ class MessageErrorActions extends React.Component {
|
||||||
try {
|
try {
|
||||||
const msg = await msgCollection.find(message.id);
|
const msg = await msgCollection.find(message.id);
|
||||||
deleteBatch.push(msg.prepareDestroyPermanently());
|
deleteBatch.push(msg.prepareDestroyPermanently());
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Do nothing: message not found
|
// Do nothing: message not found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +49,7 @@ class MessageErrorActions extends React.Component {
|
||||||
// If the whole thread was removed, delete the thread
|
// If the whole thread was removed, delete the thread
|
||||||
const thread = await threadCollection.find(tmid);
|
const thread = await threadCollection.find(tmid);
|
||||||
deleteBatch.push(thread.prepareDestroyPermanently());
|
deleteBatch.push(thread.prepareDestroyPermanently());
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Do nothing: thread not found
|
// Do nothing: thread not found
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -78,7 +59,7 @@ class MessageErrorActions extends React.Component {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Do nothing: message not found
|
// Do nothing: message not found
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,39 +69,34 @@ class MessageErrorActions extends React.Component {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e);
|
log(e);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
showActionSheet = () => {
|
const showMessageErrorActions = (message) => {
|
||||||
ActionSheet.showActionSheetWithOptions({
|
showActionSheet({
|
||||||
options: this.options,
|
options: [
|
||||||
cancelButtonIndex: this.CANCEL_INDEX,
|
{
|
||||||
destructiveButtonIndex: this.DELETE_INDEX,
|
title: I18n.t('Resend'),
|
||||||
title: I18n.t('Message_actions')
|
icon: 'send',
|
||||||
}, (actionIndex) => {
|
onPress: () => handleResend(message)
|
||||||
this.handleActionPress(actionIndex);
|
},
|
||||||
|
{
|
||||||
|
title: I18n.t('Delete'),
|
||||||
|
icon: 'trash',
|
||||||
|
danger: true,
|
||||||
|
onPress: () => handleDelete(message)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
hasCancel: true
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
handleActionPress = (actionIndex) => {
|
useImperativeHandle(ref, () => ({
|
||||||
const { actionsHide } = this.props;
|
showMessageErrorActions
|
||||||
switch (actionIndex) {
|
}));
|
||||||
case this.RESEND_INDEX:
|
});
|
||||||
this.handleResend();
|
MessageErrorActions.propTypes = {
|
||||||
break;
|
message: PropTypes.object,
|
||||||
case this.DELETE_INDEX:
|
tmid: PropTypes.string
|
||||||
this.handleDelete();
|
};
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
actionsHide();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MessageErrorActions;
|
export default MessageErrorActions;
|
||||||
|
|
|
@ -405,6 +405,7 @@ export default {
|
||||||
Review_app_later: 'Maybe later',
|
Review_app_later: 'Maybe later',
|
||||||
Review_app_unable_store: 'Unable to open {{store}}',
|
Review_app_unable_store: 'Unable to open {{store}}',
|
||||||
Review_this_app: 'Review this app',
|
Review_this_app: 'Review this app',
|
||||||
|
Remove: 'Remove',
|
||||||
Roles: 'Roles',
|
Roles: 'Roles',
|
||||||
Room_actions: 'Room actions',
|
Room_actions: 'Room actions',
|
||||||
Room_changed_announcement: 'Room announcement changed to: {{announcement}} by {{userBy}}',
|
Room_changed_announcement: 'Room announcement changed to: {{announcement}} by {{userBy}}',
|
||||||
|
@ -470,6 +471,7 @@ export default {
|
||||||
starred: 'starred',
|
starred: 'starred',
|
||||||
Starred: 'Starred',
|
Starred: 'Starred',
|
||||||
Start_of_conversation: 'Start of conversation',
|
Start_of_conversation: 'Start of conversation',
|
||||||
|
Start_a_Discussion: 'Start a Discussion',
|
||||||
Started_discussion: 'Started a discussion:',
|
Started_discussion: 'Started a discussion:',
|
||||||
Started_call: 'Call started by {{userBy}}',
|
Started_call: 'Call started by {{userBy}}',
|
||||||
Submit: 'Submit',
|
Submit: 'Submit',
|
||||||
|
@ -482,6 +484,8 @@ export default {
|
||||||
Terms_of_Service: ' Terms of Service ',
|
Terms_of_Service: ' Terms of Service ',
|
||||||
Theme: 'Theme',
|
Theme: 'Theme',
|
||||||
The_URL_is_invalid: 'Invalid URL or unable to establish a secure connection.\n{{contact}}',
|
The_URL_is_invalid: 'Invalid URL or unable to establish a secure connection.\n{{contact}}',
|
||||||
|
The_user_wont_be_able_to_type_in_roomName: 'The user won\'t be able to type in {{roomName}}',
|
||||||
|
The_user_will_be_able_to_type_in_roomName: 'The user will be able to type in {{roomName}}',
|
||||||
There_was_an_error_while_action: 'There was an error while {{action}}!',
|
There_was_an_error_while_action: 'There was an error while {{action}}!',
|
||||||
This_room_is_blocked: 'This room is blocked',
|
This_room_is_blocked: 'This room is blocked',
|
||||||
This_room_is_read_only: 'This room is read only',
|
This_room_is_read_only: 'This room is read only',
|
||||||
|
@ -566,6 +570,7 @@ export default {
|
||||||
Your_workspace: 'Your workspace',
|
Your_workspace: 'Your workspace',
|
||||||
Version_no: 'Version: {{version}}',
|
Version_no: 'Version: {{version}}',
|
||||||
You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!',
|
You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!',
|
||||||
|
You_will_unset_a_certificate_for_this_server: 'You will unset a certificate for this server',
|
||||||
Change_Language: 'Change Language',
|
Change_Language: 'Change Language',
|
||||||
Crash_report_disclaimer: 'We never track the content of your chats. The crash report only contains relevant information for us in order to identify problems and fix it.',
|
Crash_report_disclaimer: 'We never track the content of your chats. The crash report only contains relevant information for us in order to identify problems and fix it.',
|
||||||
Type_message: 'Type message',
|
Type_message: 'Type message',
|
||||||
|
@ -578,6 +583,7 @@ export default {
|
||||||
Search_messages: 'Search messages',
|
Search_messages: 'Search messages',
|
||||||
Scroll_messages: 'Scroll messages',
|
Scroll_messages: 'Scroll messages',
|
||||||
Reply_latest: 'Reply to latest',
|
Reply_latest: 'Reply to latest',
|
||||||
|
Reply_in_Thread: 'Reply in Thread',
|
||||||
Server_selection: 'Server selection',
|
Server_selection: 'Server selection',
|
||||||
Server_selection_numbers: 'Server selection 1...9',
|
Server_selection_numbers: 'Server selection 1...9',
|
||||||
Add_server: 'Add server',
|
Add_server: 'Add server',
|
||||||
|
|
|
@ -365,6 +365,7 @@ export default {
|
||||||
Review_app_later: 'Talvez depois',
|
Review_app_later: 'Talvez depois',
|
||||||
Review_app_unable_store: 'Não foi possível abrir {{store}}',
|
Review_app_unable_store: 'Não foi possível abrir {{store}}',
|
||||||
Review_this_app: 'Avaliar esse app',
|
Review_this_app: 'Avaliar esse app',
|
||||||
|
Remove: 'Remover',
|
||||||
Roles: 'Papéis',
|
Roles: 'Papéis',
|
||||||
Room_actions: 'Ações',
|
Room_actions: 'Ações',
|
||||||
Room_changed_announcement: 'O anúncio da sala foi alterado para: {{announcement}} por {{userBy}}',
|
Room_changed_announcement: 'O anúncio da sala foi alterado para: {{announcement}} por {{userBy}}',
|
||||||
|
@ -426,6 +427,8 @@ export default {
|
||||||
Take_a_video: 'Gravar um vídeo',
|
Take_a_video: 'Gravar um vídeo',
|
||||||
Terms_of_Service: ' Termos de Serviço ',
|
Terms_of_Service: ' Termos de Serviço ',
|
||||||
Theme: 'Tema',
|
Theme: 'Tema',
|
||||||
|
The_user_wont_be_able_to_type_in_roomName: 'O usuário não poderá digitar em {{roomName}}',
|
||||||
|
The_user_will_be_able_to_type_in_roomName: 'O usuário poderá digitar em {{roomName}}',
|
||||||
The_URL_is_invalid: 'A URL fornecida é inválida ou incapaz de estabelecer uma conexão segura.\n{{contact}}',
|
The_URL_is_invalid: 'A URL fornecida é inválida ou incapaz de estabelecer uma conexão segura.\n{{contact}}',
|
||||||
There_was_an_error_while_action: 'Aconteceu um erro {{action}}!',
|
There_was_an_error_while_action: 'Aconteceu um erro {{action}}!',
|
||||||
This_room_is_blocked: 'Este quarto está bloqueado',
|
This_room_is_blocked: 'Este quarto está bloqueado',
|
||||||
|
@ -497,6 +500,7 @@ export default {
|
||||||
Your_invite_link_will_never_expire: 'Seu link de convite nunca irá vencer.',
|
Your_invite_link_will_never_expire: 'Seu link de convite nunca irá vencer.',
|
||||||
Your_workspace: 'Sua workspace',
|
Your_workspace: 'Sua workspace',
|
||||||
You_will_not_be_able_to_recover_this_message: 'Você não será capaz de recuperar essa mensagem!',
|
You_will_not_be_able_to_recover_this_message: 'Você não será capaz de recuperar essa mensagem!',
|
||||||
|
You_will_unset_a_certificate_for_this_server: 'Você cancelará a configuração de um certificado para este servidor',
|
||||||
Would_you_like_to_return_the_inquiry: 'Deseja retornar a consulta?',
|
Would_you_like_to_return_the_inquiry: 'Deseja retornar a consulta?',
|
||||||
Write_External_Permission_Message: 'Rocket Chat precisa de acesso à sua galeria para salvar imagens',
|
Write_External_Permission_Message: 'Rocket Chat precisa de acesso à sua galeria para salvar imagens',
|
||||||
Write_External_Permission: 'Acesso à Galeria',
|
Write_External_Permission: 'Acesso à Galeria',
|
||||||
|
|
|
@ -12,3 +12,5 @@ export function withTheme(Component) {
|
||||||
hoistNonReactStatics(ThemedComponent, Component);
|
hoistNonReactStatics(ThemedComponent, Component);
|
||||||
return ThemedComponent;
|
return ThemedComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useTheme = () => React.useContext(ThemeContext);
|
||||||
|
|
|
@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
||||||
import { FlatList, View, Text } from 'react-native';
|
import { FlatList, View, Text } from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import equal from 'deep-equal';
|
import equal from 'deep-equal';
|
||||||
import ActionSheet from 'react-native-action-sheet';
|
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import Message from '../../containers/message';
|
import Message from '../../containers/message';
|
||||||
|
@ -15,11 +14,9 @@ import getFileUrlFromMessage from '../../lib/methods/helpers/getFileUrlFromMessa
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
import { getUserSelector } from '../../selectors/login';
|
import { getUserSelector } from '../../selectors/login';
|
||||||
|
import { withActionSheet } from '../../containers/ActionSheet';
|
||||||
import SafeAreaView from '../../containers/SafeAreaView';
|
import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
|
|
||||||
const ACTION_INDEX = 0;
|
|
||||||
const CANCEL_INDEX = 1;
|
|
||||||
|
|
||||||
class MessagesView extends React.Component {
|
class MessagesView extends React.Component {
|
||||||
static navigationOptions = ({ route }) => ({
|
static navigationOptions = ({ route }) => ({
|
||||||
title: I18n.t(route.params?.name)
|
title: I18n.t(route.params?.name)
|
||||||
|
@ -31,7 +28,8 @@ class MessagesView extends React.Component {
|
||||||
navigation: PropTypes.object,
|
navigation: PropTypes.object,
|
||||||
route: PropTypes.object,
|
route: PropTypes.object,
|
||||||
customEmojis: PropTypes.object,
|
customEmojis: PropTypes.object,
|
||||||
theme: PropTypes.string
|
theme: PropTypes.string,
|
||||||
|
showActionSheet: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -162,7 +160,7 @@ class MessagesView extends React.Component {
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
actionTitle: I18n.t('Unstar'),
|
action: message => ({ title: I18n.t('Unstar'), icon: message.starred ? 'star-filled' : 'star', onPress: this.handleActionPress }),
|
||||||
handleActionPress: message => RocketChat.toggleStarMessage(message._id, message.starred)
|
handleActionPress: message => RocketChat.toggleStarMessage(message._id, message.starred)
|
||||||
},
|
},
|
||||||
// Pinned Messages Screen
|
// Pinned Messages Screen
|
||||||
|
@ -179,7 +177,7 @@ class MessagesView extends React.Component {
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
actionTitle: I18n.t('Unpin'),
|
action: () => ({ title: I18n.t('Unpin'), icon: 'pin', onPress: this.handleActionPress }),
|
||||||
handleActionPress: message => RocketChat.togglePinMessage(message._id, message.pinned)
|
handleActionPress: message => RocketChat.togglePinMessage(message._id, message.pinned)
|
||||||
}
|
}
|
||||||
}[name]);
|
}[name]);
|
||||||
|
@ -225,35 +223,28 @@ class MessagesView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onLongPress = (message) => {
|
onLongPress = (message) => {
|
||||||
this.setState({ message });
|
this.setState({ message }, this.showActionSheet);
|
||||||
this.showActionSheet();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showActionSheet = () => {
|
showActionSheet = () => {
|
||||||
ActionSheet.showActionSheetWithOptions({
|
const { message } = this.state;
|
||||||
options: [this.content.actionTitle, I18n.t('Cancel')],
|
const { showActionSheet } = this.props;
|
||||||
cancelButtonIndex: CANCEL_INDEX,
|
showActionSheet({ options: [this.content.action(message)], hasCancel: true });
|
||||||
title: I18n.t('Actions')
|
|
||||||
}, (actionIndex) => {
|
|
||||||
this.handleActionPress(actionIndex);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleActionPress = async(actionIndex) => {
|
handleActionPress = async() => {
|
||||||
if (actionIndex === ACTION_INDEX) {
|
const { message } = this.state;
|
||||||
const { message } = this.state;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.content.handleActionPress(message);
|
const result = await this.content.handleActionPress(message);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
messages: prevState.messages.filter(item => item._id !== message._id),
|
messages: prevState.messages.filter(item => item._id !== message._id),
|
||||||
total: prevState.total - 1
|
total: prevState.total - 1
|
||||||
}));
|
}));
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('MessagesView -> handleActionPress -> catch -> error', error);
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// Do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,4 +303,4 @@ const mapStateToProps = state => ({
|
||||||
customEmojis: state.customEmojis
|
customEmojis: state.customEmojis
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(withTheme(MessagesView));
|
export default connect(mapStateToProps)(withTheme(withActionSheet(MessagesView)));
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import * as FileSystem from 'expo-file-system';
|
import * as FileSystem from 'expo-file-system';
|
||||||
import DocumentPicker from 'react-native-document-picker';
|
import DocumentPicker from 'react-native-document-picker';
|
||||||
import ActionSheet from 'react-native-action-sheet';
|
|
||||||
import RNUserDefaults from 'rn-user-defaults';
|
import RNUserDefaults from 'rn-user-defaults';
|
||||||
import { encode } from 'base-64';
|
import { encode } from 'base-64';
|
||||||
import parse from 'url-parse';
|
import parse from 'url-parse';
|
||||||
|
@ -26,6 +25,7 @@ import { animateNextTransition } from '../utils/layoutAnimation';
|
||||||
import { withTheme } from '../theme';
|
import { withTheme } from '../theme';
|
||||||
import { setBasicAuth, BASIC_AUTH_KEY } from '../utils/fetch';
|
import { setBasicAuth, BASIC_AUTH_KEY } from '../utils/fetch';
|
||||||
import { CloseModalButton } from '../containers/HeaderButton';
|
import { CloseModalButton } from '../containers/HeaderButton';
|
||||||
|
import { showConfirmationAlert } from '../utils/info';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
title: {
|
title: {
|
||||||
|
@ -83,14 +83,6 @@ class NewServerView extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel
|
|
||||||
this.options = [I18n.t('Cancel')];
|
|
||||||
this.CANCEL_INDEX = 0;
|
|
||||||
|
|
||||||
// Delete
|
|
||||||
this.options.push(I18n.t('Delete'));
|
|
||||||
this.DELETE_INDEX = 1;
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
text: '',
|
text: '',
|
||||||
connectingOpen: false,
|
connectingOpen: false,
|
||||||
|
@ -233,15 +225,11 @@ class NewServerView extends React.Component {
|
||||||
this.setState({ certificate });
|
this.setState({ certificate });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDelete = () => this.setState({ certificate: null }); // We not need delete file from DocumentPicker because it is a temp file
|
handleRemove = () => {
|
||||||
|
showConfirmationAlert({
|
||||||
showActionSheet = () => {
|
message: I18n.t('You_will_unset_a_certificate_for_this_server'),
|
||||||
ActionSheet.showActionSheetWithOptions({
|
callToAction: I18n.t('Remove'),
|
||||||
options: this.options,
|
onPress: this.setState({ certificate: null }) // We not need delete file from DocumentPicker because it is a temp file
|
||||||
cancelButtonIndex: this.CANCEL_INDEX,
|
|
||||||
destructiveButtonIndex: this.DELETE_INDEX
|
|
||||||
}, (actionIndex) => {
|
|
||||||
if (actionIndex === this.DELETE_INDEX) { this.handleDelete(); }
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,7 +247,7 @@ class NewServerView extends React.Component {
|
||||||
{certificate ? I18n.t('Your_certificate') : I18n.t('Do_you_have_a_certificate')}
|
{certificate ? I18n.t('Your_certificate') : I18n.t('Do_you_have_a_certificate')}
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={certificate ? this.showActionSheet : this.chooseCertificate}
|
onPress={certificate ? this.handleRemove : this.chooseCertificate}
|
||||||
testID='new-server-choose-certificate'
|
testID='new-server-choose-certificate'
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { FlatList, View } from 'react-native';
|
import { FlatList, View } from 'react-native';
|
||||||
import ActionSheet from 'react-native-action-sheet';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import * as Haptics from 'expo-haptics';
|
|
||||||
import { Q } from '@nozbe/watermelondb';
|
import { Q } from '@nozbe/watermelondb';
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
@ -23,6 +21,8 @@ import ActivityIndicator from '../../containers/ActivityIndicator';
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import { getUserSelector } from '../../selectors/login';
|
import { getUserSelector } from '../../selectors/login';
|
||||||
|
import { withActionSheet } from '../../containers/ActionSheet';
|
||||||
|
import { showConfirmationAlert } from '../../utils/info';
|
||||||
import SafeAreaView from '../../containers/SafeAreaView';
|
import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
import { goRoom } from '../../utils/goRoom';
|
import { goRoom } from '../../utils/goRoom';
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ class RoomMembersView extends React.Component {
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
token: PropTypes.string
|
token: PropTypes.string
|
||||||
}),
|
}),
|
||||||
|
showActionSheet: PropTypes.func,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
isMasterDetail: PropTypes.bool
|
isMasterDetail: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
@ -47,9 +48,7 @@ class RoomMembersView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.mounted = false;
|
this.mounted = false;
|
||||||
this.CANCEL_INDEX = 0;
|
this.MUTE_INDEX = 0;
|
||||||
this.MUTE_INDEX = 1;
|
|
||||||
this.actionSheetOptions = [''];
|
|
||||||
const rid = props.route.params?.rid;
|
const rid = props.route.params?.rid;
|
||||||
const room = props.route.params?.room;
|
const room = props.route.params?.room;
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -59,7 +58,6 @@ class RoomMembersView extends React.Component {
|
||||||
rid,
|
rid,
|
||||||
members: [],
|
members: [],
|
||||||
membersFiltered: [],
|
membersFiltered: [],
|
||||||
userLongPressed: {},
|
|
||||||
room: room || {},
|
room: room || {},
|
||||||
end: false
|
end: false
|
||||||
};
|
};
|
||||||
|
@ -140,19 +138,28 @@ class RoomMembersView extends React.Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { room } = this.state;
|
const { room } = this.state;
|
||||||
|
const { showActionSheet } = this.props;
|
||||||
const { muted } = room;
|
const { muted } = room;
|
||||||
|
|
||||||
this.actionSheetOptions = [I18n.t('Cancel')];
|
|
||||||
const userIsMuted = !!(muted || []).find(m => m === user.username);
|
const userIsMuted = !!(muted || []).find(m => m === user.username);
|
||||||
user.muted = userIsMuted;
|
user.muted = userIsMuted;
|
||||||
if (userIsMuted) {
|
|
||||||
this.actionSheetOptions.push(I18n.t('Unmute'));
|
showActionSheet({
|
||||||
} else {
|
options: [{
|
||||||
this.actionSheetOptions.push(I18n.t('Mute'));
|
icon: userIsMuted ? 'volume' : 'volume-off',
|
||||||
}
|
title: I18n.t(userIsMuted ? 'Unmute' : 'Mute'),
|
||||||
this.setState({ userLongPressed: user });
|
onPress: () => {
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
showConfirmationAlert({
|
||||||
this.showActionSheet();
|
message: I18n.t(`The_user_${ userIsMuted ? 'will' : 'wont' }_be_able_to_type_in_roomName`, {
|
||||||
|
roomName: RocketChat.getRoomTitle(room)
|
||||||
|
}),
|
||||||
|
callToAction: I18n.t(userIsMuted ? 'Unmute' : 'Mute'),
|
||||||
|
onPress: () => this.handleMute(user)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
hasCancel: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleStatus = () => {
|
toggleStatus = () => {
|
||||||
|
@ -166,16 +173,6 @@ class RoomMembersView extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showActionSheet = () => {
|
|
||||||
ActionSheet.showActionSheetWithOptions({
|
|
||||||
options: this.actionSheetOptions,
|
|
||||||
cancelButtonIndex: this.CANCEL_INDEX,
|
|
||||||
title: I18n.t('Actions')
|
|
||||||
}, (actionIndex) => {
|
|
||||||
this.handleActionPress(actionIndex);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/sort-comp
|
// eslint-disable-next-line react/sort-comp
|
||||||
fetchMembers = async() => {
|
fetchMembers = async() => {
|
||||||
const {
|
const {
|
||||||
|
@ -211,26 +208,16 @@ class RoomMembersView extends React.Component {
|
||||||
goRoom({ item, isMasterDetail });
|
goRoom({ item, isMasterDetail });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMute = async() => {
|
handleMute = async(user) => {
|
||||||
const { rid, userLongPressed } = this.state;
|
const { rid } = this.state;
|
||||||
try {
|
try {
|
||||||
await RocketChat.toggleMuteUserInRoom(rid, userLongPressed.username, !userLongPressed.muted);
|
await RocketChat.toggleMuteUserInRoom(rid, user?.username, !user?.muted);
|
||||||
EventEmitter.emit(LISTENER, { message: I18n.t('User_has_been_key', { key: userLongPressed.muted ? I18n.t('unmuted') : I18n.t('muted') }) });
|
EventEmitter.emit(LISTENER, { message: I18n.t('User_has_been_key', { key: user?.muted ? I18n.t('unmuted') : I18n.t('muted') }) });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e);
|
log(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleActionPress = (actionIndex) => {
|
|
||||||
switch (actionIndex) {
|
|
||||||
case this.MUTE_INDEX:
|
|
||||||
this.handleMute();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSearchBar = () => (
|
renderSearchBar = () => (
|
||||||
<SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='room-members-view-search' />
|
<SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='room-members-view-search' />
|
||||||
)
|
)
|
||||||
|
@ -295,4 +282,4 @@ const mapStateToProps = state => ({
|
||||||
isMasterDetail: state.app.isMasterDetail
|
isMasterDetail: state.app.isMasterDetail
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(withTheme(RoomMembersView));
|
export default connect(mapStateToProps)(withActionSheet(withTheme(RoomMembersView)));
|
||||||
|
|
|
@ -58,8 +58,7 @@ const stateAttrsUpdate = [
|
||||||
'lastOpen',
|
'lastOpen',
|
||||||
'reactionsModalVisible',
|
'reactionsModalVisible',
|
||||||
'canAutoTranslate',
|
'canAutoTranslate',
|
||||||
'showActions',
|
'selectedMessage',
|
||||||
'showErrorActions',
|
|
||||||
'loading',
|
'loading',
|
||||||
'editing',
|
'editing',
|
||||||
'replying',
|
'replying',
|
||||||
|
@ -117,8 +116,6 @@ class RoomView extends React.Component {
|
||||||
selectedMessage: selectedMessage || {},
|
selectedMessage: selectedMessage || {},
|
||||||
canAutoTranslate: false,
|
canAutoTranslate: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
showActions: false,
|
|
||||||
showErrorActions: false,
|
|
||||||
editing: false,
|
editing: false,
|
||||||
replying: !!selectedMessage,
|
replying: !!selectedMessage,
|
||||||
replyWithMention: false,
|
replyWithMention: false,
|
||||||
|
@ -501,23 +498,11 @@ class RoomView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
errorActionsShow = (message) => {
|
errorActionsShow = (message) => {
|
||||||
this.setState({ selectedMessage: message, showErrorActions: true });
|
this.messageErrorActions?.showMessageErrorActions(message);
|
||||||
}
|
|
||||||
|
|
||||||
onActionsHide = () => {
|
|
||||||
const { editing, replying, reacting } = this.state;
|
|
||||||
if (editing || replying || reacting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState({ selectedMessage: {}, showActions: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
onErrorActionsHide = () => {
|
|
||||||
this.setState({ selectedMessage: {}, showErrorActions: false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditInit = (message) => {
|
onEditInit = (message) => {
|
||||||
this.setState({ selectedMessage: message, editing: true, showActions: false });
|
this.setState({ selectedMessage: message, editing: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditCancel = () => {
|
onEditCancel = () => {
|
||||||
|
@ -535,7 +520,7 @@ class RoomView extends React.Component {
|
||||||
|
|
||||||
onReplyInit = (message, mention) => {
|
onReplyInit = (message, mention) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedMessage: message, replying: true, showActions: false, replyWithMention: mention
|
selectedMessage: message, replying: true, replyWithMention: mention
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -544,7 +529,7 @@ class RoomView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onReactionInit = (message) => {
|
onReactionInit = (message) => {
|
||||||
this.setState({ selectedMessage: message, reacting: true, showActions: false });
|
this.setState({ selectedMessage: message, reacting: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
onReactionClose = () => {
|
onReactionClose = () => {
|
||||||
|
@ -552,7 +537,7 @@ class RoomView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessageLongPress = (message) => {
|
onMessageLongPress = (message) => {
|
||||||
this.setState({ selectedMessage: message, showActions: true });
|
this.messageActions?.showMessageActions(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
showAttachment = (attachment) => {
|
showAttachment = (attachment) => {
|
||||||
|
@ -942,9 +927,7 @@ class RoomView extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
renderActions = () => {
|
renderActions = () => {
|
||||||
const {
|
const { room, readOnly } = this.state;
|
||||||
room, selectedMessage, showActions, showErrorActions, joined, readOnly
|
|
||||||
} = this.state;
|
|
||||||
const {
|
const {
|
||||||
user, navigation
|
user, navigation
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -953,29 +936,21 @@ class RoomView extends React.Component {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{joined && showActions
|
<MessageActions
|
||||||
? (
|
ref={ref => this.messageActions = ref}
|
||||||
<MessageActions
|
tmid={this.tmid}
|
||||||
tmid={this.tmid}
|
room={room}
|
||||||
room={room}
|
user={user}
|
||||||
user={user}
|
editInit={this.onEditInit}
|
||||||
message={selectedMessage}
|
replyInit={this.onReplyInit}
|
||||||
actionsHide={this.onActionsHide}
|
reactionInit={this.onReactionInit}
|
||||||
editInit={this.onEditInit}
|
onReactionPress={this.onReactionPress}
|
||||||
replyInit={this.onReplyInit}
|
isReadOnly={readOnly}
|
||||||
reactionInit={this.onReactionInit}
|
/>
|
||||||
isReadOnly={readOnly}
|
<MessageErrorActions
|
||||||
/>
|
ref={ref => this.messageErrorActions = ref}
|
||||||
)
|
tmid={this.tmid}
|
||||||
: null
|
/>
|
||||||
}
|
|
||||||
{showErrorActions ? (
|
|
||||||
<MessageErrorActions
|
|
||||||
tmid={this.tmid}
|
|
||||||
message={selectedMessage}
|
|
||||||
actionsHide={this.onErrorActionsHide}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,48 +151,54 @@ describe('Room screen', () => {
|
||||||
|
|
||||||
describe('Message', async() => {
|
describe('Message', async() => {
|
||||||
it('should copy permalink', async() => {
|
it('should copy permalink', async() => {
|
||||||
await sleep(1000);
|
|
||||||
await element(by.label(`${ data.random }message`)).atIndex(0).tap();
|
|
||||||
await element(by.label(`${ data.random }message`)).atIndex(0).longPress();
|
await element(by.label(`${ data.random }message`)).atIndex(0).longPress();
|
||||||
await waitFor(element(by.text('Message actions'))).toExist().withTimeout(5000);
|
await expect(element(by.id('action-sheet'))).toExist();
|
||||||
await expect(element(by.text('Message actions'))).toExist();
|
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||||
await element(by.text('Permalink')).tap();
|
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||||
|
await element(by.label('Permalink')).tap();
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
|
|
||||||
// TODO: test clipboard
|
// TODO: test clipboard
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should copy message', async() => {
|
it('should copy message', async() => {
|
||||||
await element(by.label(`${ data.random }message`)).atIndex(0).longPress();
|
await element(by.label(`${ data.random }message`)).atIndex(0).longPress();
|
||||||
await waitFor(element(by.text('Message actions'))).toExist().withTimeout(5000);
|
await expect(element(by.id('action-sheet'))).toExist();
|
||||||
await expect(element(by.text('Message actions'))).toExist();
|
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||||
await element(by.text('Copy')).tap();
|
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||||
|
await element(by.label('Copy')).tap();
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
|
|
||||||
// TODO: test clipboard
|
// TODO: test clipboard
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should star message', async() => {
|
it('should star message', async() => {
|
||||||
await element(by.label(`${ data.random }message`)).atIndex(0).longPress();
|
await element(by.label(`${ data.random }message`)).atIndex(0).longPress();
|
||||||
await waitFor(element(by.text('Message actions'))).toExist().withTimeout(5000);
|
await expect(element(by.id('action-sheet'))).toExist();
|
||||||
await expect(element(by.text('Message actions'))).toExist();
|
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||||
await element(by.text('Star')).tap();
|
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||||
await sleep(2000);
|
await element(by.label('Star')).tap();
|
||||||
await waitFor(element(by.text('Message actions'))).toBeNotVisible().withTimeout(5000);
|
await sleep(1000);
|
||||||
|
await waitFor(element(by.id('action-sheet'))).toNotExist().withTimeout(5000);
|
||||||
|
|
||||||
await element(by.label(`${ data.random }message`)).atIndex(0).longPress();
|
await element(by.label(`${ data.random }message`)).atIndex(0).longPress();
|
||||||
await waitFor(element(by.text('Unstar'))).toExist().withTimeout(2000);
|
await expect(element(by.id('action-sheet'))).toExist();
|
||||||
await expect(element(by.text('Unstar'))).toExist();
|
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||||
await element(by.text('Cancel')).tap();
|
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||||
await waitFor(element(by.text('Cancel'))).toBeNotVisible().withTimeout(2000);
|
await waitFor(element(by.label('Unstar'))).toBeVisible().withTimeout(2000);
|
||||||
|
await expect(element(by.label('Unstar'))).toBeVisible();
|
||||||
|
await element(by.id('action-sheet-backdrop')).tap();
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should react to message', async() => {
|
it('should react to message', async() => {
|
||||||
await element(by.label(`${ data.random }message`)).atIndex(0).longPress();
|
await element(by.label(`${ data.random }message`)).atIndex(0).longPress();
|
||||||
await waitFor(element(by.text('Message actions'))).toExist().withTimeout(5000);
|
await expect(element(by.id('action-sheet'))).toExist();
|
||||||
await expect(element(by.text('Message actions'))).toExist();
|
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||||
await element(by.text('Add Reaction')).tap();
|
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||||
await waitFor(element(by.id('reaction-picker'))).toExist().withTimeout(2000);
|
await element(by.id('add-reaction')).tap();
|
||||||
await expect(element(by.id('reaction-picker'))).toExist();
|
await waitFor(element(by.id('reaction-picker'))).toBeVisible().withTimeout(2000);
|
||||||
|
await expect(element(by.id('reaction-picker'))).toBeVisible();
|
||||||
await element(by.id('reaction-picker-😃')).tap();
|
await element(by.id('reaction-picker-😃')).tap();
|
||||||
await waitFor(element(by.id('reaction-picker-grinning'))).toExist().withTimeout(2000);
|
await waitFor(element(by.id('reaction-picker-grinning'))).toExist().withTimeout(2000);
|
||||||
await expect(element(by.id('reaction-picker-grinning'))).toExist();
|
await expect(element(by.id('reaction-picker-grinning'))).toExist();
|
||||||
|
@ -202,6 +208,19 @@ describe('Room screen', () => {
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should react to message with frequently used emoji', async() => {
|
||||||
|
await element(by.label(`${ data.random }message`)).atIndex(0).longPress();
|
||||||
|
await expect(element(by.id('action-sheet'))).toExist();
|
||||||
|
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||||
|
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||||
|
await waitFor(element(by.id('message-actions-emoji-+1'))).toBeVisible().withTimeout(2000);
|
||||||
|
await expect(element(by.id('message-actions-emoji-+1'))).toBeVisible();
|
||||||
|
await element(by.id('message-actions-emoji-+1')).tap();
|
||||||
|
await waitFor(element(by.id('message-reaction-:+1:'))).toBeVisible().withTimeout(60000);
|
||||||
|
await expect(element(by.id('message-reaction-:+1:'))).toBeVisible();
|
||||||
|
await sleep(1000);
|
||||||
|
});
|
||||||
|
|
||||||
it('should show reaction picker on add reaction button pressed and have frequently used emoji', async() => {
|
it('should show reaction picker on add reaction button pressed and have frequently used emoji', async() => {
|
||||||
await element(by.id('message-add-reaction')).tap();
|
await element(by.id('message-add-reaction')).tap();
|
||||||
await waitFor(element(by.id('reaction-picker'))).toExist().withTimeout(2000);
|
await waitFor(element(by.id('reaction-picker'))).toExist().withTimeout(2000);
|
||||||
|
@ -214,54 +233,76 @@ describe('Room screen', () => {
|
||||||
await waitFor(element(by.id('message-reaction-:grimacing:'))).toExist().withTimeout(60000);
|
await waitFor(element(by.id('message-reaction-:grimacing:'))).toExist().withTimeout(60000);
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove reaction', async() => {
|
it('should remove reaction', async() => {
|
||||||
await element(by.id('message-reaction-:grinning:')).tap();
|
await element(by.id('message-reaction-:grinning:')).tap();
|
||||||
await waitFor(element(by.id('message-reaction-:grinning:'))).toBeNotVisible().withTimeout(60000);
|
await waitFor(element(by.id('message-reaction-:grinning:'))).toBeNotVisible().withTimeout(60000);
|
||||||
await expect(element(by.id('message-reaction-:grinning:'))).toBeNotVisible();
|
await expect(element(by.id('message-reaction-:grinning:'))).toBeNotVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should edit message', async() => {
|
it('should edit message', async() => {
|
||||||
await mockMessage('edit');
|
await mockMessage('edit');
|
||||||
await element(by.label(`${ data.random }edit`)).atIndex(0).longPress();
|
await element(by.label(`${ data.random }edit`)).atIndex(0).longPress();
|
||||||
await waitFor(element(by.text('Message actions'))).toExist().withTimeout(5000);
|
await expect(element(by.id('action-sheet'))).toExist();
|
||||||
await expect(element(by.text('Message actions'))).toExist();
|
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||||
await element(by.text('Edit')).tap();
|
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||||
|
await element(by.label('Edit')).tap();
|
||||||
await element(by.id('messagebox-input')).typeText('ed');
|
await element(by.id('messagebox-input')).typeText('ed');
|
||||||
await element(by.id('messagebox-send-message')).tap();
|
await element(by.id('messagebox-send-message')).tap();
|
||||||
await waitFor(element(by.label(`${ data.random }edited (edited)`)).atIndex(0)).toExist().withTimeout(60000);
|
await waitFor(element(by.label(`${ data.random }edited (edited)`)).atIndex(0)).toExist().withTimeout(60000);
|
||||||
await expect(element(by.label(`${ data.random }edited (edited)`)).atIndex(0)).toExist();
|
await expect(element(by.label(`${ data.random }edited (edited)`)).atIndex(0)).toExist();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should quote message', async() => {
|
it('should quote message', async() => {
|
||||||
await mockMessage('quote');
|
await mockMessage('quote');
|
||||||
await element(by.label(`${ data.random }quote`)).atIndex(0).longPress();
|
await element(by.label(`${ data.random }quote`)).atIndex(0).longPress();
|
||||||
await waitFor(element(by.text('Message actions'))).toExist().withTimeout(5000);
|
await expect(element(by.id('action-sheet'))).toExist();
|
||||||
await expect(element(by.text('Message actions'))).toExist();
|
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||||
await element(by.text('Quote')).tap();
|
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||||
|
await element(by.label('Quote')).tap();
|
||||||
await element(by.id('messagebox-input')).typeText(`${ data.random }quoted`);
|
await element(by.id('messagebox-input')).typeText(`${ data.random }quoted`);
|
||||||
await element(by.id('messagebox-send-message')).tap();
|
await element(by.id('messagebox-send-message')).tap();
|
||||||
|
await sleep(1000);
|
||||||
|
|
||||||
// TODO: test if quote was sent
|
// TODO: test if quote was sent
|
||||||
await sleep(2000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pin message', async() => {
|
it('should pin message', async() => {
|
||||||
await waitFor(element(by.label(`${ data.random }edited (edited)`)).atIndex(0)).toExist();
|
await waitFor(element(by.label(`${ data.random }edited (edited)`)).atIndex(0)).toExist();
|
||||||
await element(by.label(`${ data.random }edited (edited)`)).atIndex(0).longPress();
|
await element(by.label(`${ data.random }edited (edited)`)).atIndex(0).longPress();
|
||||||
await waitFor(element(by.text('Message actions'))).toExist().withTimeout(5000);
|
await expect(element(by.id('action-sheet'))).toExist();
|
||||||
await expect(element(by.text('Message actions'))).toExist();
|
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||||
await element(by.text('Pin')).tap();
|
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||||
await waitFor(element(by.text('Message actions'))).toBeNotVisible().withTimeout(5000);
|
await element(by.label('Pin')).tap();
|
||||||
await waitFor(element(by.label('Message pinned')).atIndex(0)).toExist().withTimeout(5000);
|
await waitFor(element(by.id('action-sheet'))).toNotExist().withTimeout(5000);
|
||||||
await waitFor(element(by.label(`${ data.random }edited (edited)`)).atIndex(0)).toExist().withTimeout(60000);
|
await sleep(1500);
|
||||||
|
|
||||||
|
await waitFor(element(by.label(`${ data.random }edited (edited)`)).atIndex(0)).toBeVisible();
|
||||||
await element(by.label(`${ data.random }edited (edited)`)).atIndex(0).longPress();
|
await element(by.label(`${ data.random }edited (edited)`)).atIndex(0).longPress();
|
||||||
await waitFor(element(by.text('Unpin'))).toExist().withTimeout(2000);
|
await expect(element(by.id('action-sheet'))).toExist();
|
||||||
await expect(element(by.text('Unpin'))).toExist();
|
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||||
await element(by.text('Cancel')).tap();
|
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||||
await waitFor(element(by.text('Cancel'))).toBeNotVisible().withTimeout(2000);
|
await waitFor(element(by.label('Unpin'))).toBeVisible().withTimeout(2000);
|
||||||
|
await expect(element(by.label('Unpin'))).toBeVisible();
|
||||||
|
await element(by.id('action-sheet-backdrop')).tap();
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: delete message - swipe on action sheet missing
|
it('should delete message', async() => {
|
||||||
|
await waitFor(element(by.label(`${ data.random }quoted`)).atIndex(0)).toBeVisible();
|
||||||
|
await element(by.label(`${ data.random }quoted`)).atIndex(0).longPress();
|
||||||
|
await expect(element(by.id('action-sheet'))).toExist();
|
||||||
|
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||||
|
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||||
|
await element(by.label('Delete')).tap();
|
||||||
|
|
||||||
|
const deleteAlertMessage = 'You will not be able to recover this message!';
|
||||||
|
await waitFor(element(by.text(deleteAlertMessage)).atIndex(0)).toExist().withTimeout(10000);
|
||||||
|
await expect(element(by.text(deleteAlertMessage)).atIndex(0)).toExist();
|
||||||
|
await element(by.text('Delete')).tap();
|
||||||
|
|
||||||
|
await sleep(1000);
|
||||||
|
await expect(element(by.label(`${ data.random }quoted`)).atIndex(0)).toNotExist();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Thread', async() => {
|
describe('Thread', async() => {
|
||||||
|
@ -269,9 +310,10 @@ describe('Room screen', () => {
|
||||||
it('should create thread', async() => {
|
it('should create thread', async() => {
|
||||||
await mockMessage('thread');
|
await mockMessage('thread');
|
||||||
await element(by.label(thread)).atIndex(0).longPress();
|
await element(by.label(thread)).atIndex(0).longPress();
|
||||||
await waitFor(element(by.text('Message actions'))).toExist().withTimeout(5000);
|
await expect(element(by.id('action-sheet'))).toExist();
|
||||||
await expect(element(by.text('Message actions'))).toExist();
|
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||||
await element(by.text('Reply')).tap();
|
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||||
|
await element(by.label('Reply in Thread')).tap();
|
||||||
await element(by.id('messagebox-input')).typeText('replied');
|
await element(by.id('messagebox-input')).typeText('replied');
|
||||||
await element(by.id('messagebox-send-message')).tap();
|
await element(by.id('messagebox-send-message')).tap();
|
||||||
await waitFor(element(by.id(`message-thread-button-${ thread }`))).toExist().withTimeout(5000);
|
await waitFor(element(by.id(`message-thread-button-${ thread }`))).toExist().withTimeout(5000);
|
||||||
|
@ -305,9 +347,10 @@ describe('Room screen', () => {
|
||||||
it('should navigate to thread from thread name', async() => {
|
it('should navigate to thread from thread name', async() => {
|
||||||
await mockMessage('dummymessagebetweenthethread');
|
await mockMessage('dummymessagebetweenthethread');
|
||||||
await element(by.label(thread)).atIndex(0).longPress();
|
await element(by.label(thread)).atIndex(0).longPress();
|
||||||
await waitFor(element(by.text('Message actions'))).toExist().withTimeout(5000);
|
await expect(element(by.id('action-sheet'))).toExist();
|
||||||
await expect(element(by.text('Message actions'))).toExist();
|
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||||
await element(by.text('Reply')).tap();
|
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||||
|
await element(by.label('Reply in Thread')).tap();
|
||||||
await element(by.id('messagebox-input')).typeText('repliedagain');
|
await element(by.id('messagebox-input')).typeText('repliedagain');
|
||||||
await element(by.id('messagebox-send-message')).tap();
|
await element(by.id('messagebox-send-message')).tap();
|
||||||
await waitFor(element(by.id(`message-thread-replied-on-${ thread }`))).toExist().withTimeout(5000);
|
await waitFor(element(by.id(`message-thread-replied-on-${ thread }`))).toExist().withTimeout(5000);
|
||||||
|
|
|
@ -210,12 +210,14 @@ describe('Room actions screen', () => {
|
||||||
await element(by.id('room-actions-starred')).tap();
|
await element(by.id('room-actions-starred')).tap();
|
||||||
await waitFor(element(by.id('starred-messages-view'))).toExist().withTimeout(2000);
|
await waitFor(element(by.id('starred-messages-view'))).toExist().withTimeout(2000);
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
await waitFor(element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toExist().withTimeout(60000);
|
await waitFor(element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toBeVisible().withTimeout(60000);
|
||||||
await expect(element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toExist();
|
await expect(element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toBeVisible();
|
||||||
await element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view'))).longPress();
|
await element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view'))).longPress();
|
||||||
await waitFor(element(by.text('Unstar'))).toExist().withTimeout(2000);
|
|
||||||
await expect(element(by.text('Unstar'))).toExist();
|
await expect(element(by.id('action-sheet'))).toExist();
|
||||||
await element(by.text('Unstar')).tap();
|
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||||
|
await element(by.label('Unstar')).tap();
|
||||||
|
|
||||||
await waitFor(element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toBeNotVisible().withTimeout(60000);
|
await waitFor(element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toBeNotVisible().withTimeout(60000);
|
||||||
await expect(element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toBeNotVisible();
|
await expect(element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toBeNotVisible();
|
||||||
await backToActions();
|
await backToActions();
|
||||||
|
@ -226,12 +228,14 @@ describe('Room actions screen', () => {
|
||||||
await element(by.id('room-actions-pinned')).tap();
|
await element(by.id('room-actions-pinned')).tap();
|
||||||
await waitFor(element(by.id('pinned-messages-view'))).toExist().withTimeout(2000);
|
await waitFor(element(by.id('pinned-messages-view'))).toExist().withTimeout(2000);
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
await waitFor(element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view')))).toExist().withTimeout(60000);
|
await waitFor(element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view')))).toBeVisible().withTimeout(60000);
|
||||||
await expect(element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view')))).toExist();
|
await expect(element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view')))).toBeVisible();
|
||||||
await element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view'))).longPress();
|
await element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view'))).longPress();
|
||||||
await waitFor(element(by.text('Unpin'))).toExist().withTimeout(2000);
|
|
||||||
await expect(element(by.text('Unpin'))).toExist();
|
await expect(element(by.id('action-sheet'))).toExist();
|
||||||
await element(by.text('Unpin')).tap();
|
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||||
|
await element(by.label('Unpin')).tap();
|
||||||
|
|
||||||
await waitFor(element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view')))).toBeNotVisible().withTimeout(60000);
|
await waitFor(element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view')))).toBeNotVisible().withTimeout(60000);
|
||||||
await expect(element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view')))).toBeNotVisible();
|
await expect(element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view')))).toBeNotVisible();
|
||||||
await backToActions();
|
await backToActions();
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
"@react-native-community/async-storage": "^1.9.0",
|
"@react-native-community/async-storage": "^1.9.0",
|
||||||
"@react-native-community/cameraroll": "1.6.0",
|
"@react-native-community/cameraroll": "1.6.0",
|
||||||
"@react-native-community/datetimepicker": "2.3.2",
|
"@react-native-community/datetimepicker": "2.3.2",
|
||||||
|
"@react-native-community/hooks": "^2.5.1",
|
||||||
"@react-native-community/masked-view": "^0.1.10",
|
"@react-native-community/masked-view": "^0.1.10",
|
||||||
"@react-native-community/slider": "2.0.9",
|
"@react-native-community/slider": "2.0.9",
|
||||||
"@react-navigation/drawer": "5.8.1",
|
"@react-navigation/drawer": "5.8.1",
|
||||||
|
@ -57,7 +58,6 @@
|
||||||
"prop-types": "15.7.2",
|
"prop-types": "15.7.2",
|
||||||
"react": "16.11.0",
|
"react": "16.11.0",
|
||||||
"react-native": "0.62.2",
|
"react-native": "0.62.2",
|
||||||
"react-native-action-sheet": "^2.2.0",
|
|
||||||
"react-native-animatable": "^1.3.3",
|
"react-native-animatable": "^1.3.3",
|
||||||
"react-native-appearance": "0.3.4",
|
"react-native-appearance": "0.3.4",
|
||||||
"react-native-audio": "^4.3.0",
|
"react-native-audio": "^4.3.0",
|
||||||
|
@ -93,6 +93,7 @@
|
||||||
"react-native-responsive-ui": "^1.1.1",
|
"react-native-responsive-ui": "^1.1.1",
|
||||||
"react-native-safe-area-context": "^3.0.2",
|
"react-native-safe-area-context": "^3.0.2",
|
||||||
"react-native-screens": "^2.7.0",
|
"react-native-screens": "^2.7.0",
|
||||||
|
"react-native-scroll-bottom-sheet": "0.6.1",
|
||||||
"react-native-scrollable-tab-view": "^1.0.0",
|
"react-native-scrollable-tab-view": "^1.0.0",
|
||||||
"react-native-slowlog": "^1.0.2",
|
"react-native-slowlog": "^1.0.2",
|
||||||
"react-native-unimodules": "0.9.1",
|
"react-native-unimodules": "0.9.1",
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
diff --git a/node_modules/react-native-scroll-bottom-sheet/src/index.tsx b/node_modules/react-native-scroll-bottom-sheet/src/index.tsx
|
||||||
|
index c571856..d7179c6 100644
|
||||||
|
--- a/node_modules/react-native-scroll-bottom-sheet/src/index.tsx
|
||||||
|
+++ b/node_modules/react-native-scroll-bottom-sheet/src/index.tsx
|
||||||
|
@@ -518,6 +518,28 @@ export class ScrollBottomSheet<T extends any> extends Component<Props<T>> {
|
||||||
|
clockRunning(this.animationClock)
|
||||||
|
),
|
||||||
|
[
|
||||||
|
+ this.didScrollUpAndPullDown,
|
||||||
|
+ this.setTranslationY,
|
||||||
|
+ set(this.tempDestSnapPoint, add(snapPoints[0], this.extraOffset)),
|
||||||
|
+ cond(not(this.isManuallySetValue), set(this.nextSnapIndex, 0)),
|
||||||
|
+ set(
|
||||||
|
+ this.destSnapPoint,
|
||||||
|
+ cond(
|
||||||
|
+ this.isManuallySetValue,
|
||||||
|
+ this.manualYOffset,
|
||||||
|
+ this.calculateNextSnapPoint()
|
||||||
|
+ )
|
||||||
|
+ ),
|
||||||
|
+ cond(this.isManuallySetValue, [
|
||||||
|
+ set(this.animationFinished, 0)
|
||||||
|
+ ]),
|
||||||
|
+ set(
|
||||||
|
+ this.lastSnap,
|
||||||
|
+ sub(
|
||||||
|
+ this.destSnapPoint,
|
||||||
|
+ cond(eq(this.scrollUpAndPullDown, 1), this.lastStartScrollY, 0)
|
||||||
|
+ )
|
||||||
|
+ ),
|
||||||
|
runTiming({
|
||||||
|
clock: this.animationClock,
|
||||||
|
from: cond(
|
||||||
|
@@ -550,7 +572,7 @@ export class ScrollBottomSheet<T extends any> extends Component<Props<T>> {
|
||||||
|
);
|
||||||
|
|
||||||
|
this.position = interpolate(this.translateY, {
|
||||||
|
- inputRange: [openPosition, closedPosition],
|
||||||
|
+ inputRange: [snapPoints[snapPoints.length - 2], closedPosition],
|
||||||
|
outputRange: [1, 0],
|
||||||
|
extrapolate: Extrapolate.CLAMP,
|
||||||
|
});
|
22
yarn.lock
22
yarn.lock
|
@ -1749,6 +1749,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
invariant "^2.2.4"
|
invariant "^2.2.4"
|
||||||
|
|
||||||
|
"@react-native-community/hooks@^2.5.1":
|
||||||
|
version "2.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@react-native-community/hooks/-/hooks-2.5.1.tgz#545c76d1a6203532a8e776578bbaaa64bb754cf6"
|
||||||
|
integrity sha512-P9gwIUGpa/h8p5ASwY8QFTthXw/e/rt4mzZRfe3Xh5L13mTuOFXsYVwe9f8JAUx512cUKUsdTg6Dsg3/jTlxeg==
|
||||||
|
|
||||||
"@react-native-community/masked-view@^0.1.10":
|
"@react-native-community/masked-view@^0.1.10":
|
||||||
version "0.1.10"
|
version "0.1.10"
|
||||||
resolved "https://registry.yarnpkg.com/@react-native-community/masked-view/-/masked-view-0.1.10.tgz#5dda643e19e587793bc2034dd9bf7398ad43d401"
|
resolved "https://registry.yarnpkg.com/@react-native-community/masked-view/-/masked-view-0.1.10.tgz#5dda643e19e587793bc2034dd9bf7398ad43d401"
|
||||||
|
@ -11720,11 +11725,6 @@ react-lifecycles-compat@^3.0.4:
|
||||||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||||
|
|
||||||
react-native-action-sheet@^2.2.0:
|
|
||||||
version "2.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-native-action-sheet/-/react-native-action-sheet-2.2.0.tgz#309a87f53bf4e7b17fdd9d24b10b8dcbaebb7230"
|
|
||||||
integrity sha512-4lsuxH+Cn3/aUEs1VCwqvLhEFyXNqYTkT67CzgTwlifD9Ij4OPQAIs8D+HUD9zBvWc4NtT6cyG1lhArPVMQeVw==
|
|
||||||
|
|
||||||
react-native-animatable@1.3.3, react-native-animatable@^1.3.3:
|
react-native-animatable@1.3.3, react-native-animatable@^1.3.3:
|
||||||
version "1.3.3"
|
version "1.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-animatable/-/react-native-animatable-1.3.3.tgz#a13a4af8258e3bb14d0a9d839917e9bb9274ec8a"
|
resolved "https://registry.yarnpkg.com/react-native-animatable/-/react-native-animatable-1.3.3.tgz#a13a4af8258e3bb14d0a9d839917e9bb9274ec8a"
|
||||||
|
@ -11963,6 +11963,13 @@ react-native-screens@^2.7.0:
|
||||||
resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-2.7.0.tgz#2d3cf3c39a665e9ca1c774264fccdb90e7944047"
|
resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-2.7.0.tgz#2d3cf3c39a665e9ca1c774264fccdb90e7944047"
|
||||||
integrity sha512-n/23IBOkrTKCfuUd6tFeRkn3lB2QZ3cmvoubRscR0JU/Zl4/ZyKmwnFmUv1/Fr+2GH/H8UTX59kEKDYYg3dMgA==
|
integrity sha512-n/23IBOkrTKCfuUd6tFeRkn3lB2QZ3cmvoubRscR0JU/Zl4/ZyKmwnFmUv1/Fr+2GH/H8UTX59kEKDYYg3dMgA==
|
||||||
|
|
||||||
|
react-native-scroll-bottom-sheet@0.6.1:
|
||||||
|
version "0.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-scroll-bottom-sheet/-/react-native-scroll-bottom-sheet-0.6.1.tgz#7fa6a4f1104417e4e9bf4b10efffc46d501aeeb4"
|
||||||
|
integrity sha512-Glws8msLrbKDW5a53rCeN0pLNI41Yhvz7K7OWZnaVYLs3GPTP2ySYdJ849rd/d5P1P0xqFyKEF/0p+/KLI0nYA==
|
||||||
|
dependencies:
|
||||||
|
utility-types "^3.10.0"
|
||||||
|
|
||||||
react-native-scrollable-tab-view@^1.0.0:
|
react-native-scrollable-tab-view@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-scrollable-tab-view/-/react-native-scrollable-tab-view-1.0.0.tgz#87319896067f7bb643ecd7fba2cba4d6d8f9e18b"
|
resolved "https://registry.yarnpkg.com/react-native-scrollable-tab-view/-/react-native-scrollable-tab-view-1.0.0.tgz#87319896067f7bb643ecd7fba2cba4d6d8f9e18b"
|
||||||
|
@ -14395,6 +14402,11 @@ utila@^0.4.0, utila@~0.4:
|
||||||
resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
|
resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
|
||||||
integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=
|
integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=
|
||||||
|
|
||||||
|
utility-types@^3.10.0:
|
||||||
|
version "3.10.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b"
|
||||||
|
integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==
|
||||||
|
|
||||||
utils-merge@1.0.1:
|
utils-merge@1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||||
|
|
Loading…
Reference in New Issue