[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 {
|
||||
ROOT_LOADING, ROOT_OUTSIDE, ROOT_NEW_SERVER, ROOT_INSIDE, ROOT_SET_USERNAME, ROOT_BACKGROUND
|
||||
} from './actions/app';
|
||||
import { ActionSheetProvider } from './containers/ActionSheet';
|
||||
|
||||
// Stacks
|
||||
import AuthLoadingView from './views/AuthLoadingView';
|
||||
|
@ -53,6 +54,7 @@ const App = React.memo(({ root, isMasterDetail }) => {
|
|||
|
||||
return (
|
||||
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
||||
<ActionSheetProvider>
|
||||
<NavigationContainer
|
||||
theme={navTheme}
|
||||
ref={Navigation.navigationRef}
|
||||
|
@ -100,6 +102,7 @@ const App = React.memo(({ root, isMasterDetail }) => {
|
|||
</>
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
</ActionSheetProvider>
|
||||
</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 moment from 'moment';
|
||||
import { connect } from 'react-redux';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
import Markdown from '../markdown';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
|
@ -58,7 +59,7 @@ const ReplyPreview = React.memo(({
|
|||
>
|
||||
<View style={[styles.messageContainer, { backgroundColor: themes[theme].chatComponentBackground }]}>
|
||||
<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>
|
||||
</View>
|
||||
<Markdown
|
||||
|
@ -74,7 +75,7 @@ const ReplyPreview = React.memo(({
|
|||
<CustomIcon name='Cross' color={themes[theme].auxiliaryText} size={20} style={styles.close} onPress={close} />
|
||||
</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 = {
|
||||
replying: PropTypes.bool,
|
||||
|
|
|
@ -6,7 +6,6 @@ import { KeyboardAccessoryView } from 'react-native-keyboard-input';
|
|||
import ImagePicker from 'react-native-image-crop-picker';
|
||||
import equal from 'deep-equal';
|
||||
import DocumentPicker from 'react-native-document-picker';
|
||||
import ActionSheet from 'react-native-action-sheet';
|
||||
import { Q } from '@nozbe/watermelondb';
|
||||
|
||||
import { generateTriggerId } from '../../lib/methods/actions';
|
||||
|
@ -46,6 +45,7 @@ import CommandsPreview from './CommandsPreview';
|
|||
import { Review } from '../../utils/review';
|
||||
import { getUserSelector } from '../../selectors/login';
|
||||
import Navigation from '../../lib/Navigation';
|
||||
import { withActionSheet } from '../ActionSheet';
|
||||
|
||||
const imagePickerConfig = {
|
||||
cropping: true,
|
||||
|
@ -61,13 +61,6 @@ const videoPickerConfig = {
|
|||
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 {
|
||||
static propTypes = {
|
||||
rid: PropTypes.string.isRequired,
|
||||
|
@ -96,7 +89,8 @@ class MessageBox extends Component {
|
|||
theme: PropTypes.string,
|
||||
replyCancel: PropTypes.func,
|
||||
isMasterDetail: PropTypes.bool,
|
||||
navigation: PropTypes.object
|
||||
navigation: PropTypes.object,
|
||||
showActionSheet: PropTypes.func
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -116,14 +110,36 @@ class MessageBox extends Component {
|
|||
};
|
||||
this.text = '';
|
||||
this.focused = false;
|
||||
this.messageBoxActions = [
|
||||
I18n.t('Cancel'),
|
||||
I18n.t('Take_a_photo'),
|
||||
I18n.t('Take_a_video'),
|
||||
I18n.t('Choose_from_library'),
|
||||
I18n.t('Choose_file'),
|
||||
I18n.t('Create_Discussion')
|
||||
|
||||
// MessageBox Actions
|
||||
this.options = [
|
||||
{
|
||||
title: I18n.t('Take_a_photo'),
|
||||
icon: 'image',
|
||||
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 = {
|
||||
cropperChooseText: I18n.t('Choose'),
|
||||
cropperCancelText: I18n.t('Cancel'),
|
||||
|
@ -204,6 +220,7 @@ class MessageBox extends Component {
|
|||
if (this.text) {
|
||||
this.setShowSend(true);
|
||||
}
|
||||
this.focus();
|
||||
} else if (replying !== nextProps.replying && nextProps.replying) {
|
||||
this.focus();
|
||||
} else if (!nextProps.message) {
|
||||
|
@ -217,7 +234,7 @@ class MessageBox extends Component {
|
|||
} = this.state;
|
||||
|
||||
const {
|
||||
roomType, replying, editing, isFocused, theme
|
||||
roomType, replying, editing, isFocused, message, theme
|
||||
} = this.props;
|
||||
if (nextProps.theme !== theme) {
|
||||
return true;
|
||||
|
@ -252,6 +269,9 @@ class MessageBox extends Component {
|
|||
if (!equal(nextState.file, file)) {
|
||||
return true;
|
||||
}
|
||||
if (!equal(nextProps.message, message)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -613,34 +633,8 @@ class MessageBox extends Component {
|
|||
}
|
||||
|
||||
showMessageBoxActions = () => {
|
||||
ActionSheet.showActionSheetWithOptions({
|
||||
options: this.messageBoxActions,
|
||||
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;
|
||||
}
|
||||
const { showActionSheet } = this.props;
|
||||
showActionSheet({ options: this.options });
|
||||
}
|
||||
|
||||
editCancel = () => {
|
||||
|
@ -939,4 +933,4 @@ const dispatchToProps = ({
|
|||
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 ActionSheet from 'react-native-action-sheet';
|
||||
|
||||
import RocketChat from '../lib/rocketchat';
|
||||
import database from '../lib/database';
|
||||
import protectedFunction from '../lib/methods/helpers/protectedFunction';
|
||||
import { useActionSheet } from './ActionSheet';
|
||||
import I18n from '../i18n';
|
||||
import log from '../utils/log';
|
||||
|
||||
class MessageErrorActions extends React.Component {
|
||||
static propTypes = {
|
||||
actionsHide: PropTypes.func.isRequired,
|
||||
message: PropTypes.object,
|
||||
tmid: PropTypes.string
|
||||
};
|
||||
const MessageErrorActions = forwardRef(({ tmid }, ref) => {
|
||||
const { showActionSheet } = useActionSheet();
|
||||
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
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;
|
||||
const handleResend = protectedFunction(async(message) => {
|
||||
await RocketChat.resendMessage(message, tmid);
|
||||
});
|
||||
|
||||
handleDelete = async() => {
|
||||
const handleDelete = async(message) => {
|
||||
try {
|
||||
const { message, tmid } = this.props;
|
||||
const db = database.active;
|
||||
const deleteBatch = [];
|
||||
const msgCollection = db.collections.get('messages');
|
||||
|
@ -49,7 +30,7 @@ class MessageErrorActions extends React.Component {
|
|||
try {
|
||||
const msg = await msgCollection.find(message.id);
|
||||
deleteBatch.push(msg.prepareDestroyPermanently());
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Do nothing: message not found
|
||||
}
|
||||
|
||||
|
@ -68,7 +49,7 @@ class MessageErrorActions extends React.Component {
|
|||
// If the whole thread was removed, delete the thread
|
||||
const thread = await threadCollection.find(tmid);
|
||||
deleteBatch.push(thread.prepareDestroyPermanently());
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Do nothing: thread not found
|
||||
}
|
||||
} else {
|
||||
|
@ -78,7 +59,7 @@ class MessageErrorActions extends React.Component {
|
|||
})
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Do nothing: message not found
|
||||
}
|
||||
}
|
||||
|
@ -88,39 +69,34 @@ class MessageErrorActions extends React.Component {
|
|||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
showActionSheet = () => {
|
||||
ActionSheet.showActionSheetWithOptions({
|
||||
options: this.options,
|
||||
cancelButtonIndex: this.CANCEL_INDEX,
|
||||
destructiveButtonIndex: this.DELETE_INDEX,
|
||||
title: I18n.t('Message_actions')
|
||||
}, (actionIndex) => {
|
||||
this.handleActionPress(actionIndex);
|
||||
const showMessageErrorActions = (message) => {
|
||||
showActionSheet({
|
||||
options: [
|
||||
{
|
||||
title: I18n.t('Resend'),
|
||||
icon: 'send',
|
||||
onPress: () => handleResend(message)
|
||||
},
|
||||
{
|
||||
title: I18n.t('Delete'),
|
||||
icon: 'trash',
|
||||
danger: true,
|
||||
onPress: () => handleDelete(message)
|
||||
}
|
||||
],
|
||||
hasCancel: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleActionPress = (actionIndex) => {
|
||||
const { actionsHide } = this.props;
|
||||
switch (actionIndex) {
|
||||
case this.RESEND_INDEX:
|
||||
this.handleResend();
|
||||
break;
|
||||
case this.DELETE_INDEX:
|
||||
this.handleDelete();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
actionsHide();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
showMessageErrorActions
|
||||
}));
|
||||
});
|
||||
MessageErrorActions.propTypes = {
|
||||
message: PropTypes.object,
|
||||
tmid: PropTypes.string
|
||||
};
|
||||
|
||||
export default MessageErrorActions;
|
||||
|
|
|
@ -405,6 +405,7 @@ export default {
|
|||
Review_app_later: 'Maybe later',
|
||||
Review_app_unable_store: 'Unable to open {{store}}',
|
||||
Review_this_app: 'Review this app',
|
||||
Remove: 'Remove',
|
||||
Roles: 'Roles',
|
||||
Room_actions: 'Room actions',
|
||||
Room_changed_announcement: 'Room announcement changed to: {{announcement}} by {{userBy}}',
|
||||
|
@ -470,6 +471,7 @@ export default {
|
|||
starred: 'starred',
|
||||
Starred: 'Starred',
|
||||
Start_of_conversation: 'Start of conversation',
|
||||
Start_a_Discussion: 'Start a Discussion',
|
||||
Started_discussion: 'Started a discussion:',
|
||||
Started_call: 'Call started by {{userBy}}',
|
||||
Submit: 'Submit',
|
||||
|
@ -482,6 +484,8 @@ export default {
|
|||
Terms_of_Service: ' Terms of Service ',
|
||||
Theme: 'Theme',
|
||||
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}}!',
|
||||
This_room_is_blocked: 'This room is blocked',
|
||||
This_room_is_read_only: 'This room is read only',
|
||||
|
@ -566,6 +570,7 @@ export default {
|
|||
Your_workspace: 'Your workspace',
|
||||
Version_no: 'Version: {{version}}',
|
||||
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',
|
||||
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',
|
||||
|
@ -578,6 +583,7 @@ export default {
|
|||
Search_messages: 'Search messages',
|
||||
Scroll_messages: 'Scroll messages',
|
||||
Reply_latest: 'Reply to latest',
|
||||
Reply_in_Thread: 'Reply in Thread',
|
||||
Server_selection: 'Server selection',
|
||||
Server_selection_numbers: 'Server selection 1...9',
|
||||
Add_server: 'Add server',
|
||||
|
|
|
@ -365,6 +365,7 @@ export default {
|
|||
Review_app_later: 'Talvez depois',
|
||||
Review_app_unable_store: 'Não foi possível abrir {{store}}',
|
||||
Review_this_app: 'Avaliar esse app',
|
||||
Remove: 'Remover',
|
||||
Roles: 'Papéis',
|
||||
Room_actions: 'Ações',
|
||||
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',
|
||||
Terms_of_Service: ' Termos de Serviço ',
|
||||
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}}',
|
||||
There_was_an_error_while_action: 'Aconteceu um erro {{action}}!',
|
||||
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_workspace: 'Sua workspace',
|
||||
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?',
|
||||
Write_External_Permission_Message: 'Rocket Chat precisa de acesso à sua galeria para salvar imagens',
|
||||
Write_External_Permission: 'Acesso à Galeria',
|
||||
|
|
|
@ -12,3 +12,5 @@ export function withTheme(Component) {
|
|||
hoistNonReactStatics(ThemedComponent, Component);
|
||||
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 { connect } from 'react-redux';
|
||||
import equal from 'deep-equal';
|
||||
import ActionSheet from 'react-native-action-sheet';
|
||||
|
||||
import styles from './styles';
|
||||
import Message from '../../containers/message';
|
||||
|
@ -15,11 +14,9 @@ import getFileUrlFromMessage from '../../lib/methods/helpers/getFileUrlFromMessa
|
|||
import { themes } from '../../constants/colors';
|
||||
import { withTheme } from '../../theme';
|
||||
import { getUserSelector } from '../../selectors/login';
|
||||
import { withActionSheet } from '../../containers/ActionSheet';
|
||||
import SafeAreaView from '../../containers/SafeAreaView';
|
||||
|
||||
const ACTION_INDEX = 0;
|
||||
const CANCEL_INDEX = 1;
|
||||
|
||||
class MessagesView extends React.Component {
|
||||
static navigationOptions = ({ route }) => ({
|
||||
title: I18n.t(route.params?.name)
|
||||
|
@ -31,7 +28,8 @@ class MessagesView extends React.Component {
|
|||
navigation: PropTypes.object,
|
||||
route: PropTypes.object,
|
||||
customEmojis: PropTypes.object,
|
||||
theme: PropTypes.string
|
||||
theme: PropTypes.string,
|
||||
showActionSheet: PropTypes.func
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -162,7 +160,7 @@ class MessagesView extends React.Component {
|
|||
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)
|
||||
},
|
||||
// Pinned Messages Screen
|
||||
|
@ -179,7 +177,7 @@ class MessagesView extends React.Component {
|
|||
theme={theme}
|
||||
/>
|
||||
),
|
||||
actionTitle: I18n.t('Unpin'),
|
||||
action: () => ({ title: I18n.t('Unpin'), icon: 'pin', onPress: this.handleActionPress }),
|
||||
handleActionPress: message => RocketChat.togglePinMessage(message._id, message.pinned)
|
||||
}
|
||||
}[name]);
|
||||
|
@ -225,22 +223,16 @@ class MessagesView extends React.Component {
|
|||
}
|
||||
|
||||
onLongPress = (message) => {
|
||||
this.setState({ message });
|
||||
this.showActionSheet();
|
||||
this.setState({ message }, this.showActionSheet);
|
||||
}
|
||||
|
||||
showActionSheet = () => {
|
||||
ActionSheet.showActionSheetWithOptions({
|
||||
options: [this.content.actionTitle, I18n.t('Cancel')],
|
||||
cancelButtonIndex: CANCEL_INDEX,
|
||||
title: I18n.t('Actions')
|
||||
}, (actionIndex) => {
|
||||
this.handleActionPress(actionIndex);
|
||||
});
|
||||
const { message } = this.state;
|
||||
const { showActionSheet } = this.props;
|
||||
showActionSheet({ options: [this.content.action(message)], hasCancel: true });
|
||||
}
|
||||
|
||||
handleActionPress = async(actionIndex) => {
|
||||
if (actionIndex === ACTION_INDEX) {
|
||||
handleActionPress = async() => {
|
||||
const { message } = this.state;
|
||||
|
||||
try {
|
||||
|
@ -251,9 +243,8 @@ class MessagesView extends React.Component {
|
|||
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
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(withTheme(MessagesView));
|
||||
export default connect(mapStateToProps)(withTheme(withActionSheet(MessagesView)));
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
import { connect } from 'react-redux';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import DocumentPicker from 'react-native-document-picker';
|
||||
import ActionSheet from 'react-native-action-sheet';
|
||||
import RNUserDefaults from 'rn-user-defaults';
|
||||
import { encode } from 'base-64';
|
||||
import parse from 'url-parse';
|
||||
|
@ -26,6 +25,7 @@ import { animateNextTransition } from '../utils/layoutAnimation';
|
|||
import { withTheme } from '../theme';
|
||||
import { setBasicAuth, BASIC_AUTH_KEY } from '../utils/fetch';
|
||||
import { CloseModalButton } from '../containers/HeaderButton';
|
||||
import { showConfirmationAlert } from '../utils/info';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
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 = {
|
||||
text: '',
|
||||
connectingOpen: false,
|
||||
|
@ -233,15 +225,11 @@ class NewServerView extends React.Component {
|
|||
this.setState({ certificate });
|
||||
}
|
||||
|
||||
handleDelete = () => this.setState({ certificate: null }); // We not need delete file from DocumentPicker because it is a temp file
|
||||
|
||||
showActionSheet = () => {
|
||||
ActionSheet.showActionSheetWithOptions({
|
||||
options: this.options,
|
||||
cancelButtonIndex: this.CANCEL_INDEX,
|
||||
destructiveButtonIndex: this.DELETE_INDEX
|
||||
}, (actionIndex) => {
|
||||
if (actionIndex === this.DELETE_INDEX) { this.handleDelete(); }
|
||||
handleRemove = () => {
|
||||
showConfirmationAlert({
|
||||
message: I18n.t('You_will_unset_a_certificate_for_this_server'),
|
||||
callToAction: I18n.t('Remove'),
|
||||
onPress: this.setState({ certificate: null }) // We not need delete file from DocumentPicker because it is a temp file
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -259,7 +247,7 @@ class NewServerView extends React.Component {
|
|||
{certificate ? I18n.t('Your_certificate') : I18n.t('Do_you_have_a_certificate')}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={certificate ? this.showActionSheet : this.chooseCertificate}
|
||||
onPress={certificate ? this.handleRemove : this.chooseCertificate}
|
||||
testID='new-server-choose-certificate'
|
||||
>
|
||||
<Text
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FlatList, View } from 'react-native';
|
||||
import ActionSheet from 'react-native-action-sheet';
|
||||
import { connect } from 'react-redux';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { Q } from '@nozbe/watermelondb';
|
||||
|
||||
import styles from './styles';
|
||||
|
@ -23,6 +21,8 @@ import ActivityIndicator from '../../containers/ActivityIndicator';
|
|||
import { withTheme } from '../../theme';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { getUserSelector } from '../../selectors/login';
|
||||
import { withActionSheet } from '../../containers/ActionSheet';
|
||||
import { showConfirmationAlert } from '../../utils/info';
|
||||
import SafeAreaView from '../../containers/SafeAreaView';
|
||||
import { goRoom } from '../../utils/goRoom';
|
||||
|
||||
|
@ -40,6 +40,7 @@ class RoomMembersView extends React.Component {
|
|||
id: PropTypes.string,
|
||||
token: PropTypes.string
|
||||
}),
|
||||
showActionSheet: PropTypes.func,
|
||||
theme: PropTypes.string,
|
||||
isMasterDetail: PropTypes.bool
|
||||
}
|
||||
|
@ -47,9 +48,7 @@ class RoomMembersView extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this.mounted = false;
|
||||
this.CANCEL_INDEX = 0;
|
||||
this.MUTE_INDEX = 1;
|
||||
this.actionSheetOptions = [''];
|
||||
this.MUTE_INDEX = 0;
|
||||
const rid = props.route.params?.rid;
|
||||
const room = props.route.params?.room;
|
||||
this.state = {
|
||||
|
@ -59,7 +58,6 @@ class RoomMembersView extends React.Component {
|
|||
rid,
|
||||
members: [],
|
||||
membersFiltered: [],
|
||||
userLongPressed: {},
|
||||
room: room || {},
|
||||
end: false
|
||||
};
|
||||
|
@ -140,19 +138,28 @@ class RoomMembersView extends React.Component {
|
|||
return;
|
||||
}
|
||||
const { room } = this.state;
|
||||
const { showActionSheet } = this.props;
|
||||
const { muted } = room;
|
||||
|
||||
this.actionSheetOptions = [I18n.t('Cancel')];
|
||||
const userIsMuted = !!(muted || []).find(m => m === user.username);
|
||||
user.muted = userIsMuted;
|
||||
if (userIsMuted) {
|
||||
this.actionSheetOptions.push(I18n.t('Unmute'));
|
||||
} else {
|
||||
this.actionSheetOptions.push(I18n.t('Mute'));
|
||||
|
||||
showActionSheet({
|
||||
options: [{
|
||||
icon: userIsMuted ? 'volume' : 'volume-off',
|
||||
title: I18n.t(userIsMuted ? 'Unmute' : 'Mute'),
|
||||
onPress: () => {
|
||||
showConfirmationAlert({
|
||||
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)
|
||||
});
|
||||
}
|
||||
this.setState({ userLongPressed: user });
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
this.showActionSheet();
|
||||
}],
|
||||
hasCancel: true
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
fetchMembers = async() => {
|
||||
const {
|
||||
|
@ -211,26 +208,16 @@ class RoomMembersView extends React.Component {
|
|||
goRoom({ item, isMasterDetail });
|
||||
}
|
||||
|
||||
handleMute = async() => {
|
||||
const { rid, userLongPressed } = this.state;
|
||||
handleMute = async(user) => {
|
||||
const { rid } = this.state;
|
||||
try {
|
||||
await RocketChat.toggleMuteUserInRoom(rid, userLongPressed.username, !userLongPressed.muted);
|
||||
EventEmitter.emit(LISTENER, { message: I18n.t('User_has_been_key', { key: userLongPressed.muted ? I18n.t('unmuted') : I18n.t('muted') }) });
|
||||
await RocketChat.toggleMuteUserInRoom(rid, user?.username, !user?.muted);
|
||||
EventEmitter.emit(LISTENER, { message: I18n.t('User_has_been_key', { key: user?.muted ? I18n.t('unmuted') : I18n.t('muted') }) });
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
handleActionPress = (actionIndex) => {
|
||||
switch (actionIndex) {
|
||||
case this.MUTE_INDEX:
|
||||
this.handleMute();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
renderSearchBar = () => (
|
||||
<SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='room-members-view-search' />
|
||||
)
|
||||
|
@ -295,4 +282,4 @@ const mapStateToProps = state => ({
|
|||
isMasterDetail: state.app.isMasterDetail
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(withTheme(RoomMembersView));
|
||||
export default connect(mapStateToProps)(withActionSheet(withTheme(RoomMembersView)));
|
||||
|
|
|
@ -58,8 +58,7 @@ const stateAttrsUpdate = [
|
|||
'lastOpen',
|
||||
'reactionsModalVisible',
|
||||
'canAutoTranslate',
|
||||
'showActions',
|
||||
'showErrorActions',
|
||||
'selectedMessage',
|
||||
'loading',
|
||||
'editing',
|
||||
'replying',
|
||||
|
@ -117,8 +116,6 @@ class RoomView extends React.Component {
|
|||
selectedMessage: selectedMessage || {},
|
||||
canAutoTranslate: false,
|
||||
loading: true,
|
||||
showActions: false,
|
||||
showErrorActions: false,
|
||||
editing: false,
|
||||
replying: !!selectedMessage,
|
||||
replyWithMention: false,
|
||||
|
@ -501,23 +498,11 @@ class RoomView extends React.Component {
|
|||
}
|
||||
|
||||
errorActionsShow = (message) => {
|
||||
this.setState({ selectedMessage: message, showErrorActions: true });
|
||||
}
|
||||
|
||||
onActionsHide = () => {
|
||||
const { editing, replying, reacting } = this.state;
|
||||
if (editing || replying || reacting) {
|
||||
return;
|
||||
}
|
||||
this.setState({ selectedMessage: {}, showActions: false });
|
||||
}
|
||||
|
||||
onErrorActionsHide = () => {
|
||||
this.setState({ selectedMessage: {}, showErrorActions: false });
|
||||
this.messageErrorActions?.showMessageErrorActions(message);
|
||||
}
|
||||
|
||||
onEditInit = (message) => {
|
||||
this.setState({ selectedMessage: message, editing: true, showActions: false });
|
||||
this.setState({ selectedMessage: message, editing: true });
|
||||
}
|
||||
|
||||
onEditCancel = () => {
|
||||
|
@ -535,7 +520,7 @@ class RoomView extends React.Component {
|
|||
|
||||
onReplyInit = (message, mention) => {
|
||||
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) => {
|
||||
this.setState({ selectedMessage: message, reacting: true, showActions: false });
|
||||
this.setState({ selectedMessage: message, reacting: true });
|
||||
}
|
||||
|
||||
onReactionClose = () => {
|
||||
|
@ -552,7 +537,7 @@ class RoomView extends React.Component {
|
|||
}
|
||||
|
||||
onMessageLongPress = (message) => {
|
||||
this.setState({ selectedMessage: message, showActions: true });
|
||||
this.messageActions?.showMessageActions(message);
|
||||
}
|
||||
|
||||
showAttachment = (attachment) => {
|
||||
|
@ -942,9 +927,7 @@ class RoomView extends React.Component {
|
|||
};
|
||||
|
||||
renderActions = () => {
|
||||
const {
|
||||
room, selectedMessage, showActions, showErrorActions, joined, readOnly
|
||||
} = this.state;
|
||||
const { room, readOnly } = this.state;
|
||||
const {
|
||||
user, navigation
|
||||
} = this.props;
|
||||
|
@ -953,29 +936,21 @@ class RoomView extends React.Component {
|
|||
}
|
||||
return (
|
||||
<>
|
||||
{joined && showActions
|
||||
? (
|
||||
<MessageActions
|
||||
ref={ref => this.messageActions = ref}
|
||||
tmid={this.tmid}
|
||||
room={room}
|
||||
user={user}
|
||||
message={selectedMessage}
|
||||
actionsHide={this.onActionsHide}
|
||||
editInit={this.onEditInit}
|
||||
replyInit={this.onReplyInit}
|
||||
reactionInit={this.onReactionInit}
|
||||
onReactionPress={this.onReactionPress}
|
||||
isReadOnly={readOnly}
|
||||
/>
|
||||
)
|
||||
: null
|
||||
}
|
||||
{showErrorActions ? (
|
||||
<MessageErrorActions
|
||||
ref={ref => this.messageErrorActions = ref}
|
||||
tmid={this.tmid}
|
||||
message={selectedMessage}
|
||||
actionsHide={this.onErrorActionsHide}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -151,12 +151,11 @@ describe('Room screen', () => {
|
|||
|
||||
describe('Message', 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 waitFor(element(by.text('Message actions'))).toExist().withTimeout(5000);
|
||||
await expect(element(by.text('Message actions'))).toExist();
|
||||
await element(by.text('Permalink')).tap();
|
||||
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('Permalink')).tap();
|
||||
await sleep(1000);
|
||||
|
||||
// TODO: test clipboard
|
||||
|
@ -164,35 +163,42 @@ describe('Room screen', () => {
|
|||
|
||||
it('should copy message', async() => {
|
||||
await element(by.label(`${ data.random }message`)).atIndex(0).longPress();
|
||||
await waitFor(element(by.text('Message actions'))).toExist().withTimeout(5000);
|
||||
await expect(element(by.text('Message actions'))).toExist();
|
||||
await element(by.text('Copy')).tap();
|
||||
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('Copy')).tap();
|
||||
await sleep(1000);
|
||||
|
||||
// TODO: test clipboard
|
||||
});
|
||||
|
||||
it('should star message', async() => {
|
||||
await element(by.label(`${ data.random }message`)).atIndex(0).longPress();
|
||||
await waitFor(element(by.text('Message actions'))).toExist().withTimeout(5000);
|
||||
await expect(element(by.text('Message actions'))).toExist();
|
||||
await element(by.text('Star')).tap();
|
||||
await sleep(2000);
|
||||
await waitFor(element(by.text('Message actions'))).toBeNotVisible().withTimeout(5000);
|
||||
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('Star')).tap();
|
||||
await sleep(1000);
|
||||
await waitFor(element(by.id('action-sheet'))).toNotExist().withTimeout(5000);
|
||||
|
||||
await element(by.label(`${ data.random }message`)).atIndex(0).longPress();
|
||||
await waitFor(element(by.text('Unstar'))).toExist().withTimeout(2000);
|
||||
await expect(element(by.text('Unstar'))).toExist();
|
||||
await element(by.text('Cancel')).tap();
|
||||
await waitFor(element(by.text('Cancel'))).toBeNotVisible().withTimeout(2000);
|
||||
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.label('Unstar'))).toBeVisible().withTimeout(2000);
|
||||
await expect(element(by.label('Unstar'))).toBeVisible();
|
||||
await element(by.id('action-sheet-backdrop')).tap();
|
||||
await sleep(1000);
|
||||
});
|
||||
|
||||
it('should react to message', async() => {
|
||||
await element(by.label(`${ data.random }message`)).atIndex(0).longPress();
|
||||
await waitFor(element(by.text('Message actions'))).toExist().withTimeout(5000);
|
||||
await expect(element(by.text('Message actions'))).toExist();
|
||||
await element(by.text('Add Reaction')).tap();
|
||||
await waitFor(element(by.id('reaction-picker'))).toExist().withTimeout(2000);
|
||||
await expect(element(by.id('reaction-picker'))).toExist();
|
||||
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.id('add-reaction')).tap();
|
||||
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 waitFor(element(by.id('reaction-picker-grinning'))).toExist().withTimeout(2000);
|
||||
await expect(element(by.id('reaction-picker-grinning'))).toExist();
|
||||
|
@ -202,6 +208,19 @@ describe('Room screen', () => {
|
|||
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() => {
|
||||
await element(by.id('message-add-reaction')).tap();
|
||||
await waitFor(element(by.id('reaction-picker'))).toExist().withTimeout(2000);
|
||||
|
@ -224,9 +243,10 @@ describe('Room screen', () => {
|
|||
it('should edit message', async() => {
|
||||
await mockMessage('edit');
|
||||
await element(by.label(`${ data.random }edit`)).atIndex(0).longPress();
|
||||
await waitFor(element(by.text('Message actions'))).toExist().withTimeout(5000);
|
||||
await expect(element(by.text('Message actions'))).toExist();
|
||||
await element(by.text('Edit')).tap();
|
||||
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('Edit')).tap();
|
||||
await element(by.id('messagebox-input')).typeText('ed');
|
||||
await element(by.id('messagebox-send-message')).tap();
|
||||
await waitFor(element(by.label(`${ data.random }edited (edited)`)).atIndex(0)).toExist().withTimeout(60000);
|
||||
|
@ -236,32 +256,53 @@ describe('Room screen', () => {
|
|||
it('should quote message', async() => {
|
||||
await mockMessage('quote');
|
||||
await element(by.label(`${ data.random }quote`)).atIndex(0).longPress();
|
||||
await waitFor(element(by.text('Message actions'))).toExist().withTimeout(5000);
|
||||
await expect(element(by.text('Message actions'))).toExist();
|
||||
await element(by.text('Quote')).tap();
|
||||
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('Quote')).tap();
|
||||
await element(by.id('messagebox-input')).typeText(`${ data.random }quoted`);
|
||||
await element(by.id('messagebox-send-message')).tap();
|
||||
await sleep(1000);
|
||||
|
||||
// TODO: test if quote was sent
|
||||
await sleep(2000);
|
||||
});
|
||||
|
||||
it('should pin message', async() => {
|
||||
await waitFor(element(by.label(`${ data.random }edited (edited)`)).atIndex(0)).toExist();
|
||||
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.text('Message actions'))).toExist();
|
||||
await element(by.text('Pin')).tap();
|
||||
await waitFor(element(by.text('Message actions'))).toBeNotVisible().withTimeout(5000);
|
||||
await waitFor(element(by.label('Message pinned')).atIndex(0)).toExist().withTimeout(5000);
|
||||
await waitFor(element(by.label(`${ data.random }edited (edited)`)).atIndex(0)).toExist().withTimeout(60000);
|
||||
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('Pin')).tap();
|
||||
await waitFor(element(by.id('action-sheet'))).toNotExist().withTimeout(5000);
|
||||
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 waitFor(element(by.text('Unpin'))).toExist().withTimeout(2000);
|
||||
await expect(element(by.text('Unpin'))).toExist();
|
||||
await element(by.text('Cancel')).tap();
|
||||
await waitFor(element(by.text('Cancel'))).toBeNotVisible().withTimeout(2000);
|
||||
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.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() => {
|
||||
|
@ -269,9 +310,10 @@ describe('Room screen', () => {
|
|||
it('should create thread', async() => {
|
||||
await mockMessage('thread');
|
||||
await element(by.label(thread)).atIndex(0).longPress();
|
||||
await waitFor(element(by.text('Message actions'))).toExist().withTimeout(5000);
|
||||
await expect(element(by.text('Message actions'))).toExist();
|
||||
await element(by.text('Reply')).tap();
|
||||
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('Reply in Thread')).tap();
|
||||
await element(by.id('messagebox-input')).typeText('replied');
|
||||
await element(by.id('messagebox-send-message')).tap();
|
||||
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() => {
|
||||
await mockMessage('dummymessagebetweenthethread');
|
||||
await element(by.label(thread)).atIndex(0).longPress();
|
||||
await waitFor(element(by.text('Message actions'))).toExist().withTimeout(5000);
|
||||
await expect(element(by.text('Message actions'))).toExist();
|
||||
await element(by.text('Reply')).tap();
|
||||
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('Reply in Thread')).tap();
|
||||
await element(by.id('messagebox-input')).typeText('repliedagain');
|
||||
await element(by.id('messagebox-send-message')).tap();
|
||||
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 waitFor(element(by.id('starred-messages-view'))).toExist().withTimeout(2000);
|
||||
await sleep(1000);
|
||||
await waitFor(element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toExist().withTimeout(60000);
|
||||
await expect(element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toExist();
|
||||
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')))).toBeVisible();
|
||||
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 element(by.text('Unstar')).tap();
|
||||
|
||||
await expect(element(by.id('action-sheet'))).toExist();
|
||||
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 expect(element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toBeNotVisible();
|
||||
await backToActions();
|
||||
|
@ -226,12 +228,14 @@ describe('Room actions screen', () => {
|
|||
await element(by.id('room-actions-pinned')).tap();
|
||||
await waitFor(element(by.id('pinned-messages-view'))).toExist().withTimeout(2000);
|
||||
await sleep(1000);
|
||||
await waitFor(element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view')))).toExist().withTimeout(60000);
|
||||
await expect(element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view')))).toExist();
|
||||
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')))).toBeVisible();
|
||||
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 element(by.text('Unpin')).tap();
|
||||
|
||||
await expect(element(by.id('action-sheet'))).toExist();
|
||||
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 expect(element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view')))).toBeNotVisible();
|
||||
await backToActions();
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"@react-native-community/async-storage": "^1.9.0",
|
||||
"@react-native-community/cameraroll": "1.6.0",
|
||||
"@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/slider": "2.0.9",
|
||||
"@react-navigation/drawer": "5.8.1",
|
||||
|
@ -57,7 +58,6 @@
|
|||
"prop-types": "15.7.2",
|
||||
"react": "16.11.0",
|
||||
"react-native": "0.62.2",
|
||||
"react-native-action-sheet": "^2.2.0",
|
||||
"react-native-animatable": "^1.3.3",
|
||||
"react-native-appearance": "0.3.4",
|
||||
"react-native-audio": "^4.3.0",
|
||||
|
@ -93,6 +93,7 @@
|
|||
"react-native-responsive-ui": "^1.1.1",
|
||||
"react-native-safe-area-context": "^3.0.2",
|
||||
"react-native-screens": "^2.7.0",
|
||||
"react-native-scroll-bottom-sheet": "0.6.1",
|
||||
"react-native-scrollable-tab-view": "^1.0.0",
|
||||
"react-native-slowlog": "^1.0.2",
|
||||
"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:
|
||||
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":
|
||||
version "0.1.10"
|
||||
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"
|
||||
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:
|
||||
version "1.3.3"
|
||||
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"
|
||||
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:
|
||||
version "1.0.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
|
|
Loading…
Reference in New Issue