[IMPROVE] Redesign emoji picker (#4328)

Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Danish Ahmed Mirza 2022-10-21 23:57:55 +05:30 committed by Diego Mello
parent 736e409b8b
commit 80171a9fdc
71 changed files with 1156 additions and 823 deletions

View File

@ -3,7 +3,13 @@ import { Provider } from 'react-redux';
import { themes } from '../app/lib/constants'; import { themes } from '../app/lib/constants';
import MessageContext from '../app/containers/message/Context'; import MessageContext from '../app/containers/message/Context';
import { selectServerRequest } from '../app/actions/server';
import { mockedStore as store } from '../app/reducers/mockedStore'; import { mockedStore as store } from '../app/reducers/mockedStore';
import { setUser } from '../app/actions/login';
const baseUrl = 'https://open.rocket.chat';
store.dispatch(selectServerRequest(baseUrl));
store.dispatch(setUser({ id: 'abc', username: 'rocket.cat', name: 'Rocket Cat' }));
export const decorators = [ export const decorators = [
Story => ( Story => (
@ -15,7 +21,7 @@ export const decorators = [
username: 'diego.mello', username: 'diego.mello',
token: 'abc' token: 'abc'
}, },
baseUrl: 'https://open.rocket.chat', baseUrl,
onPress: () => {}, onPress: () => {},
onLongPress: () => {}, onLongPress: () => {},
reactionInit: () => {}, reactionInit: () => {},

View File

@ -1,11 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Chip Chip Text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Rocket.Cat\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`; exports[`Storyshots Chip Chip Text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4},{\\"marginRight\\":8,\\"marginVertical\\":8}],\\"testID\\":\\"avatar\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"overflow\\":\\"hidden\\"},{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4}]},\\"children\\":[{\\"type\\":\\"FastImageView\\",\\"props\\":{\\"style\\":{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},\\"source\\":{\\"uri\\":\\"https://open.rocket.chat/avatar/rocket.cat?format=png&size=56\\",\\"headers\\":{\\"User-Agent\\":\\"RC Mobile; ios unknown; vunknown (unknown)\\"},\\"priority\\":\\"high\\"},\\"resizeMode\\":\\"cover\\"},\\"children\\":null}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Rocket.Cat\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`;
exports[`Storyshots Chip Chip With Short Text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Short\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`; exports[`Storyshots Chip Chip With Short Text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4},{\\"marginRight\\":8,\\"marginVertical\\":8}],\\"testID\\":\\"avatar\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"overflow\\":\\"hidden\\"},{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4}]},\\"children\\":[{\\"type\\":\\"FastImageView\\",\\"props\\":{\\"style\\":{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},\\"source\\":{\\"uri\\":\\"https://open.rocket.chat/avatar/rocket.cat?format=png&size=56\\",\\"headers\\":{\\"User-Agent\\":\\"RC Mobile; ios unknown; vunknown (unknown)\\"},\\"priority\\":\\"high\\"},\\"resizeMode\\":\\"cover\\"},\\"children\\":null}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Short\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`;
exports[`Storyshots Chip Chip Without Avatar 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Avatar\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`; exports[`Storyshots Chip Chip Without Avatar 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Avatar\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`;
exports[`Storyshots Chip Chip Without Avatar And Icon 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":true},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Avatar and Icon\\"]}]}]}]}]}"`; exports[`Storyshots Chip Chip Without Avatar And Icon 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":true},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Avatar and Icon\\"]}]}]}]}]}"`;
exports[`Storyshots Chip Chip Without Icon 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":true},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Icon\\"]}]}]}]}]}"`; exports[`Storyshots Chip Chip Without Icon 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":true},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4},{\\"marginRight\\":8,\\"marginVertical\\":8}],\\"testID\\":\\"avatar\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"overflow\\":\\"hidden\\"},{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4}]},\\"children\\":[{\\"type\\":\\"FastImageView\\",\\"props\\":{\\"style\\":{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},\\"source\\":{\\"uri\\":\\"https://open.rocket.chat/avatar/rocket.cat?format=png&size=56\\",\\"headers\\":{\\"User-Agent\\":\\"RC Mobile; ios unknown; vunknown (unknown)\\"},\\"priority\\":\\"high\\"},\\"resizeMode\\":\\"cover\\"},\\"children\\":null}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Icon\\"]}]}]}]}]}"`;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -18,10 +18,10 @@ const HANDLE_HEIGHT = isIOS ? 40 : 56;
const MIN_SNAP_HEIGHT = 16; const MIN_SNAP_HEIGHT = 16;
const CANCEL_HEIGHT = 64; const CANCEL_HEIGHT = 64;
const ANIMATION_DURATION = 250; export const ACTION_SHEET_ANIMATION_DURATION = 250;
const ANIMATION_CONFIG = { const ANIMATION_CONFIG = {
duration: ANIMATION_DURATION, duration: ACTION_SHEET_ANIMATION_DURATION,
// https://easings.net/#easeInOutCubic // https://easings.net/#easeInOutCubic
easing: Easing.bezier(0.645, 0.045, 0.355, 1.0) easing: Easing.bezier(0.645, 0.045, 0.355, 1.0)
}; };
@ -140,7 +140,7 @@ const ActionSheet = React.memo(
style={{ ...styles.container, ...bottomSheet }} style={{ ...styles.container, ...bottomSheet }}
backgroundStyle={{ backgroundColor: colors.focusedBackground }} backgroundStyle={{ backgroundColor: colors.focusedBackground }}
onChange={index => index === -1 && onClose()} onChange={index => index === -1 && onClose()}
// We need this to allow horizontal swipe gestures inside bottom sheet like in reaction picker // We need this to allow horizontal swipe gesture inside the bottom sheet like in reaction picker
enableContentPanningGesture={data?.enableContentPanningGesture ?? true} enableContentPanningGesture={data?.enableContentPanningGesture ?? true}
{...androidTablet} {...androidTablet}
> >

View File

@ -52,7 +52,11 @@ const BottomSheetContent = React.memo(({ options, hasCancel, hide, children }: I
/> />
); );
} }
return <BottomSheetView style={styles.contentContainer}>{children}</BottomSheetView>; return (
<BottomSheetView testID='action-sheet' style={styles.contentContainer}>
{children}
</BottomSheetView>
);
}); });
export default BottomSheetContent; export default BottomSheetContent;

View File

@ -1 +1,2 @@
export * from './Provider'; export * from './Provider';
export * from './ActionSheet';

View File

@ -43,9 +43,7 @@ const Avatar = React.memo(
let image; let image;
if (emoji) { if (emoji) {
image = ( image = <Emoji getCustomEmoji={getCustomEmoji} isMessageContainsOnlyEmoji literal={emoji} style={avatarStyle} />;
<Emoji baseUrl={server} getCustomEmoji={getCustomEmoji} isMessageContainsOnlyEmoji literal={emoji} style={avatarStyle} />
);
} else { } else {
let uri = avatar; let uri = avatar;
if (!isStatic) { if (!isStatic) {

View File

@ -1,24 +1,30 @@
import React from 'react'; import React from 'react';
import FastImage from 'react-native-fast-image'; import { StyleProp } from 'react-native';
import FastImage, { ImageStyle } from 'react-native-fast-image';
import { ICustomEmoji } from '../../definitions/IEmoji'; import { useAppSelector } from '../../lib/hooks';
import { ICustomEmoji } from '../../definitions';
interface ICustomEmojiProps {
emoji: ICustomEmoji;
style: StyleProp<ImageStyle>;
}
const CustomEmoji = React.memo( const CustomEmoji = React.memo(
({ baseUrl, emoji, style }: ICustomEmoji) => ( ({ emoji, style }: ICustomEmojiProps) => {
<FastImage const baseUrl = useAppSelector(state => state.share.server.server || state.server.server);
style={style} return (
source={{ <FastImage
uri: `${baseUrl}/emoji-custom/${encodeURIComponent(emoji.content || emoji.name)}.${emoji.extension}`, style={style}
priority: FastImage.priority.high source={{
}} uri: `${baseUrl}/emoji-custom/${encodeURIComponent(emoji.name)}.${emoji.extension}`,
resizeMode={FastImage.resizeMode.contain} priority: FastImage.priority.high
/> }}
), resizeMode={FastImage.resizeMode.contain}
(prevProps, nextProps) => { />
const prevEmoji = prevProps.emoji.content || prevProps.emoji.name; );
const nextEmoji = nextProps.emoji.content || nextProps.emoji.name; },
return prevEmoji === nextEmoji; () => true
}
); );
export default CustomEmoji; export default CustomEmoji;

View File

@ -0,0 +1,18 @@
import React from 'react';
import { Text } from 'react-native';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import styles from './styles';
import CustomEmoji from './CustomEmoji';
import { IEmoji } from '../../definitions/IEmoji';
interface IEmojiProps {
emoji: IEmoji;
}
export const Emoji = ({ emoji }: IEmojiProps): React.ReactElement => {
if (typeof emoji === 'string') {
return <Text style={styles.categoryEmoji}>{shortnameToUnicode(`:${emoji}:`)}</Text>;
}
return <CustomEmoji style={styles.customCategoryEmoji} emoji={emoji} />;
};

View File

@ -1,75 +1,45 @@
import React from 'react'; import React from 'react';
import { FlatList, Text, TouchableOpacity } from 'react-native'; import { useWindowDimensions } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode'; import { EMOJI_BUTTON_SIZE } from './styles';
import styles from './styles';
import CustomEmoji from './CustomEmoji';
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps'; import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
import { IEmoji, IEmojiCategory } from '../../definitions/IEmoji'; import { IEmoji } from '../../definitions/IEmoji';
import { PressableEmoji } from './PressableEmoji';
const EMOJI_SIZE = 50; interface IEmojiCategoryProps {
emojis: IEmoji[];
onEmojiSelected: (emoji: IEmoji) => void;
tabLabel?: string; // needed for react-native-scrollable-tab-view only
}
const renderEmoji = (emoji: IEmoji, size: number, baseUrl: string) => { const EmojiCategory = ({ onEmojiSelected, emojis }: IEmojiCategoryProps): React.ReactElement | null => {
if (emoji && emoji.isCustom) { const { width } = useWindowDimensions();
return (
<CustomEmoji const numColumns = Math.trunc(width / EMOJI_BUTTON_SIZE);
style={[styles.customCategoryEmoji, { height: size - 16, width: size - 16 }]} const marginHorizontal = (width % EMOJI_BUTTON_SIZE) / 2;
emoji={emoji}
baseUrl={baseUrl} const renderItem = ({ item }: { item: IEmoji }) => <PressableEmoji emoji={item} onPress={onEmojiSelected} />;
/>
); if (!width) {
return null;
} }
return ( return (
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}> <FlatList
{shortnameToUnicode(`:${emoji}:`)} // needed to update the numColumns when the width changes
</Text> key={`emoji-category-${width}`}
keyExtractor={item => (typeof item === 'string' ? item : item.name)}
data={emojis}
renderItem={renderItem}
numColumns={numColumns}
initialNumToRender={45}
removeClippedSubviews
contentContainerStyle={{ marginHorizontal }}
{...scrollPersistTaps}
keyboardDismissMode={'none'}
/>
); );
}; };
class EmojiCategory extends React.Component<IEmojiCategory> {
renderItem(emoji: IEmoji) {
const { baseUrl, onEmojiSelected } = this.props;
return (
<TouchableOpacity
activeOpacity={0.7}
// @ts-ignore
key={emoji && emoji.isCustom ? emoji.content : emoji}
onPress={() => onEmojiSelected(emoji)}
testID={`reaction-picker-${emoji && emoji.isCustom ? emoji.content : emoji}`}
>
{renderEmoji(emoji, EMOJI_SIZE, baseUrl)}
</TouchableOpacity>
);
}
render() {
const { emojis, width } = this.props;
if (!width) {
return null;
}
const numColumns = Math.trunc(width / EMOJI_SIZE);
const marginHorizontal = (width - numColumns * EMOJI_SIZE) / 2;
return (
<FlatList
contentContainerStyle={{ marginHorizontal }}
// rerender FlatList in case of width changes
key={`emoji-category-${width}`}
// @ts-ignore
keyExtractor={item => (item && item.isCustom && item.content) || item}
data={emojis}
extraData={this.props}
renderItem={({ item }) => this.renderItem(item)}
numColumns={numColumns}
initialNumToRender={45}
removeClippedSubviews
{...scrollPersistTaps}
keyboardDismissMode={'none'}
/>
);
}
}
export default EmojiCategory; export default EmojiCategory;

View File

@ -0,0 +1,61 @@
import React, { useState } from 'react';
import { StyleSheet, TextInputProps } from 'react-native';
import { FormTextInput } from '../TextInput/FormTextInput';
import { useTheme } from '../../theme';
import I18n from '../../i18n';
import { isIOS } from '../../lib/methods/helpers';
const styles = StyleSheet.create({
input: {
height: 32,
borderWidth: 0,
paddingVertical: 0,
borderRadius: 4
},
textInputContainer: {
marginBottom: 0
}
});
interface IEmojiSearchBarProps {
onBlur?: TextInputProps['onBlur'];
onChangeText: TextInputProps['onChangeText'];
bottomSheet?: boolean;
}
export const EmojiSearch = ({ onBlur, onChangeText, bottomSheet }: IEmojiSearchBarProps): React.ReactElement => {
const { colors } = useTheme();
const [searchText, setSearchText] = useState<string>('');
const handleTextChange = (text: string) => {
setSearchText(text);
if (onChangeText) {
onChangeText(text);
}
};
return (
<FormTextInput
autoCapitalize='none'
autoCorrect={false}
autoComplete='off'
returnKeyType='search'
textContentType='none'
blurOnSubmit
placeholder={I18n.t('Search_emoji')}
placeholderTextColor={colors.auxiliaryText}
underlineColorAndroid='transparent'
onChangeText={handleTextChange}
inputStyle={[styles.input, { backgroundColor: colors.textInputSecondaryBackground }]}
containerStyle={styles.textInputContainer}
value={searchText}
onClearInput={() => handleTextChange('')}
onBlur={onBlur}
iconRight={'search'}
testID='emoji-searchbar-input'
bottomSheet={bottomSheet && isIOS}
autoFocus={!bottomSheet} // focus on input when not in reaction picker
/>
);
};

View File

@ -0,0 +1,36 @@
import React from 'react';
import { View, Pressable } from 'react-native';
import { useTheme } from '../../theme';
import { CustomIcon } from '../CustomIcon';
import styles from './styles';
import { IFooterProps } from './interfaces';
const BUTTON_HIT_SLOP = { top: 15, right: 15, bottom: 15, left: 15 };
const Footer = ({ onSearchPressed, onBackspacePressed }: IFooterProps): React.ReactElement => {
const { colors } = useTheme();
return (
<View style={[styles.footerContainer, { borderTopColor: colors.borderColor }]}>
<Pressable
onPress={onSearchPressed}
hitSlop={BUTTON_HIT_SLOP}
style={({ pressed }) => [styles.footerButtonsContainer, { opacity: pressed ? 0.7 : 1 }]}
testID='emoji-picker-search'
>
<CustomIcon color={colors.auxiliaryTintColor} size={24} name='search' />
</Pressable>
<Pressable
onPress={onBackspacePressed}
hitSlop={BUTTON_HIT_SLOP}
style={({ pressed }) => [styles.footerButtonsContainer, { opacity: pressed ? 0.7 : 1 }]}
testID='emoji-picker-backspace'
>
<CustomIcon color={colors.auxiliaryTintColor} size={24} name='backspace' />
</Pressable>
</View>
);
};
export default Footer;

View File

@ -0,0 +1,28 @@
import React from 'react';
import { Pressable } from 'react-native';
import styles, { EMOJI_BUTTON_SIZE } from './styles';
import { IEmoji } from '../../definitions/IEmoji';
import { useTheme } from '../../theme';
import { isIOS } from '../../lib/methods/helpers';
import { Emoji } from './Emoji';
export const PressableEmoji = ({ emoji, onPress }: { emoji: IEmoji; onPress: (emoji: IEmoji) => void }): React.ReactElement => {
const { colors } = useTheme();
return (
<Pressable
key={typeof emoji === 'string' ? emoji : emoji.name}
onPress={() => onPress(emoji)}
testID={`emoji-${typeof emoji === 'string' ? emoji : emoji.name}`}
android_ripple={{ color: colors.bannerBackground, borderless: true, radius: EMOJI_BUTTON_SIZE / 2 }}
style={({ pressed }: { pressed: boolean }) => [
styles.emojiButton,
{
backgroundColor: isIOS && pressed ? colors.bannerBackground : 'transparent'
}
]}
>
<Emoji emoji={emoji} />
</Pressable>
);
};

View File

@ -1,56 +1,42 @@
import React from 'react'; import React from 'react';
import { StyleProp, Text, TextStyle, TouchableOpacity, View } from 'react-native'; import { Pressable, View } from 'react-native';
import styles from './styles'; import styles from './styles';
import { themes } from '../../lib/constants'; import { useTheme } from '../../theme';
import { TSupportedThemes } from '../../theme'; import { ITabBarProps } from './interfaces';
import { isIOS } from '../../lib/methods/helpers';
import { CustomIcon } from '../CustomIcon';
interface ITabBarProps { const TabBar = ({ activeTab, tabs, goToPage }: ITabBarProps): React.ReactElement => {
goToPage?: (page: number) => void; const { colors } = useTheme();
activeTab?: number;
tabs?: string[];
tabEmojiStyle: StyleProp<TextStyle>;
theme: TSupportedThemes;
}
export default class TabBar extends React.Component<ITabBarProps> { return (
shouldComponentUpdate(nextProps: ITabBarProps) { <View style={styles.tabsContainer}>
const { activeTab, theme } = this.props; {tabs?.map((tab, i) => (
if (nextProps.activeTab !== activeTab) { <Pressable
return true; key={tab}
} onPress={() => goToPage?.(i)}
if (nextProps.theme !== theme) { testID={`emoji-picker-tab-${tab}`}
return true; android_ripple={{ color: colors.bannerBackground }}
} style={({ pressed }: { pressed: boolean }) => [
return false; styles.tab,
} {
backgroundColor: isIOS && pressed ? colors.bannerBackground : 'transparent'
}
]}
>
<CustomIcon name={tab} size={24} color={activeTab === i ? colors.tintColor : colors.auxiliaryTintColor} />
<View
style={
activeTab === i
? [styles.activeTabLine, { backgroundColor: colors.tintColor }]
: [styles.tabLine, { backgroundColor: colors.borderColor }]
}
/>
</Pressable>
))}
</View>
);
};
render() { export default TabBar;
const { tabs, goToPage, tabEmojiStyle, activeTab, theme } = this.props;
return (
<View style={styles.tabsContainer}>
{tabs?.map((tab, i) => (
<TouchableOpacity
activeOpacity={0.7}
key={tab}
onPress={() => {
if (goToPage) {
goToPage(i);
}
}}
style={styles.tab}
testID={`reaction-picker-${tab}`}
>
<Text style={[styles.tabEmoji, tabEmojiStyle]}>{tab}</Text>
{activeTab === i ? (
<View style={[styles.activeTabLine, { backgroundColor: themes[theme].tintColor }]} />
) : (
<View style={styles.tabLine} />
)}
</TouchableOpacity>
))}
</View>
);
}
}

View File

@ -1,155 +1,44 @@
import React, { Component } from 'react'; import React from 'react';
import { StyleProp, TextStyle, View } from 'react-native'; import { View } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view'; import ScrollableTabView from 'react-native-scrollable-tab-view';
import { dequal } from 'dequal';
import { connect } from 'react-redux';
import orderBy from 'lodash/orderBy';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { ImageStyle } from 'react-native-fast-image';
import TabBar from './TabBar'; import TabBar from './TabBar';
import EmojiCategory from './EmojiCategory'; import EmojiCategory from './EmojiCategory';
import Footer from './Footer';
import styles from './styles'; import styles from './styles';
import categories from './categories'; import { categories, emojisByCategory } from '../../lib/constants';
import database from '../../lib/database'; import { useTheme } from '../../theme';
import { emojisByCategory } from './emojis'; import { IEmoji, ICustomEmojis } from '../../definitions';
import protectedFunction from '../../lib/methods/helpers/protectedFunction'; import { useAppSelector, useFrequentlyUsedEmoji } from '../../lib/hooks';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode'; import { addFrequentlyUsed } from '../../lib/methods';
import log from '../../lib/methods/helpers/log'; import { IEmojiPickerProps, EventTypes } from './interfaces';
import { themes } from '../../lib/constants';
import { TSupportedThemes } from '../../theme';
import { IEmoji, TGetCustomEmoji, IApplicationState, ICustomEmojis, TFrequentlyUsedEmojiModel } from '../../definitions';
interface IEmojiPickerProps { const EmojiPicker = ({
isMessageContainsOnlyEmoji?: boolean; onItemClicked,
getCustomEmoji?: TGetCustomEmoji; isEmojiKeyboard = false,
baseUrl: string; searching = false,
customEmojis: ICustomEmojis; searchedEmojis = []
style?: StyleProp<ImageStyle>; }: IEmojiPickerProps): React.ReactElement | null => {
theme: TSupportedThemes; const { colors } = useTheme();
onEmojiSelected: (emoji: string, shortname?: string) => void; const { frequentlyUsed, loaded } = useFrequentlyUsedEmoji();
tabEmojiStyle?: StyleProp<TextStyle>;
}
interface IEmojiPickerState { const allCustomEmojis: ICustomEmojis = useAppSelector(
frequentlyUsed: (string | { content?: string; extension?: string; isCustom: boolean })[]; state => state.customEmojis,
customEmojis: any; () => true
show: boolean; );
width: number | null; const customEmojis = Object.keys(allCustomEmojis)
} .filter(item => item === allCustomEmojis[item].name)
.map(item => ({
name: allCustomEmojis[item].name,
extension: allCustomEmojis[item].extension
}));
class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> { const handleEmojiSelect = (emoji: IEmoji) => {
constructor(props: IEmojiPickerProps) { onItemClicked(EventTypes.EMOJI_PRESSED, emoji);
super(props); addFrequentlyUsed(emoji);
const customEmojis = Object.keys(props.customEmojis)
.filter(item => item === props.customEmojis[item].name)
.map(item => ({
content: props.customEmojis[item].name,
extension: props.customEmojis[item].extension,
isCustom: true
}));
this.state = {
frequentlyUsed: [],
customEmojis,
show: false,
width: null
};
}
async componentDidMount() {
await this.updateFrequentlyUsed();
this.setState({ show: true });
}
shouldComponentUpdate(nextProps: IEmojiPickerProps, nextState: IEmojiPickerState) {
const { frequentlyUsed, show, width } = this.state;
const { theme } = this.props;
if (nextProps.theme !== theme) {
return true;
}
if (nextState.show !== show) {
return true;
}
if (nextState.width !== width) {
return true;
}
if (!dequal(nextState.frequentlyUsed, frequentlyUsed)) {
return true;
}
return false;
}
onEmojiSelected = (emoji: IEmoji) => {
try {
const { onEmojiSelected } = this.props;
if (emoji.isCustom) {
this._addFrequentlyUsed({
content: emoji.content,
extension: emoji.extension,
isCustom: true
});
onEmojiSelected(`:${emoji.content}:`);
} else {
const content = emoji;
this._addFrequentlyUsed({ content, isCustom: false });
const shortname = `:${emoji}:`;
onEmojiSelected(shortnameToUnicode(shortname), shortname);
}
} catch (e) {
log(e);
}
}; };
_addFrequentlyUsed = protectedFunction(async (emoji: IEmoji) => { const renderCategory = (category: keyof typeof emojisByCategory, i: number, label: string) => {
const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojiRecord: TFrequentlyUsedEmojiModel;
try {
freqEmojiRecord = await freqEmojiCollection.find(emoji.content);
} catch (error) {
// Do nothing
}
await db.write(async () => {
if (freqEmojiRecord) {
await freqEmojiRecord.update(f => {
if (f.count) {
f.count += 1;
}
});
} else {
await freqEmojiCollection.create(f => {
f._raw = sanitizedRaw({ id: emoji.content }, freqEmojiCollection.schema);
Object.assign(f, emoji);
f.count = 1;
});
}
});
});
updateFrequentlyUsed = async () => {
const db = database.active;
const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch();
const frequentlyUsedOrdered = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
const frequentlyUsed = frequentlyUsedOrdered.map(item => {
if (item.isCustom) {
return { content: item.content, extension: item.extension, isCustom: item.isCustom };
}
return shortnameToUnicode(`${item.content}`);
});
this.setState({ frequentlyUsed });
};
onLayout = ({
nativeEvent: {
layout: { width }
}
}: any) => this.setState({ width });
renderCategory(category: keyof typeof emojisByCategory, i: number, label: string) {
const { frequentlyUsed, customEmojis, width } = this.state;
const { baseUrl } = this.props;
let emojis = []; let emojis = [];
if (i === 0) { if (i === 0) {
emojis = frequentlyUsed; emojis = frequentlyUsed;
@ -158,49 +47,40 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
} else { } else {
emojis = emojisByCategory[category]; emojis = emojisByCategory[category];
} }
return ( if (!emojis.length) {
<EmojiCategory
emojis={emojis}
onEmojiSelected={(emoji: IEmoji) => this.onEmojiSelected(emoji)}
style={styles.categoryContainer}
width={width}
baseUrl={baseUrl}
tabLabel={label}
/>
);
}
render() {
const { show, frequentlyUsed } = this.state;
const { tabEmojiStyle, theme } = this.props;
if (!show) {
return null; return null;
} }
return ( return <EmojiCategory emojis={emojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} tabLabel={label} />;
<View onLayout={this.onLayout} style={{ flex: 1 }}> };
if (!loaded) {
return null;
}
return (
<View style={styles.emojiPickerContainer}>
{searching ? (
<EmojiCategory emojis={searchedEmojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} />
) : (
<ScrollableTabView <ScrollableTabView
renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} theme={theme} />} renderTabBar={() => <TabBar />}
contentProps={{ contentProps={{
keyboardShouldPersistTaps: 'always', keyboardShouldPersistTaps: 'always',
keyboardDismissMode: 'none' keyboardDismissMode: 'none'
}} }}
style={{ backgroundColor: themes[theme].focusedBackground }} style={{ backgroundColor: colors.messageboxBackground }}
> >
{categories.tabs.map((tab: any, i) => {categories.tabs.map((tab: any, i) => renderCategory(tab.category, i, tab.tabLabel))}
i === 0 && frequentlyUsed.length === 0
? null // when no frequentlyUsed don't show the tab
: this.renderCategory(tab.category, i, tab.tabLabel)
)}
</ScrollableTabView> </ScrollableTabView>
</View> )}
); {isEmojiKeyboard && (
} <Footer
} onSearchPressed={() => onItemClicked(EventTypes.SEARCH_PRESSED)}
onBackspacePressed={() => onItemClicked(EventTypes.BACKSPACE_PRESSED)}
/>
)}
</View>
);
};
const mapStateToProps = (state: IApplicationState) => ({ export default EmojiPicker;
customEmojis: state.customEmojis,
baseUrl: state.share.server.server || state.server.server
});
export default connect(mapStateToProps)(EmojiPicker);

View File

@ -0,0 +1,26 @@
import { TIconsName } from '../CustomIcon';
import { IEmoji } from '../../definitions';
export enum EventTypes {
EMOJI_PRESSED = 'emojiPressed',
BACKSPACE_PRESSED = 'backspacePressed',
SEARCH_PRESSED = 'searchPressed'
}
export interface IEmojiPickerProps {
onItemClicked: (event: EventTypes, emoji?: IEmoji) => void;
isEmojiKeyboard?: boolean;
searching?: boolean;
searchedEmojis?: IEmoji[];
}
export interface IFooterProps {
onBackspacePressed: () => void;
onSearchPressed: () => void;
}
export interface ITabBarProps {
goToPage?: (page: number) => void;
activeTab?: number;
tabs?: TIconsName[];
}

View File

@ -2,20 +2,23 @@ import { StyleSheet } from 'react-native';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
export const EMOJI_BUTTON_SIZE = 44;
export const EMOJI_SIZE = EMOJI_BUTTON_SIZE - 16;
export default StyleSheet.create({ export default StyleSheet.create({
container: { container: {
flex: 1 flex: 1
}, },
tabsContainer: { tabsContainer: {
height: 45, height: EMOJI_BUTTON_SIZE,
flexDirection: 'row', flexDirection: 'row'
paddingTop: 5
}, },
tab: { tab: {
flex: 1, flex: 1,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingBottom: 10 paddingVertical: 10,
width: EMOJI_BUTTON_SIZE
}, },
tabEmoji: { tabEmoji: {
fontSize: 20, fontSize: 20,
@ -33,7 +36,6 @@ export default StyleSheet.create({
left: 0, left: 0,
right: 0, right: 0,
height: 2, height: 2,
backgroundColor: 'rgba(0,0,0,0.05)',
bottom: 0 bottom: 0
}, },
categoryContainer: { categoryContainer: {
@ -49,10 +51,34 @@ export default StyleSheet.create({
}, },
categoryEmoji: { categoryEmoji: {
...sharedStyles.textAlignCenter, ...sharedStyles.textAlignCenter,
textAlignVertical: 'center',
fontSize: EMOJI_SIZE,
backgroundColor: 'transparent', backgroundColor: 'transparent',
color: '#ffffff' color: '#ffffff'
}, },
customCategoryEmoji: { customCategoryEmoji: {
margin: 8 height: EMOJI_SIZE,
} width: EMOJI_SIZE
},
emojiButton: {
alignItems: 'center',
justifyContent: 'center',
height: EMOJI_BUTTON_SIZE,
width: EMOJI_BUTTON_SIZE
},
footerContainer: {
height: EMOJI_BUTTON_SIZE,
paddingHorizontal: 12,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderTopWidth: 1
},
footerButtonsContainer: {
height: EMOJI_BUTTON_SIZE,
width: EMOJI_BUTTON_SIZE,
justifyContent: 'center',
alignItems: 'center'
},
emojiPickerContainer: { flex: 1 }
}); });

View File

@ -1,32 +1,29 @@
import React, { useEffect, useState } from 'react'; import React from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native'; import { FlatList, StyleSheet, Text, View } from 'react-native';
import { TSupportedThemes, useTheme } from '../../theme'; import { TSupportedThemes, useTheme } from '../../theme';
import { themes } from '../../lib/constants'; import { themes } from '../../lib/constants';
import { CustomIcon } from '../CustomIcon'; import { CustomIcon } from '../CustomIcon';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode'; import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import { addFrequentlyUsed } from '../../lib/methods';
import { useFrequentlyUsedEmoji } from '../../lib/hooks';
import CustomEmoji from '../EmojiPicker/CustomEmoji'; import CustomEmoji from '../EmojiPicker/CustomEmoji';
import database from '../../lib/database';
import { useDimensions } from '../../dimensions'; import { useDimensions } from '../../dimensions';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { TAnyMessageModel, TFrequentlyUsedEmojiModel } from '../../definitions'; import { IEmoji, TAnyMessageModel } from '../../definitions';
import Touch from '../Touch'; import Touch from '../Touch';
type TItem = TFrequentlyUsedEmojiModel | string;
export interface IHeader { export interface IHeader {
handleReaction: (emoji: TItem, message: TAnyMessageModel) => void; handleReaction: (emoji: IEmoji, message: TAnyMessageModel) => void;
server: string;
message: TAnyMessageModel; message: TAnyMessageModel;
isMasterDetail: boolean; isMasterDetail: boolean;
} }
type TOnReaction = ({ emoji }: { emoji: TItem }) => void; type TOnReaction = ({ emoji }: { emoji: IEmoji }) => void;
interface THeaderItem { interface THeaderItem {
item: TItem; item: IEmoji;
onReaction: TOnReaction; onReaction: TOnReaction;
server: string;
theme: TSupportedThemes; theme: TSupportedThemes;
} }
@ -64,30 +61,19 @@ const styles = StyleSheet.create({
} }
}); });
const keyExtractor = (item: TItem) => { const HeaderItem = ({ item, onReaction, theme }: THeaderItem) => (
const emojiModel = item as TFrequentlyUsedEmojiModel; <Touch
return (emojiModel.id ? emojiModel.content : item) as string; testID={`message-actions-emoji-${item}`}
}; onPress={() => onReaction({ emoji: item })}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
const DEFAULT_EMOJIS = ['clap', '+1', 'heart_eyes', 'grinning', 'thinking_face', 'smiley']; >
{typeof item === 'string' ? (
const HeaderItem = ({ item, onReaction, server, theme }: THeaderItem) => { <Text style={styles.headerIcon}>{shortnameToUnicode(`:${item}:`)}</Text>
const emojiModel = item as TFrequentlyUsedEmojiModel; ) : (
const emoji = (emojiModel.id ? emojiModel.content : item) as string; <CustomEmoji style={styles.customEmoji} emoji={item} />
return ( )}
<Touch </Touch>
testID={`message-actions-emoji-${emoji}`} );
onPress={() => onReaction({ emoji: `:${emoji}:` })}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
>
{emojiModel?.isCustom ? (
<CustomEmoji style={styles.customEmoji} emoji={emojiModel} baseUrl={server} />
) : (
<Text style={styles.headerIcon}>{shortnameToUnicode(`:${emoji}:`)}</Text>
)}
</Touch>
);
};
const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => ( const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => (
<Touch <Touch
@ -99,49 +85,35 @@ const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => (
</Touch> </Touch>
); );
const Header = React.memo(({ handleReaction, server, message, isMasterDetail }: IHeader) => { const Header = React.memo(({ handleReaction, message, isMasterDetail }: IHeader) => {
const [items, setItems] = useState<TItem[]>([]);
const { width, height } = useDimensions(); const { width, height } = useDimensions();
const { theme } = useTheme(); const { theme } = useTheme();
const { frequentlyUsed, loaded } = useFrequentlyUsedEmoji(true);
const isLandscape = width > height;
const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;
const quantity = Math.trunc(size / (ITEM_SIZE + ITEM_MARGIN * 2) - 1);
// TODO: create custom hook to re-render based on screen size const onReaction: TOnReaction = ({ emoji }) => {
const setEmojis = async () => { handleReaction(emoji, message);
try { addFrequentlyUsed(emoji);
const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojis: TItem[] = await freqEmojiCollection.query().fetch();
const isLandscape = width > height;
const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;
const quantity = size / (ITEM_SIZE + ITEM_MARGIN * 2) - 1;
freqEmojis = freqEmojis.concat(DEFAULT_EMOJIS).slice(0, quantity);
setItems(freqEmojis);
} catch {
// Do nothing
}
}; };
useEffect(() => { const renderItem = ({ item }: { item: IEmoji }) => <HeaderItem item={item} onReaction={onReaction} theme={theme} />;
setEmojis();
}, []);
const onReaction: TOnReaction = ({ emoji }) => handleReaction(emoji, message);
const renderItem = ({ item }: { item: TItem }) => (
<HeaderItem item={item} onReaction={onReaction} server={server} theme={theme} />
);
const renderFooter = () => <HeaderFooter onReaction={onReaction} theme={theme} />; const renderFooter = () => <HeaderFooter onReaction={onReaction} theme={theme} />;
if (!loaded) {
return null;
}
return ( return (
<View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}> <View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}>
<FlatList <FlatList
data={items} data={frequentlyUsed.slice(0, quantity)}
renderItem={renderItem} renderItem={renderItem}
ListFooterComponent={renderFooter} ListFooterComponent={renderFooter}
style={{ backgroundColor: themes[theme].focusedBackground }} style={{ backgroundColor: themes[theme].focusedBackground }}
keyExtractor={keyExtractor} keyExtractor={item => (typeof item === 'string' ? item : item.name)}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
scrollEnabled={false} scrollEnabled={false}
horizontal horizontal

View File

@ -12,10 +12,10 @@ import { getMessageTranslation } from '../message/utils';
import { LISTENER } from '../Toast'; import { LISTENER } from '../Toast';
import EventEmitter from '../../lib/methods/helpers/events'; import EventEmitter from '../../lib/methods/helpers/events';
import { showConfirmationAlert } from '../../lib/methods/helpers/info'; import { showConfirmationAlert } from '../../lib/methods/helpers/info';
import { TActionSheetOptionsItem, useActionSheet } from '../ActionSheet'; import { TActionSheetOptionsItem, useActionSheet, ACTION_SHEET_ANIMATION_DURATION } from '../ActionSheet';
import Header, { HEADER_HEIGHT, IHeader } from './Header'; import Header, { HEADER_HEIGHT, IHeader } from './Header';
import events from '../../lib/methods/helpers/log/events'; import events from '../../lib/methods/helpers/log/events';
import { IApplicationState, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions'; import { IApplicationState, IEmoji, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
import { getPermalinkMessage } from '../../lib/methods'; import { getPermalinkMessage } from '../../lib/methods';
import { hasPermission } from '../../lib/methods/helpers'; import { hasPermission } from '../../lib/methods/helpers';
import { Services } from '../../lib/services'; import { Services } from '../../lib/services';
@ -26,7 +26,7 @@ export interface IMessageActionsProps {
user: Pick<ILoggedUser, 'id'>; user: Pick<ILoggedUser, 'id'>;
editInit: (message: TAnyMessageModel) => void; editInit: (message: TAnyMessageModel) => void;
reactionInit: (message: TAnyMessageModel) => void; reactionInit: (message: TAnyMessageModel) => void;
onReactionPress: (shortname: string, messageId: string) => void; onReactionPress: (shortname: IEmoji, messageId: string) => void;
replyInit: (message: TAnyMessageModel, mention: boolean) => void; replyInit: (message: TAnyMessageModel, mention: boolean) => void;
isMasterDetail: boolean; isMasterDetail: boolean;
isReadOnly: boolean; isReadOnly: boolean;
@ -37,7 +37,6 @@ export interface IMessageActionsProps {
Message_AllowPinning?: boolean; Message_AllowPinning?: boolean;
Message_AllowStarring?: boolean; Message_AllowStarring?: boolean;
Message_Read_Receipt_Store_Users?: boolean; Message_Read_Receipt_Store_Users?: boolean;
server: string;
editMessagePermission?: string[]; editMessagePermission?: string[];
deleteMessagePermission?: string[]; deleteMessagePermission?: string[];
forceDeleteMessagePermission?: string[]; forceDeleteMessagePermission?: string[];
@ -60,7 +59,6 @@ const MessageActions = React.memo(
onReactionPress, onReactionPress,
replyInit, replyInit,
isReadOnly, isReadOnly,
server,
Message_AllowDeleting, Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes, Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing, Message_AllowEditing,
@ -261,12 +259,10 @@ const MessageActions = React.memo(
const handleReaction: IHeader['handleReaction'] = (shortname, message) => { const handleReaction: IHeader['handleReaction'] = (shortname, message) => {
logEvent(events.ROOM_MSG_ACTION_REACTION); logEvent(events.ROOM_MSG_ACTION_REACTION);
if (shortname) { if (shortname) {
// TODO: evaluate unification with IEmoji onReactionPress(shortname, message.id);
onReactionPress(shortname as any, message.id);
} else { } else {
reactionInit(message); setTimeout(() => reactionInit(message), ACTION_SHEET_ANIMATION_DURATION);
} }
// close actionSheet when click at header
hideActionSheet(); hideActionSheet();
}; };
@ -460,7 +456,7 @@ const MessageActions = React.memo(
headerHeight: HEADER_HEIGHT, headerHeight: HEADER_HEIGHT,
customHeader: customHeader:
!isReadOnly || room.reactWhenReadOnly ? ( !isReadOnly || room.reactWhenReadOnly ? (
<Header server={server} handleReaction={handleReaction} isMasterDetail={isMasterDetail} message={message} /> <Header handleReaction={handleReaction} isMasterDetail={isMasterDetail} message={message} />
) : null ) : null
}); });
}; };

View File

@ -6,22 +6,31 @@ import { Provider } from 'react-redux';
import store from '../../lib/store'; import store from '../../lib/store';
import EmojiPicker from '../EmojiPicker'; import EmojiPicker from '../EmojiPicker';
import styles from './styles'; import styles from './styles';
import { themes } from '../../lib/constants'; import { ThemeContext, TSupportedThemes } from '../../theme';
import { TSupportedThemes } from '../../theme'; import { EventTypes } from '../EmojiPicker/interfaces';
import { IEmoji } from '../../definitions';
import { colors } from '../../lib/constants';
const EmojiKeyboard = ({ theme }: { theme: TSupportedThemes }) => { const EmojiKeyboard = ({ theme }: { theme: TSupportedThemes }) => {
const onEmojiSelected = (emoji: string) => { const onItemClicked = (eventType: EventTypes, emoji?: IEmoji) => {
KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji }); KeyboardRegistry.onItemSelected('EmojiKeyboard', { eventType, emoji });
}; };
return ( return (
<Provider store={store}> <Provider store={store}>
<View <ThemeContext.Provider
style={[styles.emojiKeyboardContainer, { borderTopColor: themes[theme].borderColor }]} value={{
testID='messagebox-keyboard-emoji' theme,
colors: colors[theme]
}}
> >
<EmojiPicker onEmojiSelected={onEmojiSelected} theme={theme} /> <View
</View> style={[styles.emojiKeyboardContainer, { borderTopColor: colors[theme].borderColor }]}
testID='messagebox-keyboard-emoji'
>
<EmojiPicker onItemClicked={onItemClicked} isEmojiKeyboard={true} />
</View>
</ThemeContext.Provider>
</Provider> </Provider>
); );
}; };

View File

@ -0,0 +1,116 @@
import React, { useState } from 'react';
import { View, Text, Pressable, FlatList, StyleSheet } from 'react-native';
import { useTheme } from '../../theme';
import I18n from '../../i18n';
import { CustomIcon } from '../CustomIcon';
import { IEmoji } from '../../definitions';
import { useFrequentlyUsedEmoji } from '../../lib/hooks';
import { addFrequentlyUsed, searchEmojis } from '../../lib/methods';
import { useDebounce } from '../../lib/methods/helpers';
import sharedStyles from '../../views/Styles';
import { PressableEmoji } from '../EmojiPicker/PressableEmoji';
import { EmojiSearch } from '../EmojiPicker/EmojiSearch';
import { EMOJI_BUTTON_SIZE } from '../EmojiPicker/styles';
import { events, logEvent } from '../../lib/methods/helpers/log';
const BUTTON_HIT_SLOP = { top: 4, right: 4, bottom: 4, left: 4 };
const styles = StyleSheet.create({
listContainer: {
height: EMOJI_BUTTON_SIZE,
margin: 8,
flexGrow: 1
},
container: {
borderTopWidth: 1
},
searchContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
marginBottom: 12
},
backButton: {
width: 32,
height: 32,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 10
},
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center'
},
emptyText: {
...sharedStyles.textRegular,
fontSize: 16
},
inputContainer: {
flex: 1
}
});
interface IEmojiSearchBarProps {
openEmoji: () => void;
closeEmoji: () => void;
onEmojiSelected: (emoji: IEmoji) => void;
}
const EmojiSearchBar = ({ openEmoji, closeEmoji, onEmojiSelected }: IEmojiSearchBarProps): React.ReactElement => {
const { colors } = useTheme();
const [searchText, setSearchText] = useState<string>('');
const { frequentlyUsed } = useFrequentlyUsedEmoji(true);
const [emojis, setEmojis] = useState<IEmoji[]>([]);
const handleTextChange = useDebounce(async (text: string) => {
logEvent(events.MB_SB_EMOJI_SEARCH);
setSearchText(text);
const result = await searchEmojis(text);
setEmojis(result);
}, 300);
const handleEmojiSelected = (emoji: IEmoji) => {
logEvent(events.MB_SB_EMOJI_SELECTED);
onEmojiSelected(emoji);
addFrequentlyUsed(emoji);
};
const renderItem = ({ item }: { item: IEmoji }) => <PressableEmoji emoji={item} onPress={handleEmojiSelected} />;
return (
<View style={[styles.container, { borderTopColor: colors.borderColor, backgroundColor: colors.messageboxBackground }]}>
<FlatList
horizontal
data={searchText ? emojis : frequentlyUsed}
renderItem={renderItem}
showsHorizontalScrollIndicator={false}
ListEmptyComponent={() => (
<View style={styles.emptyContainer} testID='no-results-found'>
<Text style={[styles.emptyText, { color: colors.auxiliaryText }]}>{I18n.t('No_results_found')}</Text>
</View>
)}
keyExtractor={item => (typeof item === 'string' ? item : item.name)}
contentContainerStyle={styles.listContainer}
keyboardShouldPersistTaps='always'
/>
<View style={styles.searchContainer}>
<Pressable
style={({ pressed }: { pressed: boolean }) => [styles.backButton, { opacity: pressed ? 0.7 : 1 }]}
onPress={openEmoji}
hitSlop={BUTTON_HIT_SLOP}
testID='openback-emoji-keyboard'
>
<CustomIcon name='chevron-left' size={24} color={colors.auxiliaryTintColor} />
</Pressable>
<View style={styles.inputContainer}>
<EmojiSearch onBlur={closeEmoji} onChangeText={handleTextChange} />
</View>
</View>
</View>
);
};
export default EmojiSearchBar;

View File

@ -1,10 +1,9 @@
import React, { useContext } from 'react'; import React from 'react';
import { Text } from 'react-native'; import { Text } from 'react-native';
import { IEmoji } from '../../../definitions/IEmoji'; import { IEmoji } from '../../../definitions/IEmoji';
import shortnameToUnicode from '../../../lib/methods/helpers/shortnameToUnicode'; import shortnameToUnicode from '../../../lib/methods/helpers/shortnameToUnicode';
import CustomEmoji from '../../EmojiPicker/CustomEmoji'; import CustomEmoji from '../../EmojiPicker/CustomEmoji';
import MessageboxContext from '../Context';
import styles from '../styles'; import styles from '../styles';
interface IMessageBoxMentionEmoji { interface IMessageBoxMentionEmoji {
@ -12,13 +11,10 @@ interface IMessageBoxMentionEmoji {
} }
const MentionEmoji = ({ item }: IMessageBoxMentionEmoji) => { const MentionEmoji = ({ item }: IMessageBoxMentionEmoji) => {
const context = useContext(MessageboxContext); if (typeof item === 'string') {
const { baseUrl } = context; return <Text style={styles.mentionItemEmoji}>{shortnameToUnicode(`:${item}:`)}</Text>;
if (item.name) {
return <CustomEmoji style={styles.mentionItemCustomEmoji} emoji={item} baseUrl={baseUrl} />;
} }
return <Text style={styles.mentionItemEmoji}>{shortnameToUnicode(`:${item}:`)}</Text>; return <CustomEmoji style={styles.mentionItemCustomEmoji} emoji={item} />;
}; };
export default MentionEmoji; export default MentionEmoji;

View File

@ -4,5 +4,6 @@ export const MENTIONS_TRACKING_TYPE_COMMANDS = '/';
export const MENTIONS_TRACKING_TYPE_ROOMS = '#'; export const MENTIONS_TRACKING_TYPE_ROOMS = '#';
export const MENTIONS_TRACKING_TYPE_CANNED = '!'; export const MENTIONS_TRACKING_TYPE_CANNED = '!';
export const MENTIONS_COUNT_TO_DISPLAY = 4; export const MENTIONS_COUNT_TO_DISPLAY = 4;
export const MAX_EMOJIS_TO_DISPLAY = 20;
export const TIMEOUT_CLOSE_EMOJI = 300; export const TIMEOUT_CLOSE_EMOJI = 300;

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Alert, Keyboard, NativeModules, Text, View } from 'react-native'; import { Alert, Keyboard, NativeModules, Text, View, BackHandler } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { KeyboardAccessoryView } from 'react-native-ui-lib/keyboard'; import { KeyboardAccessoryView } from 'react-native-ui-lib/keyboard';
import ImagePicker, { Image, ImageOrVideo, Options } from 'react-native-image-crop-picker'; import ImagePicker, { Image, ImageOrVideo, Options } from 'react-native-image-crop-picker';
@ -13,12 +13,11 @@ import { TextInput, IThemedTextInput } from '../TextInput';
import { userTyping as userTypingAction } from '../../actions/room'; import { userTyping as userTypingAction } from '../../actions/room';
import styles from './styles'; import styles from './styles';
import database from '../../lib/database'; import database from '../../lib/database';
import { emojis } from '../EmojiPicker/emojis';
import log, { events, logEvent } from '../../lib/methods/helpers/log'; import log, { events, logEvent } from '../../lib/methods/helpers/log';
import RecordAudio from './RecordAudio'; import RecordAudio from './RecordAudio';
import I18n from '../../i18n'; import I18n from '../../i18n';
import ReplyPreview from './ReplyPreview'; import ReplyPreview from './ReplyPreview';
import { themes } from '../../lib/constants'; import { themes, emojis } from '../../lib/constants';
import LeftButtons from './LeftButtons'; import LeftButtons from './LeftButtons';
import RightButtons from './RightButtons'; import RightButtons from './RightButtons';
import { canUploadFile } from '../../lib/methods/helpers/media'; import { canUploadFile } from '../../lib/methods/helpers/media';
@ -51,7 +50,8 @@ import {
TGetCustomEmoji, TGetCustomEmoji,
TSubscriptionModel, TSubscriptionModel,
TThreadModel, TThreadModel,
IMessage IMessage,
IEmoji
} from '../../definitions'; } from '../../definitions';
import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types';
import { getPermalinkMessage, search, sendFileMessage } from '../../lib/methods'; import { getPermalinkMessage, search, sendFileMessage } from '../../lib/methods';
@ -59,6 +59,9 @@ import { hasPermission, debounce, isAndroid, isIOS, isTablet, compareServerVersi
import { Services } from '../../lib/services'; import { Services } from '../../lib/services';
import { TSupportedThemes } from '../../theme'; import { TSupportedThemes } from '../../theme';
import { ChatsStackParamList } from '../../stacks/types'; import { ChatsStackParamList } from '../../stacks/types';
import { EventTypes } from '../EmojiPicker/interfaces';
import EmojiSearchbar from './EmojiSearchbar';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
require('./EmojiKeyboard'); require('./EmojiKeyboard');
@ -129,6 +132,7 @@ interface IMessageBoxState {
tshow: boolean; tshow: boolean;
mentionLoading: boolean; mentionLoading: boolean;
permissionToUpload: boolean; permissionToUpload: boolean;
showEmojiSearchbar: boolean;
} }
class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> { class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
@ -183,7 +187,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
command: {}, command: {},
tshow: this.sendThreadToChannel, tshow: this.sendThreadToChannel,
mentionLoading: false, mentionLoading: false,
permissionToUpload: true permissionToUpload: true,
showEmojiSearchbar: false
}; };
this.text = ''; this.text = '';
this.selection = { start: 0, end: 0 }; this.selection = { start: 0, end: 0 };
@ -209,6 +214,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
...videoPickerConfig, ...videoPickerConfig,
...libPickerLabels ...libPickerLabels
}; };
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
} }
get sendThreadToChannel() { get sendThreadToChannel() {
@ -326,7 +333,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
tshow, tshow,
mentionLoading, mentionLoading,
trackingType, trackingType,
permissionToUpload permissionToUpload,
showEmojiSearchbar
} = this.state; } = this.state;
const { const {
@ -346,6 +354,9 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (nextState.showEmojiKeyboard !== showEmojiKeyboard) { if (nextState.showEmojiKeyboard !== showEmojiKeyboard) {
return true; return true;
} }
if (nextState.showEmojiSearchbar !== showEmojiSearchbar) {
return true;
}
if (!isFocused()) { if (!isFocused()) {
return false; return false;
} }
@ -438,6 +449,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (isTablet) { if (isTablet) {
EventEmiter.removeListener(KEY_COMMAND, this.handleCommands); EventEmiter.removeListener(KEY_COMMAND, this.handleCommands);
} }
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
} }
setOptions = async () => { setOptions = async () => {
@ -519,7 +531,10 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}, 100); }, 100);
onKeyboardResigned = () => { onKeyboardResigned = () => {
this.closeEmoji(); const { showEmojiSearchbar } = this.state;
if (!showEmojiSearchbar) {
this.closeEmoji();
}
}; };
onPressMention = (item: any) => { onPressMention = (item: any) => {
@ -577,18 +592,50 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
} }
}; };
onEmojiSelected = (keyboardId: string, params: { emoji: string }) => { onKeyboardItemSelected = (keyboardId: string, params: { eventType: EventTypes; emoji: IEmoji }) => {
const { eventType, emoji } = params;
const { text } = this; const { text } = this;
const { emoji } = params;
let newText = ''; let newText = '';
// if messagebox has an active cursor // if messagebox has an active cursor
const { start, end } = this.selection; const { start, end } = this.selection;
const cursor = Math.max(start, end); const cursor = Math.max(start, end);
newText = `${text.substr(0, cursor)}${emoji}${text.substr(cursor)}`; let newCursor;
const newCursor = cursor + emoji.length;
this.setInput(newText, { start: newCursor, end: newCursor }); switch (eventType) {
this.setShowSend(true); case EventTypes.BACKSPACE_PRESSED:
logEvent(events.MB_BACKSPACE);
const emojiRegex = /\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]/;
let charsToRemove = 1;
const lastEmoji = text.substr(cursor > 0 ? cursor - 2 : text.length - 2, cursor > 0 ? cursor : text.length);
// Check if last character is an emoji
if (emojiRegex.test(lastEmoji)) charsToRemove = 2;
newText =
text.substr(0, (cursor > 0 ? cursor : text.length) - charsToRemove) + text.substr(cursor > 0 ? cursor : text.length);
newCursor = cursor - charsToRemove;
this.setInput(newText, { start: newCursor, end: newCursor });
this.setShowSend(newText !== '');
break;
case EventTypes.EMOJI_PRESSED:
logEvent(events.MB_EMOJI_SELECTED);
let emojiText = '';
if (typeof emoji === 'string') {
const shortname = `:${emoji}:`;
emojiText = shortnameToUnicode(shortname);
} else {
emojiText = `:${emoji.name}:`;
}
newText = `${text.substr(0, cursor)}${emojiText}${text.substr(cursor)}`;
newCursor = cursor + emojiText.length;
this.setInput(newText, { start: newCursor, end: newCursor });
this.setShowSend(true);
break;
case EventTypes.SEARCH_PRESSED:
logEvent(events.MB_EMOJI_SEARCH_PRESSED);
this.setState({ showEmojiKeyboard: false, showEmojiSearchbar: true });
break;
default:
// Do nothing
}
}; };
getPermalink = async (message: any) => { getPermalink = async (message: any) => {
@ -621,16 +668,20 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
this.setState({ mentions: res, mentionLoading: false }); this.setState({ mentions: res, mentionLoading: false });
}, 300); }, 300);
getEmojis = debounce(async (keyword: any) => { getCustomEmojis = async (keyword: any, count: number) => {
const db = database.active;
const customEmojisCollection = db.get('custom_emojis');
const likeString = sanitizeLikeString(keyword); const likeString = sanitizeLikeString(keyword);
const whereClause = []; const whereClause = [];
if (likeString) { if (likeString) {
whereClause.push(Q.where('name', Q.like(`${likeString}%`))); whereClause.push(Q.where('name', Q.like(`${likeString}%`)));
} }
let customEmojis = await customEmojisCollection.query(...whereClause).fetch(); const db = database.active;
customEmojis = customEmojis.slice(0, MENTIONS_COUNT_TO_DISPLAY); const customEmojisCollection = db.get('custom_emojis');
const customEmojis = await (await customEmojisCollection.query(...whereClause).fetch()).slice(0, count);
return customEmojis;
};
getEmojis = debounce(async (keyword: any) => {
const customEmojis = await this.getCustomEmojis(keyword, MENTIONS_COUNT_TO_DISPLAY);
const filteredEmojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, MENTIONS_COUNT_TO_DISPLAY); const filteredEmojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, MENTIONS_COUNT_TO_DISPLAY);
const mergedEmojis = [...customEmojis, ...filteredEmojis].slice(0, MENTIONS_COUNT_TO_DISPLAY); const mergedEmojis = [...customEmojis, ...filteredEmojis].slice(0, MENTIONS_COUNT_TO_DISPLAY);
this.setState({ mentions: mergedEmojis || [], mentionLoading: false }); this.setState({ mentions: mergedEmojis || [], mentionLoading: false });
@ -881,7 +932,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
openEmoji = () => { openEmoji = () => {
logEvent(events.ROOM_OPEN_EMOJI); logEvent(events.ROOM_OPEN_EMOJI);
this.setState({ showEmojiKeyboard: true }); this.setState({ showEmojiKeyboard: true, showEmojiSearchbar: false });
this.stopTrackingMention();
}; };
recordingCallback = (recording: any) => { recordingCallback = (recording: any) => {
@ -903,7 +955,13 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}; };
closeEmoji = () => { closeEmoji = () => {
this.setState({ showEmojiKeyboard: false }); this.setState({ showEmojiKeyboard: false, showEmojiSearchbar: false });
};
closeEmojiKeyboardAndFocus = () => {
logEvent(events.ROOM_CLOSE_EMOJI);
this.closeEmoji();
this.focus();
}; };
closeEmojiAndAction = (action?: Function, params?: any) => { closeEmojiAndAction = (action?: Function, params?: any) => {
@ -926,7 +984,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
this.clearInput(); this.clearInput();
this.debouncedOnChangeText.stop(); this.debouncedOnChangeText.stop();
this.closeEmoji(); this.closeEmojiKeyboardAndFocus();
this.stopTrackingMention(); this.stopTrackingMention();
this.handleTyping(false); this.handleTyping(false);
if (message.trim() === '' && !showSend) { if (message.trim() === '' && !showSend) {
@ -1083,10 +1141,34 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
); );
}; };
renderEmojiSearchbar = () => {
const { showEmojiSearchbar } = this.state;
return showEmojiSearchbar ? (
<EmojiSearchbar
openEmoji={this.openEmoji}
closeEmoji={this.closeEmoji}
onEmojiSelected={(emoji: IEmoji) => {
this.onKeyboardItemSelected('EmojiKeyboard', { eventType: EventTypes.EMOJI_PRESSED, emoji });
}}
/>
) : null;
};
handleBackPress = () => {
const { showEmojiSearchbar } = this.state;
if (showEmojiSearchbar) {
this.setState({ showEmojiSearchbar: false });
return true;
}
return false;
};
renderContent = () => { renderContent = () => {
const { const {
recording, recording,
showEmojiKeyboard, showEmojiKeyboard,
showEmojiSearchbar,
showSend, showSend,
mentions, mentions,
trackingType, trackingType,
@ -1149,11 +1231,11 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const textInputAndButtons = !recording ? ( const textInputAndButtons = !recording ? (
<> <>
<LeftButtons <LeftButtons
showEmojiKeyboard={showEmojiKeyboard} showEmojiKeyboard={showEmojiKeyboard || showEmojiSearchbar}
editing={editing} editing={editing}
editCancel={this.editCancel} editCancel={this.editCancel}
openEmoji={this.openEmoji} openEmoji={this.openEmoji}
closeEmoji={this.closeEmoji} closeEmoji={this.closeEmojiKeyboardAndFocus}
/> />
<TextInput <TextInput
ref={component => (this.component = component)} ref={component => (this.component = component)}
@ -1197,6 +1279,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
{recordAudio} {recordAudio}
</View> </View>
{this.renderSendToChannel()} {this.renderSendToChannel()}
{this.renderEmojiSearchbar()}
</View> </View>
{children} {children}
</> </>
@ -1224,7 +1307,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
kbComponent={showEmojiKeyboard ? 'EmojiKeyboard' : null} kbComponent={showEmojiKeyboard ? 'EmojiKeyboard' : null}
kbInitialProps={{ theme }} kbInitialProps={{ theme }}
onKeyboardResigned={this.onKeyboardResigned} onKeyboardResigned={this.onKeyboardResigned}
onItemSelected={this.onEmojiSelected} onItemSelected={this.onKeyboardItemSelected}
trackInteractive trackInteractive
requiresSameParentToManageScrollView requiresSameParentToManageScrollView
addBottomView addBottomView

View File

@ -105,7 +105,7 @@ export default StyleSheet.create({
}, },
emojiKeyboardContainer: { emojiKeyboardContainer: {
flex: 1, flex: 1,
borderTopWidth: StyleSheet.hairlineWidth borderTopWidth: 1
}, },
slash: { slash: {
height: 30, height: 30,

View File

@ -23,7 +23,6 @@ interface IAllTabProps {
const AllReactionsListItem = ({ item, getCustomEmoji }: IAllReactionsListItemProps) => { const AllReactionsListItem = ({ item, getCustomEmoji }: IAllReactionsListItemProps) => {
const { colors } = useTheme(); const { colors } = useTheme();
const useRealName = useAppSelector(state => state.settings.UI_Use_Real_Name); const useRealName = useAppSelector(state => state.settings.UI_Use_Real_Name);
const server = useAppSelector(state => state.server.server);
const username = useAppSelector(state => state.login.user.username); const username = useAppSelector(state => state.login.user.username);
const count = item.usernames.length; const count = item.usernames.length;
@ -50,7 +49,6 @@ const AllReactionsListItem = ({ item, getCustomEmoji }: IAllReactionsListItemPro
content={item.emoji} content={item.emoji}
standardEmojiStyle={styles.allTabStandardEmojiStyle} standardEmojiStyle={styles.allTabStandardEmojiStyle}
customEmojiStyle={styles.allTabCustomEmojiStyle} customEmojiStyle={styles.allTabCustomEmojiStyle}
baseUrl={server}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
/> />
<View style={styles.textContainer}> <View style={styles.textContainer}>

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { TGetCustomEmoji, IEmoji } from '../../definitions'; import { TGetCustomEmoji, ICustomEmoji } from '../../definitions';
import ReactionsList from '.'; import ReactionsList from '.';
import { mockedStore as store } from '../../reducers/mockedStore'; import { mockedStore as store } from '../../reducers/mockedStore';
import { updateSettings } from '../../actions/settings'; import { updateSettings } from '../../actions/settings';
@ -11,7 +11,7 @@ const getCustomEmoji: TGetCustomEmoji = content => {
marioparty: { name: content, extension: 'gif' }, marioparty: { name: content, extension: 'gif' },
react_rocket: { name: content, extension: 'png' }, react_rocket: { name: content, extension: 'png' },
nyan_rocket: { name: content, extension: 'png' } nyan_rocket: { name: content, extension: 'png' }
}[content] as IEmoji; }[content] as ICustomEmoji;
return customEmoji; return customEmoji;
}; };

View File

@ -8,7 +8,6 @@ import { TGetCustomEmoji } from '../../definitions/IEmoji';
import I18n from '../../i18n'; import I18n from '../../i18n';
import styles, { MIN_TAB_WIDTH } from './styles'; import styles, { MIN_TAB_WIDTH } from './styles';
import { useDimensions, useOrientation } from '../../dimensions'; import { useDimensions, useOrientation } from '../../dimensions';
import { useAppSelector } from '../../lib/hooks';
interface ITabBarItem { interface ITabBarItem {
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
@ -25,7 +24,6 @@ interface IReactionsTabBar {
const TabBarItem = ({ tab, index, goToPage, getCustomEmoji }: ITabBarItem) => { const TabBarItem = ({ tab, index, goToPage, getCustomEmoji }: ITabBarItem) => {
const { colors } = useTheme(); const { colors } = useTheme();
const server = useAppSelector(state => state.server.server);
return ( return (
<Pressable <Pressable
key={tab.emoji} key={tab.emoji}
@ -46,7 +44,6 @@ const TabBarItem = ({ tab, index, goToPage, getCustomEmoji }: ITabBarItem) => {
content={tab.emoji} content={tab.emoji}
standardEmojiStyle={styles.standardEmojiStyle} standardEmojiStyle={styles.standardEmojiStyle}
customEmojiStyle={styles.customEmojiStyle} customEmojiStyle={styles.customEmojiStyle}
baseUrl={server}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
/> />
<Text style={[styles.reactionCount, { color: colors.auxiliaryTintColor }]}>{tab.usernames.length}</Text> <Text style={[styles.reactionCount, { color: colors.auxiliaryTintColor }]}>{tab.usernames.length}</Text>

View File

@ -10,33 +10,24 @@ interface IEmoji {
literal: string; literal: string;
isMessageContainsOnlyEmoji: boolean; isMessageContainsOnlyEmoji: boolean;
getCustomEmoji?: Function; getCustomEmoji?: Function;
baseUrl: string;
customEmojis?: any; customEmojis?: any;
style?: object; style?: object;
onEmojiSelected?: Function; onEmojiSelected?: Function;
tabEmojiStyle?: object; tabEmojiStyle?: object;
} }
const Emoji = React.memo( const Emoji = React.memo(({ literal, isMessageContainsOnlyEmoji, getCustomEmoji, customEmojis = true, style = {} }: IEmoji) => {
({ literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl, customEmojis = true, style = {} }: IEmoji) => { const { colors } = useTheme();
const { colors } = useTheme(); const emojiUnicode = shortnameToUnicode(literal);
const emojiUnicode = shortnameToUnicode(literal); const emoji: any = getCustomEmoji && getCustomEmoji(literal.replace(/:/g, ''));
const emoji: any = getCustomEmoji && getCustomEmoji(literal.replace(/:/g, '')); if (emoji && customEmojis) {
if (emoji && customEmojis) { return <CustomEmoji style={[isMessageContainsOnlyEmoji ? styles.customEmojiBig : styles.customEmoji, style]} emoji={emoji} />;
return (
<CustomEmoji
baseUrl={baseUrl}
style={[isMessageContainsOnlyEmoji ? styles.customEmojiBig : styles.customEmoji, style]}
emoji={emoji}
/>
);
}
return (
<Text style={[{ color: colors.bodyText }, isMessageContainsOnlyEmoji ? styles.textBig : styles.text, style]}>
{emojiUnicode}
</Text>
);
} }
); return (
<Text style={[{ color: colors.bodyText }, isMessageContainsOnlyEmoji ? styles.textBig : styles.text, style]}>
{emojiUnicode}
</Text>
);
});
export default Emoji; export default Emoji;

View File

@ -4,7 +4,7 @@ import { NavigationContainer } from '@react-navigation/native';
import Markdown, { MarkdownPreview } from '.'; import Markdown, { MarkdownPreview } from '.';
import { themes } from '../../lib/constants'; import { themes } from '../../lib/constants';
import { TGetCustomEmoji, IEmoji } from '../../definitions/IEmoji'; import { TGetCustomEmoji, ICustomEmoji } from '../../definitions/IEmoji';
const theme = 'light'; const theme = 'light';
@ -16,7 +16,6 @@ const styles = StyleSheet.create({
} }
}); });
const baseUrl = 'https://open.rocket.chat';
const longText = const longText =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'; 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
const lineBreakText = `a const lineBreakText = `a
@ -34,7 +33,7 @@ const getCustomEmoji: TGetCustomEmoji = content => {
marioparty: { name: content, extension: 'gif' }, marioparty: { name: content, extension: 'gif' },
react_rocket: { name: content, extension: 'png' }, react_rocket: { name: content, extension: 'png' },
nyan_rocket: { name: content, extension: 'png' } nyan_rocket: { name: content, extension: 'png' }
}[content] as IEmoji; }[content] as ICustomEmoji;
return customEmoji; return customEmoji;
}; };
@ -108,13 +107,8 @@ export const Emoji = () => (
<View style={styles.container}> <View style={styles.container}>
<Markdown msg='Unicode: 😃😇👍' theme={theme} /> <Markdown msg='Unicode: 😃😇👍' theme={theme} />
<Markdown msg='Shortnames: :joy::+1:' theme={theme} /> <Markdown msg='Shortnames: :joy::+1:' theme={theme} />
<Markdown <Markdown msg='Custom emojis: :react_rocket: :nyan_rocket: :marioparty:' theme={theme} getCustomEmoji={getCustomEmoji} />
msg='Custom emojis: :react_rocket: :nyan_rocket: :marioparty:' <Markdown msg='😃 :+1: :marioparty:' theme={theme} getCustomEmoji={getCustomEmoji} />
theme={theme}
getCustomEmoji={getCustomEmoji}
baseUrl={baseUrl}
/>
<Markdown msg='😃 :+1: :marioparty:' theme={theme} getCustomEmoji={getCustomEmoji} baseUrl={baseUrl} />
</View> </View>
); );

View File

@ -33,7 +33,6 @@ interface IMarkdownProps {
md?: MarkdownAST; md?: MarkdownAST;
mentions?: IUserMention[]; mentions?: IUserMention[];
getCustomEmoji?: TGetCustomEmoji; getCustomEmoji?: TGetCustomEmoji;
baseUrl?: string;
username?: string; username?: string;
tmid?: string; tmid?: string;
numberOfLines?: number; numberOfLines?: number;
@ -235,13 +234,12 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
}; };
renderEmoji = ({ literal }: TLiteral) => { renderEmoji = ({ literal }: TLiteral) => {
const { getCustomEmoji, baseUrl = '', customEmojis, style } = this.props; const { getCustomEmoji, customEmojis, style } = this.props;
return ( return (
<MarkdownEmoji <MarkdownEmoji
literal={literal} literal={literal}
isMessageContainsOnlyEmoji={this.isMessageContainsOnlyEmoji} isMessageContainsOnlyEmoji={this.isMessageContainsOnlyEmoji}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
baseUrl={baseUrl}
customEmojis={customEmojis} customEmojis={customEmojis}
style={style} style={style}
/> />
@ -312,18 +310,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
}; };
render() { render() {
const { const { msg, md, mentions, channels, navToRoomInfo, useRealName, username = '', getCustomEmoji, onLinkPress } = this.props;
msg,
md,
mentions,
channels,
navToRoomInfo,
useRealName,
username = '',
getCustomEmoji,
baseUrl = '',
onLinkPress
} = this.props;
if (!msg) { if (!msg) {
return null; return null;
@ -333,7 +320,6 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
return ( return (
<NewMarkdown <NewMarkdown
username={username} username={username}
baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
useRealName={useRealName} useRealName={useRealName}
tokens={md} tokens={md}

View File

@ -3,7 +3,6 @@ import { Text } from 'react-native';
import { Emoji as EmojiProps } from '@rocket.chat/message-parser'; import { Emoji as EmojiProps } from '@rocket.chat/message-parser';
import shortnameToUnicode from '../../../lib/methods/helpers/shortnameToUnicode'; import shortnameToUnicode from '../../../lib/methods/helpers/shortnameToUnicode';
import { themes } from '../../../lib/constants';
import { useTheme } from '../../../theme'; import { useTheme } from '../../../theme';
import styles from '../styles'; import styles from '../styles';
import CustomEmoji from '../../EmojiPicker/CustomEmoji'; import CustomEmoji from '../../EmojiPicker/CustomEmoji';
@ -15,21 +14,21 @@ interface IEmojiProps {
} }
const Emoji = ({ block, isBigEmoji }: IEmojiProps) => { const Emoji = ({ block, isBigEmoji }: IEmojiProps) => {
const { theme } = useTheme(); const { colors } = useTheme();
const { baseUrl, getCustomEmoji } = useContext(MarkdownContext); const { getCustomEmoji } = useContext(MarkdownContext);
if ('unicode' in block) { if ('unicode' in block) {
return <Text style={[{ color: themes[theme].bodyText }, isBigEmoji ? styles.textBig : styles.text]}>{block.unicode}</Text>; return <Text style={[{ color: colors.bodyText }, isBigEmoji ? styles.textBig : styles.text]}>{block.unicode}</Text>;
} }
const emojiToken = block?.shortCode ? `:${block.shortCode}:` : `:${block.value?.value}:`; const emojiToken = block?.shortCode ? `:${block.shortCode}:` : `:${block.value?.value}:`;
const emojiUnicode = shortnameToUnicode(emojiToken); const emojiUnicode = shortnameToUnicode(emojiToken);
const emoji = getCustomEmoji?.(block.value?.value); const emoji = getCustomEmoji?.(block.value?.value);
if (emoji) { if (emoji) {
return <CustomEmoji baseUrl={baseUrl} style={[isBigEmoji ? styles.customEmojiBig : styles.customEmoji]} emoji={emoji} />; return <CustomEmoji style={[isBigEmoji ? styles.customEmojiBig : styles.customEmoji]} emoji={emoji} />;
} }
return ( return (
<Text style={[{ color: themes[theme].bodyText }, isBigEmoji && emojiToken !== emojiUnicode ? styles.textBig : styles.text]}> <Text style={[{ color: colors.bodyText }, isBigEmoji && emojiToken !== emojiUnicode ? styles.textBig : styles.text]}>
{emojiUnicode} {emojiUnicode}
</Text> </Text>
); );

View File

@ -7,7 +7,6 @@ interface IMarkdownContext {
channels?: IUserChannel[]; channels?: IUserChannel[];
useRealName?: boolean; useRealName?: boolean;
username?: string; username?: string;
baseUrl?: string;
navToRoomInfo?: Function; navToRoomInfo?: Function;
getCustomEmoji?: Function; getCustomEmoji?: Function;
onLinkPress?: Function; onLinkPress?: Function;
@ -18,7 +17,6 @@ const defaultState = {
channels: [], channels: [],
useRealName: false, useRealName: false,
username: '', username: '',
baseUrl: '',
navToRoomInfo: () => {} navToRoomInfo: () => {}
}; };

View File

@ -34,11 +34,8 @@ const getCustomEmoji = (content: string) => {
}[content]; }[content];
return customEmoji; return customEmoji;
}; };
const baseUrl = 'https://open.rocket.chat';
const NewMarkdown = ({ ...props }) => ( const NewMarkdown = ({ ...props }) => <NewMarkdownComponent getCustomEmoji={getCustomEmoji} username='rocket.cat' {...props} />;
<NewMarkdownComponent baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} username='rocket.cat' {...props} />
);
const simpleTextMsg = [ const simpleTextMsg = [
{ {
@ -340,7 +337,7 @@ const emojiTokens = [
export const Emoji = () => ( export const Emoji = () => (
<View style={styles.container}> <View style={styles.container}>
<NewMarkdown tokens={bigEmojiTokens} /> <NewMarkdown tokens={bigEmojiTokens} />
<NewMarkdown tokens={emojiTokens} getCustomEmoji={getCustomEmoji} baseUrl={baseUrl} /> <NewMarkdown tokens={emojiTokens} getCustomEmoji={getCustomEmoji} />
</View> </View>
); );

View File

@ -24,7 +24,6 @@ interface IBodyProps {
navToRoomInfo?: Function; navToRoomInfo?: Function;
useRealName?: boolean; useRealName?: boolean;
username: string; username: string;
baseUrl: string;
} }
const Body = ({ const Body = ({
@ -35,7 +34,6 @@ const Body = ({
username, username,
navToRoomInfo, navToRoomInfo,
getCustomEmoji, getCustomEmoji,
baseUrl,
onLinkPress onLinkPress
}: IBodyProps): React.ReactElement | null => { }: IBodyProps): React.ReactElement | null => {
if (isEmpty(tokens)) { if (isEmpty(tokens)) {
@ -51,7 +49,6 @@ const Body = ({
username, username,
navToRoomInfo, navToRoomInfo,
getCustomEmoji, getCustomEmoji,
baseUrl,
onLinkPress onLinkPress
}} }}
> >

View File

@ -283,7 +283,6 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
<Markdown <Markdown
msg={description} msg={description}
style={[isReply && style]} style={[isReply && style]}
baseUrl={baseUrl}
username={user.username} username={user.username}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
theme={theme} theme={theme}

View File

@ -30,7 +30,7 @@ export const Item = () => (
user: { username: 'Marcos' } user: { username: 'Marcos' }
}} }}
> >
<CollapsibleQuote key={0} index={0} attachment={testAttachment} getCustomEmoji={() => {}} timeFormat='LT' /> <CollapsibleQuote key={0} index={0} attachment={testAttachment} getCustomEmoji={() => null} timeFormat='LT' />
</MessageContext.Provider> </MessageContext.Provider>
</View> </View>
); );

View File

@ -82,7 +82,7 @@ interface IMessageReply {
const Fields = React.memo( const Fields = React.memo(
({ attachment, getCustomEmoji }: IMessageFields) => { ({ attachment, getCustomEmoji }: IMessageFields) => {
const { theme } = useTheme(); const { theme } = useTheme();
const { baseUrl, user } = useContext(MessageContext); const { user } = useContext(MessageContext);
if (!attachment.fields) { if (!attachment.fields) {
return null; return null;
@ -97,7 +97,6 @@ const Fields = React.memo(
</Text> </Text>
<Markdown <Markdown
msg={field?.value || ''} msg={field?.value || ''}
baseUrl={baseUrl}
username={user.username} username={user.username}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
theme={theme} theme={theme}

View File

@ -16,7 +16,7 @@ import { MessageTypesValues } from '../../definitions';
const Content = React.memo( const Content = React.memo(
(props: IMessageContent) => { (props: IMessageContent) => {
const { theme } = useTheme(); const { theme } = useTheme();
const { baseUrl, user, onLinkPress } = useContext(MessageContext); const { user, onLinkPress } = useContext(MessageContext);
if (props.isInfo) { if (props.isInfo) {
// @ts-ignore // @ts-ignore
@ -54,7 +54,6 @@ const Content = React.memo(
<Markdown <Markdown
msg={props.msg} msg={props.msg}
md={props.md} md={props.md}
baseUrl={baseUrl}
getCustomEmoji={props.getCustomEmoji} getCustomEmoji={props.getCustomEmoji}
enableMessageParser={user.enableMessageParserEarlyAdoption} enableMessageParser={user.enableMessageParserEarlyAdoption}
username={user.username} username={user.username}

View File

@ -6,11 +6,11 @@ import CustomEmoji from '../EmojiPicker/CustomEmoji';
import { IMessageEmoji } from './interfaces'; import { IMessageEmoji } from './interfaces';
const Emoji = React.memo( const Emoji = React.memo(
({ content, baseUrl, standardEmojiStyle, customEmojiStyle, getCustomEmoji }: IMessageEmoji) => { ({ content, standardEmojiStyle, customEmojiStyle, getCustomEmoji }: IMessageEmoji) => {
const parsedContent = content.replace(/^:|:$/g, ''); const parsedContent = content.replace(/^:|:$/g, '');
const emoji = getCustomEmoji(parsedContent); const emoji = getCustomEmoji(parsedContent);
if (emoji) { if (emoji) {
return <CustomEmoji key={content} baseUrl={baseUrl} style={customEmojiStyle} emoji={emoji} />; return <CustomEmoji key={content} style={customEmojiStyle} emoji={emoji} />;
} }
return <Text style={standardEmojiStyle}>{shortnameToUnicode(content)}</Text>; return <Text style={standardEmojiStyle}>{shortnameToUnicode(content)}</Text>;
}, },

View File

@ -81,7 +81,6 @@ const ImageContainer = React.memo(
<Markdown <Markdown
msg={file.description} msg={file.description}
style={[isReply && style]} style={[isReply && style]}
baseUrl={baseUrl}
username={user.username} username={user.username}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
theme={theme} theme={theme}

View File

@ -47,7 +47,7 @@ const AddReaction = React.memo(({ theme }: { theme: TSupportedThemes }) => {
}); });
const Reaction = React.memo(({ reaction, getCustomEmoji, theme }: IMessageReaction) => { const Reaction = React.memo(({ reaction, getCustomEmoji, theme }: IMessageReaction) => {
const { onReactionPress, onReactionLongPress, baseUrl, user } = useContext(MessageContext); const { onReactionPress, onReactionLongPress, user } = useContext(MessageContext);
const reacted = reaction.usernames.findIndex((item: string) => item === user.username) !== -1; const reacted = reaction.usernames.findIndex((item: string) => item === user.username) !== -1;
return ( return (
<Touchable <Touchable
@ -67,7 +67,6 @@ const Reaction = React.memo(({ reaction, getCustomEmoji, theme }: IMessageReacti
content={reaction.emoji} content={reaction.emoji}
standardEmojiStyle={styles.reactionEmoji} standardEmojiStyle={styles.reactionEmoji}
customEmojiStyle={styles.reactionCustomEmoji} customEmojiStyle={styles.reactionCustomEmoji}
baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
/> />
<Text style={[styles.reactionCount, { color: themes[theme].tintColor }]}>{reaction.usernames.length}</Text> <Text style={[styles.reactionCount, { color: themes[theme].tintColor }]}>{reaction.usernames.length}</Text>

View File

@ -121,7 +121,7 @@ const Description = React.memo(
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
theme: TSupportedThemes; theme: TSupportedThemes;
}) => { }) => {
const { baseUrl, user } = useContext(MessageContext); const { user } = useContext(MessageContext);
const text = attachment.text || attachment.title; const text = attachment.text || attachment.title;
if (!text) { if (!text) {
@ -132,7 +132,6 @@ const Description = React.memo(
<Markdown <Markdown
msg={text} msg={text}
style={[{ color: themes[theme].auxiliaryTintColor, fontSize: 14 }]} style={[{ color: themes[theme].auxiliaryTintColor, fontSize: 14 }]}
baseUrl={baseUrl}
username={user.username} username={user.username}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
theme={theme} theme={theme}
@ -177,7 +176,7 @@ const Fields = React.memo(
theme: TSupportedThemes; theme: TSupportedThemes;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
}) => { }) => {
const { baseUrl, user } = useContext(MessageContext); const { user } = useContext(MessageContext);
if (!attachment.fields) { if (!attachment.fields) {
return null; return null;
@ -188,13 +187,7 @@ const Fields = React.memo(
{attachment.fields.map(field => ( {attachment.fields.map(field => (
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}> <View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
<Text style={[styles.fieldTitle, { color: themes[theme].bodyText }]}>{field.title}</Text> <Text style={[styles.fieldTitle, { color: themes[theme].bodyText }]}>{field.title}</Text>
<Markdown <Markdown msg={field?.value || ''} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
msg={field?.value || ''}
baseUrl={baseUrl}
username={user.username}
getCustomEmoji={getCustomEmoji}
theme={theme}
/>
</View> </View>
))} ))}
</View> </View>
@ -278,13 +271,7 @@ const Reply = React.memo(
) : null} ) : null}
</View> </View>
</Touchable> </Touchable>
<Markdown <Markdown msg={attachment.description} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
msg={attachment.description}
baseUrl={baseUrl}
username={user.username}
getCustomEmoji={getCustomEmoji}
theme={theme}
/>
</> </>
); );
}, },

View File

@ -79,7 +79,6 @@ const Video = React.memo(
<> <>
<Markdown <Markdown
msg={file.description} msg={file.description}
baseUrl={baseUrl}
username={user.username} username={user.username}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
style={[isReply && style]} style={[isReply && style]}

View File

@ -67,7 +67,6 @@ export interface IMessageContent {
export interface IMessageEmoji { export interface IMessageEmoji {
content: string; content: string;
baseUrl: string;
standardEmojiStyle: { fontSize: number }; standardEmojiStyle: { fontSize: number };
customEmojiStyle: StyleProp<ImageStyle>; customEmojiStyle: StyleProp<ImageStyle>;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;

View File

@ -1,43 +1,35 @@
import Model from '@nozbe/watermelondb/Model'; import Model from '@nozbe/watermelondb/Model';
import { StyleProp } from 'react-native';
import { ImageStyle } from 'react-native-fast-image';
export interface IEmoji { export interface IFrequentlyUsedEmoji {
content: string; content: string;
name: string; extension?: string;
extension: string;
isCustom: boolean; isCustom: boolean;
count?: number; count?: number;
} }
export interface ICustomEmojis { type TBasicEmoji = string;
[key: string]: Pick<IEmoji, 'name' | 'extension'>;
}
export interface ICustomEmoji { export interface ICustomEmoji {
baseUrl?: string; name: string;
emoji: IEmoji; extension: string;
style: StyleProp<ImageStyle>;
} }
export type IEmoji = ICustomEmoji | TBasicEmoji;
export interface ICustomEmojis {
[key: string]: ICustomEmoji;
}
export type TGetCustomEmoji = (name: string) => ICustomEmoji | null;
export type TFrequentlyUsedEmojiModel = IFrequentlyUsedEmoji & Model;
export interface ICustomEmojiModel { export interface ICustomEmojiModel {
_id: string; _id: string;
name?: string; name: string;
aliases?: string[]; aliases?: string[];
extension: string; extension: string;
_updatedAt: Date; _updatedAt: Date;
} }
export interface IEmojiCategory {
baseUrl: string;
emojis: IEmoji[];
onEmojiSelected: (emoji: IEmoji) => void;
width: number | null;
style: StyleProp<ImageStyle>;
tabLabel: string;
}
export type TGetCustomEmoji = (name: string) => any;
export type TFrequentlyUsedEmojiModel = IEmoji & Model;
export type TCustomEmojiModel = ICustomEmojiModel & Model; export type TCustomEmojiModel = ICustomEmojiModel & Model;

View File

@ -480,6 +480,7 @@
"Search_Messages": "Search Messages", "Search_Messages": "Search Messages",
"Search": "Search", "Search": "Search",
"Search_by": "Search by", "Search_by": "Search by",
"Search_emoji": "Search emoji",
"Search_global_users": "Search for global users", "Search_global_users": "Search for global users",
"Search_global_users_description": "If you turn-on, you can search for any user from others companies or servers.", "Search_global_users_description": "If you turn-on, you can search for any user from others companies or servers.",
"Seconds": "{{second}} seconds", "Seconds": "{{second}} seconds",

View File

@ -457,6 +457,7 @@
"Search_Messages": "Buscar Mensagens", "Search_Messages": "Buscar Mensagens",
"Search": "Buscar", "Search": "Buscar",
"Search_by": "Buscar por", "Search_by": "Buscar por",
"Search_emoji": "Buscar emoji",
"Search_global_users": "Busca por usuários globais", "Search_global_users": "Busca por usuários globais",
"Search_global_users_description": "Caso ativado, busca por usuários de outras empresas ou servidores.", "Search_global_users_description": "Caso ativado, busca por usuários de outras empresas ou servidores.",
"Security_and_privacy": "Segurança e privacidade", "Security_and_privacy": "Segurança e privacidade",

View File

@ -70,6 +70,7 @@ export const colors = {
collapsibleQuoteBorder: '#CBCED1', collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A', collapsibleChevron: '#6C727A',
cancelButton: '#E4E7EA', cancelButton: '#E4E7EA',
textInputSecondaryBackground: '#E4E7EA',
...mentions ...mentions
}, },
dark: { dark: {
@ -122,6 +123,7 @@ export const colors = {
collapsibleQuoteBorder: '#CBCED1', collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A', collapsibleChevron: '#6C727A',
cancelButton: '#E4E7EA', cancelButton: '#E4E7EA',
textInputSecondaryBackground: '#030b1b', // backgroundColor
...mentions ...mentions
}, },
black: { black: {
@ -174,6 +176,7 @@ export const colors = {
collapsibleQuoteBorder: '#CBCED1', collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A', collapsibleChevron: '#6C727A',
cancelButton: '#E4E7EA', cancelButton: '#E4E7EA',
textInputSecondaryBackground: '#000000', // backgroundColor
...mentions ...mentions
} }
}; };

View File

@ -1,44 +1,44 @@
const list = ['frequentlyUsed', 'custom', 'people', 'nature', 'food', 'activity', 'travel', 'objects', 'symbols', 'flags']; const list = ['frequentlyUsed', 'custom', 'people', 'nature', 'food', 'activity', 'travel', 'objects', 'symbols', 'flags'];
const tabs = [ const tabs = [
{ {
tabLabel: '🕒', tabLabel: 'clock',
category: list[0] category: list[0]
}, },
{ {
tabLabel: '🚀', tabLabel: 'rocket',
category: list[1] category: list[1]
}, },
{ {
tabLabel: '😃', tabLabel: 'emoji',
category: list[2] category: list[2]
}, },
{ {
tabLabel: '🐶', tabLabel: 'leaf',
category: list[3] category: list[3]
}, },
{ {
tabLabel: '🍔', tabLabel: 'burger',
category: list[4] category: list[4]
}, },
{ {
tabLabel: '', tabLabel: 'basketball',
category: list[5] category: list[5]
}, },
{ {
tabLabel: '🚌', tabLabel: 'airplane',
category: list[6] category: list[6]
}, },
{ {
tabLabel: '💡', tabLabel: 'lamp-bulb',
category: list[7] category: list[7]
}, },
{ {
tabLabel: '💛', tabLabel: 'percentage',
category: list[8] category: list[8]
}, },
{ {
tabLabel: '🏁', tabLabel: 'flag',
category: list[9] category: list[9]
} }
]; ];
export default { list, tabs }; export const categories = { list, tabs };

View File

@ -2813,3 +2813,5 @@ export const emojis = [
'flag_tc', 'flag_tc',
'flag_mf' 'flag_mf'
]; ];
export const DEFAULT_EMOJIS = ['clap', 'thumbsup', 'heart_eyes', 'grinning', 'thinking', 'smiley'];

View File

@ -0,0 +1,2 @@
export * from './emojis';
export * from './categories';

View File

@ -1,5 +1,6 @@
export * from './colors'; export * from './colors';
export * from './constantDisplayMode'; export * from './constantDisplayMode';
export * from './emojis';
export * from './environment'; export * from './environment';
export * from './keys'; export * from './keys';
export * from './links'; export * from './links';

View File

@ -1,2 +1,3 @@
export * from './useAppSelector'; export * from './useAppSelector';
export * from './usePermissions'; export * from './usePermissions';
export * from './useFrequentlyUsedEmoji';

View File

@ -0,0 +1,43 @@
import { useEffect, useState } from 'react';
import { unstable_batchedUpdates } from 'react-native';
import { Q } from '@nozbe/watermelondb';
import database from '../database';
import { IEmoji } from '../../definitions';
import { DEFAULT_EMOJIS } from '../constants';
export const useFrequentlyUsedEmoji = (
withDefaultEmojis = false
): {
frequentlyUsed: IEmoji[];
loaded: boolean;
} => {
const [frequentlyUsed, setFrequentlyUsed] = useState<IEmoji[]>([]);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const getFrequentlyUsedEmojis = async () => {
const db = database.active;
const frequentlyUsedRecords = await db.get('frequently_used_emojis').query(Q.experimentalSortBy('count', Q.desc)).fetch();
let frequentlyUsedEmojis = frequentlyUsedRecords.map(item => {
if (item.isCustom) {
return { name: item.content, extension: item.extension! }; // if isCustom is true, extension is not null
}
return item.content;
});
if (withDefaultEmojis && frequentlyUsedEmojis.length < DEFAULT_EMOJIS.length) {
frequentlyUsedEmojis = frequentlyUsedEmojis
.concat(DEFAULT_EMOJIS.filter(de => !frequentlyUsedEmojis.find(fue => typeof fue === 'string' && fue === de)))
.slice(0, DEFAULT_EMOJIS.length);
}
// TODO: remove once we update to React 18
unstable_batchedUpdates(() => {
setFrequentlyUsed(frequentlyUsedEmojis);
setLoaded(true);
});
};
getFrequentlyUsedEmojis();
}, []);
return { frequentlyUsed, loaded };
};

67
app/lib/methods/emojis.ts Normal file
View File

@ -0,0 +1,67 @@
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { Q } from '@nozbe/watermelondb';
import database from '../database';
import { IEmoji, TFrequentlyUsedEmojiModel } from '../../definitions';
import log from './helpers/log';
import { sanitizeLikeString } from '../database/utils';
import { emojis } from '../constants';
export const addFrequentlyUsed = async (emoji: IEmoji) => {
const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojiRecord: TFrequentlyUsedEmojiModel;
try {
if (typeof emoji === 'string') {
freqEmojiRecord = await freqEmojiCollection.find(emoji);
} else {
freqEmojiRecord = await freqEmojiCollection.find(emoji.name);
}
} catch (error) {
// Do nothing
}
try {
await db.write(async () => {
if (freqEmojiRecord) {
await freqEmojiRecord.update(f => {
if (f.count) {
f.count += 1;
}
});
} else {
await freqEmojiCollection.create(f => {
if (typeof emoji === 'string') {
f._raw = sanitizedRaw({ id: emoji }, freqEmojiCollection.schema);
Object.assign(f, { content: emoji, isCustom: false });
} else {
f._raw = sanitizedRaw({ id: emoji.name }, freqEmojiCollection.schema);
Object.assign(f, { content: emoji.name, extension: emoji.extension, isCustom: true });
}
f.count = 1;
});
}
});
} catch (e) {
log(e);
}
};
export const searchEmojis = async (keyword: string): Promise<IEmoji[]> => {
const likeString = sanitizeLikeString(keyword);
const whereClause = [];
if (likeString) {
whereClause.push(Q.where('name', Q.like(`${likeString}%`)));
}
const db = database.active;
const customEmojisCollection = await db
.get('custom_emojis')
.query(...whereClause)
.fetch();
const customEmojis = customEmojisCollection?.map(emoji => ({
name: emoji?.name,
extension: emoji?.extension
}));
const filteredEmojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1);
return [...customEmojis, ...filteredEmojis];
};

View File

@ -187,6 +187,7 @@ export default {
ROOM_SEND_MESSAGE: 'room_send_message', ROOM_SEND_MESSAGE: 'room_send_message',
ROOM_ENCRYPTED_PRESS: 'room_encrypted_press', ROOM_ENCRYPTED_PRESS: 'room_encrypted_press',
ROOM_OPEN_EMOJI: 'room_open_emoji', ROOM_OPEN_EMOJI: 'room_open_emoji',
ROOM_CLOSE_EMOJI: 'room_close_emoji',
ROOM_AUDIO_RECORD: 'room_audio_record', ROOM_AUDIO_RECORD: 'room_audio_record',
ROOM_AUDIO_RECORD_F: 'room_audio_record_f', ROOM_AUDIO_RECORD_F: 'room_audio_record_f',
ROOM_AUDIO_FINISH: 'room_audio_finish', ROOM_AUDIO_FINISH: 'room_audio_finish',
@ -238,6 +239,13 @@ export default {
ROOM_MENTION_GO_USER_INFO: 'room_mention_go_user_info', ROOM_MENTION_GO_USER_INFO: 'room_mention_go_user_info',
COMMAND_RUN: 'command_run', COMMAND_RUN: 'command_run',
COMMAND_RUN_F: 'command_run_f', COMMAND_RUN_F: 'command_run_f',
MB_BACKSPACE: 'mb_backspace',
MB_EMOJI_SELECTED: 'mb_emoji_selected',
MB_EMOJI_SEARCH_PRESSED: 'mb_emoji_search_pressed',
MB_SB_EMOJI_SEARCH: 'mb_sb_emoji_search',
MB_SB_EMOJI_SELECTED: 'mb_sb_emoji_selected',
REACTION_PICKER_EMOJI_SELECTED: 'reaction_picker_emoji_selected',
REACTION_PICKER_SEARCH_EMOJIS: 'reaction_picker_search_emojis',
// ROOM ACTIONS VIEW // ROOM ACTIONS VIEW
RA_JITSI_VIDEO: 'ra_jitsi_video', RA_JITSI_VIDEO: 'ra_jitsi_video',

View File

@ -3,6 +3,7 @@ export * from './callJitsi';
export * from './canOpenRoom'; export * from './canOpenRoom';
export * from './clearCache'; export * from './clearCache';
export * from './enterpriseModules'; export * from './enterpriseModules';
export * from './emojis';
export * from './getCustomEmojis'; export * from './getCustomEmojis';
export * from './getPermalinks'; export * from './getPermalinks';
export * from './getPermissions'; export * from './getPermissions';

View File

@ -23,13 +23,14 @@ import { IRoomInfoParam } from '../SearchMessagesView';
import { import {
IApplicationState, IApplicationState,
TMessageModel, TMessageModel,
IEmoji,
ISubscription, ISubscription,
SubscriptionType, SubscriptionType,
IAttachment, IAttachment,
IMessage, IMessage,
TAnyMessageModel, TAnyMessageModel,
IUrl IUrl,
TGetCustomEmoji,
ICustomEmoji
} from '../../definitions'; } from '../../definitions';
import { Services } from '../../lib/services'; import { Services } from '../../lib/services';
@ -45,7 +46,7 @@ interface IMessagesViewProps {
StackNavigationProp<MasterDetailInsideStackParamList> StackNavigationProp<MasterDetailInsideStackParamList>
>; >;
route: RouteProp<ChatsStackParamList, 'MessagesView'>; route: RouteProp<ChatsStackParamList, 'MessagesView'>;
customEmojis: { [key: string]: IEmoji }; customEmojis: { [key: string]: ICustomEmoji };
theme: TSupportedThemes; theme: TSupportedThemes;
showActionSheet: (params: { options: string[]; hasCancel: boolean }) => void; showActionSheet: (params: { options: string[]; hasCancel: boolean }) => void;
useRealName: boolean; useRealName: boolean;
@ -297,7 +298,7 @@ class MessagesView extends React.Component<IMessagesViewProps, IMessagesViewStat
} }
}; };
getCustomEmoji = (name: string) => { getCustomEmoji: TGetCustomEmoji = name => {
const { customEmojis } = this.props; const { customEmojis } = this.props;
const emoji = customEmojis[name]; const emoji = customEmojis[name];
if (emoji) { if (emoji) {

View File

@ -1,85 +1,52 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { connect } from 'react-redux';
import Modal from 'react-native-modal';
import EmojiPicker from '../../containers/EmojiPicker'; import EmojiPicker from '../../containers/EmojiPicker';
import { isAndroid } from '../../lib/methods/helpers';
import { themes } from '../../lib/constants';
import { TSupportedThemes, withTheme } from '../../theme';
import styles from './styles'; import styles from './styles';
import { IApplicationState } from '../../definitions'; import { IEmoji } from '../../definitions';
import { EventTypes } from '../../containers/EmojiPicker/interfaces';
const margin = isAndroid ? 40 : 20; import { searchEmojis } from '../../lib/methods';
const maxSize = 400; import { useDebounce } from '../../lib/methods/helpers/debounce';
import { EmojiSearch } from '../../containers/EmojiPicker/EmojiSearch';
import { events, logEvent } from '../../lib/methods/helpers/log';
interface IReactionPickerProps { interface IReactionPickerProps {
message?: any; message?: any;
show: boolean;
isMasterDetail: boolean;
reactionClose: () => void; reactionClose: () => void;
onEmojiSelected: (shortname: string, id: string) => void; onEmojiSelected: (emoji: IEmoji, id: string) => void;
width: number;
height: number;
theme: TSupportedThemes;
} }
class ReactionPicker extends React.Component<IReactionPickerProps> { const ReactionPicker = ({ onEmojiSelected, message, reactionClose }: IReactionPickerProps): React.ReactElement => {
shouldComponentUpdate(nextProps: IReactionPickerProps) { const [searchedEmojis, setSearchedEmojis] = React.useState<IEmoji[]>([]);
const { show, width, height } = this.props; const [searching, setSearching] = React.useState<boolean>(false);
return nextProps.show !== show || width !== nextProps.width || height !== nextProps.height;
}
onEmojiSelected = (emoji: string, shortname?: string) => { const handleTextChange = useDebounce((text: string) => {
// standard emojis: `emoji` is unicode and `shortname` is :joy: setSearching(text !== '');
// custom emojis: only `emoji` is returned with shortname type (:joy:) handleSearchEmojis(text);
// to set reactions, we need shortname type }, 300);
const { onEmojiSelected, message } = this.props;
if (message) { const handleSearchEmojis = async (text: string) => {
onEmojiSelected(shortname || emoji, message.id); logEvent(events.REACTION_PICKER_SEARCH_EMOJIS);
} const emojis = await searchEmojis(text);
setSearchedEmojis(emojis);
}; };
render() { const handleEmojiSelect = (_eventType: EventTypes, emoji?: IEmoji) => {
const { width, height, show, reactionClose, isMasterDetail, theme } = this.props; logEvent(events.REACTION_PICKER_EMOJI_SELECTED);
if (message && emoji) {
let widthStyle = width - margin; onEmojiSelected(emoji, message.id);
let heightStyle = Math.min(width, height) - margin * 2;
if (isMasterDetail) {
widthStyle = maxSize;
heightStyle = maxSize;
} }
reactionClose();
};
return show ? ( return (
<Modal <View style={styles.reactionPickerContainer} testID='reaction-picker'>
isVisible={show} <View style={styles.reactionSearchContainer}>
style={{ alignItems: 'center' }} <EmojiSearch onChangeText={handleTextChange} bottomSheet />
onBackdropPress={reactionClose} </View>
onBackButtonPress={reactionClose} <EmojiPicker onItemClicked={handleEmojiSelect} searching={searching} searchedEmojis={searchedEmojis} />
animationIn='fadeIn' </View>
animationOut='fadeOut' );
backdropOpacity={themes[theme].backdropOpacity} };
>
<View
style={[
styles.reactionPickerContainer,
{
width: widthStyle,
height: heightStyle
}
]}
testID='reaction-picker'
>
<EmojiPicker theme={theme} onEmojiSelected={this.onEmojiSelected} />
</View>
</Modal>
) : null;
}
}
const mapStateToProps = (state: IApplicationState) => ({ export default ReactionPicker;
isMasterDetail: state.app.isMasterDetail
});
export default connect(mapStateToProps)(withTheme(ReactionPicker));

View File

@ -74,8 +74,9 @@ import {
TMessageModel, TMessageModel,
TSubscriptionModel, TSubscriptionModel,
TThreadModel, TThreadModel,
ICustomEmojis,
IEmoji, IEmoji,
ICustomEmojis TGetCustomEmoji
} from '../../definitions'; } from '../../definitions';
import { E2E_MESSAGE_TYPE, E2E_STATUS, MESSAGE_TYPE_ANY_LOAD, MessageTypeLoad, themes } from '../../lib/constants'; import { E2E_MESSAGE_TYPE, E2E_STATUS, MESSAGE_TYPE_ANY_LOAD, MessageTypeLoad, themes } from '../../lib/constants';
import { TListRef } from './List/List'; import { TListRef } from './List/List';
@ -112,7 +113,6 @@ const stateAttrsUpdate = [
'loading', 'loading',
'editing', 'editing',
'replying', 'replying',
'reacting',
'readOnly', 'readOnly',
'member', 'member',
'canForwardGuest', 'canForwardGuest',
@ -164,7 +164,6 @@ interface IRoomViewProps extends IActionSheetProvider, IBaseScreen<ChatsStackPar
isMasterDetail: boolean; isMasterDetail: boolean;
replyBroadcast: Function; replyBroadcast: Function;
width: number; width: number;
height: number;
insets: EdgeInsets; insets: EdgeInsets;
transferLivechatGuestPermission?: string[]; // TODO: Check if its the correct type transferLivechatGuestPermission?: string[]; // TODO: Check if its the correct type
viewCannedResponsesPermission?: string[]; // TODO: Check if its the correct type viewCannedResponsesPermission?: string[]; // TODO: Check if its the correct type
@ -200,7 +199,6 @@ interface IRoomViewState {
editing: boolean; editing: boolean;
replying: boolean; replying: boolean;
replyWithMention: boolean; replyWithMention: boolean;
reacting: boolean;
readOnly: boolean; readOnly: boolean;
unreadsCount: number | null; unreadsCount: number | null;
roomUserId?: string | null; roomUserId?: string | null;
@ -274,7 +272,6 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
editing: false, editing: false,
replying: !!selectedMessage, replying: !!selectedMessage,
replyWithMention: false, replyWithMention: false,
reacting: false,
readOnly: false, readOnly: false,
unreadsCount: null, unreadsCount: null,
roomUserId, roomUserId,
@ -828,12 +825,27 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
this.setState({ selectedMessage: undefined, replying: false, replyWithMention: false }); this.setState({ selectedMessage: undefined, replying: false, replyWithMention: false });
}; };
showReactionPicker = () => {
const { showActionSheet } = this.props;
const { selectedMessage } = this.state;
showActionSheet({
children: (
<ReactionPicker message={selectedMessage} onEmojiSelected={this.onReactionPress} reactionClose={this.onReactionClose} />
),
snaps: [400],
enableContentPanningGesture: false
});
};
onReactionInit = (message: TAnyMessageModel) => { onReactionInit = (message: TAnyMessageModel) => {
this.setState({ selectedMessage: message, reacting: true }); this.messagebox?.current?.closeEmojiAndAction(() => {
this.setState({ selectedMessage: message }, this.showReactionPicker);
});
}; };
onReactionClose = () => { onReactionClose = () => {
this.setState({ selectedMessage: undefined, reacting: false }); const { hideActionSheet } = this.props;
this.setState({ selectedMessage: undefined }, hideActionSheet);
}; };
onMessageLongPress = (message: TAnyMessageModel) => { onMessageLongPress = (message: TAnyMessageModel) => {
@ -850,8 +862,14 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
navigation.navigate('AttachmentView', { attachment }); navigation.navigate('AttachmentView', { attachment });
}; };
onReactionPress = async (shortname: string, messageId: string) => { onReactionPress = async (emoji: IEmoji, messageId: string) => {
try { try {
let shortname = '';
if (typeof emoji === 'string') {
shortname = emoji;
} else {
shortname = emoji.name;
}
await Services.setReaction(shortname, messageId); await Services.setReaction(shortname, messageId);
this.onReactionClose(); this.onReactionClose();
Review.pushPositiveEvent(); Review.pushPositiveEvent();
@ -1026,7 +1044,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
}); });
}; };
getCustomEmoji = (name: string): Pick<IEmoji, 'name' | 'extension'> | null => { getCustomEmoji: TGetCustomEmoji = name => {
const { customEmojis } = this.props; const { customEmojis } = this.props;
const emoji = customEmojis[name]; const emoji = customEmojis[name];
if (emoji) { if (emoji) {
@ -1489,8 +1507,8 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
render() { render() {
console.count(`${this.constructor.name}.render calls`); console.count(`${this.constructor.name}.render calls`);
const { room, selectedMessage, loading, reacting } = this.state; const { room, loading } = this.state;
const { user, baseUrl, theme, navigation, Hide_System_Messages, width, height, serverVersion } = this.props; const { user, baseUrl, theme, navigation, Hide_System_Messages, width, serverVersion } = this.props;
const { rid, t } = room; const { rid, t } = room;
let sysMes; let sysMes;
let bannerClosed; let bannerClosed;
@ -1521,15 +1539,6 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
/> />
{this.renderFooter()} {this.renderFooter()}
{this.renderActions()} {this.renderActions()}
<ReactionPicker
show={reacting}
message={selectedMessage}
onEmojiSelected={this.onReactionPress}
reactionClose={this.onReactionClose}
width={width}
height={height}
theme={theme}
/>
<UploadProgress rid={rid} user={user} baseUrl={baseUrl} width={width} /> <UploadProgress rid={rid} user={user} baseUrl={baseUrl} width={width} />
<JoinCode ref={this.joinCode} onJoin={this.onJoin} rid={rid} t={t} theme={theme} /> <JoinCode ref={this.joinCode} onJoin={this.onJoin} rid={rid} t={t} theme={theme} />
</SafeAreaView> </SafeAreaView>

View File

@ -14,10 +14,13 @@ export default StyleSheet.create({
alignItems: 'center', alignItems: 'center',
marginVertical: 15 marginVertical: 15
}, },
reactionSearchContainer: {
marginHorizontal: 12,
marginBottom: 8
},
reactionPickerContainer: { reactionPickerContainer: {
borderRadius: 4, flex: 1,
flexDirection: 'column', flexDirection: 'column'
overflow: 'hidden'
}, },
bannerContainer: { bannerContainer: {
paddingVertical: 12, paddingVertical: 12,
@ -64,5 +67,14 @@ export default StyleSheet.create({
previewMode: { previewMode: {
fontSize: 16, fontSize: 16,
...sharedStyles.textMedium ...sharedStyles.textMedium
},
searchbarContainer: {
height: 56,
marginBottom: 8,
paddingHorizontal: 12
},
reactionPickerSearchbar: {
paddingHorizontal: 20,
minHeight: 48
} }
}); });

View File

@ -31,11 +31,12 @@ import {
IUser, IUser,
TMessageModel, TMessageModel,
IUrl, IUrl,
IEmoji,
IAttachment, IAttachment,
ISubscription, ISubscription,
SubscriptionType, SubscriptionType,
TSubscriptionModel TSubscriptionModel,
TGetCustomEmoji,
ICustomEmoji
} from '../../definitions'; } from '../../definitions';
import { Services } from '../../lib/services'; import { Services } from '../../lib/services';
@ -68,7 +69,7 @@ interface ISearchMessagesViewProps extends INavigationOption {
baseUrl: string; baseUrl: string;
serverVersion: string; serverVersion: string;
customEmojis: { customEmojis: {
[key: string]: IEmoji; [key: string]: ICustomEmoji;
}; };
theme: TSupportedThemes; theme: TSupportedThemes;
useRealName: boolean; useRealName: boolean;
@ -201,7 +202,7 @@ class SearchMessagesView extends React.Component<ISearchMessagesViewProps, ISear
await this.getMessages(searchText, true); await this.getMessages(searchText, true);
}, 1000); }, 1000);
getCustomEmoji = (name: string) => { getCustomEmoji: TGetCustomEmoji = name => {
const { customEmojis } = this.props; const { customEmojis } = this.props;
const emoji = customEmojis[name]; const emoji = customEmojis[name];
if (emoji) { if (emoji) {

View File

@ -107,7 +107,9 @@ describe('Profile screen', () => {
it('should change email and password', async () => { it('should change email and password', async () => {
await element(by.id('profile-view-email')).replaceText(`mobile+profileChangesNew${data.random}@rocket.chat`); await element(by.id('profile-view-email')).replaceText(`mobile+profileChangesNew${data.random}@rocket.chat`);
await element(by.id('profile-view-new-password')).replaceText(`${profileChangeUser.password}new`); await element(by.id('profile-view-new-password')).replaceText(`${profileChangeUser.password}new`);
await sleep(300); await waitFor(element(by.id('profile-view-submit')))
.toExist()
.withTimeout(2000);
await element(by.id('profile-view-submit')).tap(); await element(by.id('profile-view-submit')).tap();
await waitFor(element(by.id('profile-view-enter-password-sheet'))) await waitFor(element(by.id('profile-view-enter-password-sheet')))
.toBeVisible() .toBeVisible()

View File

@ -8,8 +8,6 @@ import {
tapBack, tapBack,
sleep, sleep,
searchRoom, searchRoom,
starMessage,
pinMessage,
dismissReviewNag, dismissReviewNag,
tryTapping, tryTapping,
platformTypes, platformTypes,
@ -63,9 +61,7 @@ describe('Room screen', () => {
}); });
it('should have open emoji button', async () => { it('should have open emoji button', async () => {
if (device.getPlatform() === 'android') { await expect(element(by.id('messagebox-open-emoji'))).toExist();
await expect(element(by.id('messagebox-open-emoji'))).toExist();
}
}); });
it('should have message input', async () => { it('should have message input', async () => {
@ -89,24 +85,110 @@ describe('Room screen', () => {
await expect(element(by[textMatcher](`${data.random}message`)).atIndex(0)).toExist(); await expect(element(by[textMatcher](`${data.random}message`)).atIndex(0)).toExist();
}); });
it('should show/hide emoji keyboard', async () => { describe('Emoji Keyboard', () => {
if (device.getPlatform() === 'android') { it('should open emoji keyboard, select an emoji and send it', async () => {
await element(by.id('messagebox-open-emoji')).tap(); await element(by.id('messagebox-open-emoji')).tap();
await waitFor(element(by.id('messagebox-keyboard-emoji'))) await waitFor(element(by.id('messagebox-keyboard-emoji')))
.toExist() .toExist()
.withTimeout(10000); .withTimeout(10000);
await expect(element(by.id('messagebox-close-emoji'))).toExist(); await expect(element(by.id('emoji-picker-tab-emoji'))).toExist();
await expect(element(by.id('messagebox-open-emoji'))).toBeNotVisible(); await element(by.id('emoji-picker-tab-emoji')).tap();
await expect(element(by.id('emoji-blush'))).toExist();
await element(by.id('emoji-blush')).tap();
await expect(element(by.id('messagebox-input'))).toHaveText('😊');
await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by[textMatcher]('😊')))
.toExist()
.withTimeout(60000);
await element(by[textMatcher]('😊')).atIndex(0).tap();
});
it('should open emoji keyboard, select an emoji and delete it using emoji keyboards backspace', async () => {
await element(by.id('messagebox-open-emoji')).tap();
await waitFor(element(by.id('messagebox-keyboard-emoji')))
.toExist()
.withTimeout(10000);
await expect(element(by.id('emoji-picker-tab-emoji'))).toExist();
await element(by.id('emoji-picker-tab-emoji')).tap();
await expect(element(by.id('emoji-upside_down'))).toExist();
await element(by.id('emoji-upside_down')).tap();
await expect(element(by.id('messagebox-input'))).toHaveText('🙃');
await waitFor(element(by.id('emoji-picker-backspace')))
.toExist()
.withTimeout(2000);
await element(by.id('emoji-picker-backspace')).tap();
await expect(element(by.id('messagebox-input'))).toHaveText('');
await element(by.id('messagebox-close-emoji')).tap(); await element(by.id('messagebox-close-emoji')).tap();
await waitFor(element(by.id('messagebox-keyboard-emoji'))) await waitFor(element(by.id('messagebox-keyboard-emoji')))
.toBeNotVisible() .not.toBeVisible()
.withTimeout(10000); .withTimeout(10000);
await expect(element(by.id('messagebox-close-emoji'))).toBeNotVisible(); });
await expect(element(by.id('messagebox-open-emoji'))).toExist();
} it('should search emoji and send it', async () => {
await element(by.id('messagebox-open-emoji')).tap();
await waitFor(element(by.id('emoji-picker-search')))
.toExist()
.withTimeout(4000);
await element(by.id('emoji-picker-search')).tap();
await waitFor(element(by.id('emoji-searchbar-input')))
.toExist()
.withTimeout(2000);
await element(by.id('emoji-searchbar-input')).replaceText('no_mouth');
await waitFor(element(by.id('emoji-no_mouth')))
.toExist()
.withTimeout(2000);
await element(by.id('emoji-no_mouth')).tap();
await expect(element(by.id('messagebox-input'))).toHaveText('😶');
await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by[textMatcher]('😶')))
.toExist()
.withTimeout(60000);
await element(by[textMatcher]('😶')).atIndex(0).tap();
});
it('should search emojis, go back to Emoji keyboard and then close the Emoji keyboard', async () => {
await element(by.id('messagebox-open-emoji')).tap();
await waitFor(element(by.id('emoji-picker-search')))
.toExist()
.withTimeout(4000);
await element(by.id('emoji-picker-search')).tap();
await waitFor(element(by.id('emoji-searchbar-input')))
.toExist()
.withTimeout(2000);
await element(by.id('openback-emoji-keyboard')).tap();
await waitFor(element(by.id('emoji-searchbar-input')))
.not.toBeVisible()
.withTimeout(2000);
await expect(element(by.id('messagebox-close-emoji'))).toExist();
await element(by.id('messagebox-close-emoji')).tap();
await waitFor(element(by.id('messagebox-keyboard-emoji')))
.not.toBeVisible()
.withTimeout(10000);
});
it('frequently used emojis should contain the recently used emojis', async () => {
await element(by.id('messagebox-open-emoji')).tap();
await waitFor(element(by.id('emoji-picker-tab-clock')));
await element(by.id('emoji-picker-tab-clock')).tap();
await waitFor(element(by.id('emoji-blush')))
.toExist()
.withTimeout(2000);
await waitFor(element(by.id('emoji-upside_down')))
.toExist()
.withTimeout(2000);
await waitFor(element(by.id('emoji-no_mouth')))
.toExist()
.withTimeout(2000);
await expect(element(by.id('messagebox-close-emoji'))).toExist();
await element(by.id('messagebox-close-emoji')).tap();
await waitFor(element(by.id('messagebox-keyboard-emoji')))
.not.toBeVisible()
.withTimeout(10000);
});
}); });
it('should show/hide emoji autocomplete', async () => { it('should show/hide emoji autocomplete', async () => {
await element(by.id('messagebox-input')).clearText();
await element(by.id('messagebox-input')).typeText(':joy'); await element(by.id('messagebox-input')).typeText(':joy');
await sleep(300); await sleep(300);
await waitFor(element(by.id('messagebox-container'))) await waitFor(element(by.id('messagebox-container')))
@ -223,10 +305,8 @@ describe('Room screen', () => {
await expect(element(by.id('action-sheet-handle'))).toBeVisible(); 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('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by[textMatcher]('Permalink')).atIndex(0).tap(); await element(by[textMatcher]('Permalink')).atIndex(0).tap();
// TODO: test clipboard // TODO: test clipboard
}); });
it('should copy message', async () => { it('should copy message', async () => {
await element(by[textMatcher](`${data.random}message`)) await element(by[textMatcher](`${data.random}message`))
.atIndex(0) .atIndex(0)
@ -237,28 +317,9 @@ describe('Room screen', () => {
await expect(element(by.id('action-sheet-handle'))).toBeVisible(); 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('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by[textMatcher]('Copy')).atIndex(0).tap(); await element(by[textMatcher]('Copy')).atIndex(0).tap();
// TODO: test clipboard // TODO: test clipboard
}); });
it('should star message', async () => {
await starMessage('message');
await sleep(1000); // https://github.com/RocketChat/Rocket.Chat.ReactNative/issues/2324
await element(by[textMatcher](`${data.random}message`))
.atIndex(0)
.longPress();
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(2000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'slow', 0.5);
await waitFor(element(by[textMatcher]('Unstar')).atIndex(0))
.toExist()
.withTimeout(6000);
await element(by.id('action-sheet-handle')).swipe('down', 'fast', 0.8);
});
it('should react to message', async () => { it('should react to message', async () => {
await waitFor(element(by[textMatcher](`${data.random}message`))) await waitFor(element(by[textMatcher](`${data.random}message`)))
.toExist() .toExist()
@ -275,19 +336,47 @@ describe('Room screen', () => {
await expect(element(by.id('action-sheet-handle'))).toBeVisible(); 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('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by.id('add-reaction')).tap(); await element(by.id('add-reaction')).tap();
await waitFor(element(by.id('reaction-picker'))) await waitFor(element(by.id('emoji-picker-tab-emoji')))
.toBeVisible() .toExist()
.withTimeout(2000); .withTimeout(2000);
await element(by.id('reaction-picker-😃')).tap(); await element(by.id('action-sheet-handle')).swipe('up', 'fast', 1);
await waitFor(element(by.id('reaction-picker-grinning'))) await element(by.id('emoji-picker-tab-emoji')).tap();
await waitFor(element(by.id('emoji-grinning')))
.toExist() .toExist()
.withTimeout(10000); .withTimeout(10000);
await element(by.id('reaction-picker-grinning')).tap(); await element(by.id('emoji-grinning')).tap();
await waitFor(element(by.id('message-reaction-:grinning:'))) await waitFor(element(by.id('message-reaction-:grinning:')))
.toExist() .toExist()
.withTimeout(60000); .withTimeout(60000);
}); });
it('should ask for review', async () => {
await dismissReviewNag(); // TODO: Create a proper test for this elsewhere.
});
it('should search emojis in the reaction picker and react', async () => {
await element(by[textMatcher](`${data.random}message`))
.atIndex(0)
.longPress();
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(2000);
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('emoji-searchbar-input')))
.toExist()
.withTimeout(2000);
await element(by.id('emoji-searchbar-input')).typeText('laughing');
await waitFor(element(by.id('emoji-laughing')))
.toExist()
.withTimeout(4000);
await element(by.id('emoji-laughing')).tap();
await waitFor(element(by.id('message-reaction-:laughing:')))
.toExist()
.withTimeout(60000);
});
it('should react to message with frequently used emoji', async () => { it('should react to message with frequently used emoji', async () => {
await element(by[textMatcher](`${data.random}message`)) await element(by[textMatcher](`${data.random}message`))
.atIndex(0) .atIndex(0)
@ -297,37 +386,38 @@ describe('Room screen', () => {
.withTimeout(2000); .withTimeout(2000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible(); 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('action-sheet-handle')).swipe('up', 'fast', 0.5);
await waitFor(element(by.id('message-actions-emoji-+1'))) await waitFor(element(by.id('message-actions-emoji-upside_down')))
.toBeVisible() .toBeVisible()
.withTimeout(2000); .withTimeout(2000);
await element(by.id('message-actions-emoji-+1')).tap(); await element(by.id('message-actions-emoji-upside_down')).tap();
await waitFor(element(by.id('message-reaction-:+1:'))) await waitFor(element(by.id('message-reaction-:upside_down:')))
.toBeVisible() .toBeVisible()
.withTimeout(60000); .withTimeout(60000);
}); });
it('should show reaction picker on add reaction button pressed and have frequently used emoji', async () => { it('should show reaction picker on add reaction button pressed and have frequently used emoji', async () => {
await element(by.id('message-add-reaction')).tap(); await element(by.id('message-add-reaction')).tap();
await waitFor(element(by.id('reaction-picker'))) await waitFor(element(by.id('action-sheet')))
.toExist() .toExist()
.withTimeout(2000); .withTimeout(2000);
await waitFor(element(by.id('reaction-picker-grinning'))) await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 1);
await waitFor(element(by.id('emoji-upside_down')))
.toExist()
.withTimeout(4000);
await waitFor(element(by.id('emoji-picker-tab-emoji')))
.toExist() .toExist()
.withTimeout(2000); .withTimeout(2000);
await element(by.id('reaction-picker-😃')).tap(); await element(by.id('emoji-picker-tab-emoji')).tap();
await waitFor(element(by.id('reaction-picker-grimacing'))) await waitFor(element(by.id('emoji-wink')))
.toExist() .toExist()
.withTimeout(2000); .withTimeout(10000);
await element(by.id('reaction-picker-grimacing')).tap(); await element(by.id('emoji-wink')).tap();
await waitFor(element(by.id('message-reaction-:grimacing:'))) await waitFor(element(by.id('message-reaction-:wink:')))
.toExist() .toExist()
.withTimeout(60000); .withTimeout(60000);
}); });
it('should ask for review', async () => {
await dismissReviewNag(); // TODO: Create a proper test for this elsewhere.
});
it('should open/close reactions list', async () => { it('should open/close reactions list', async () => {
await element(by.id('message-reaction-:grinning:')).longPress(); await element(by.id('message-reaction-:grinning:')).longPress();
await waitFor(element(by.id('reactionsList'))) await waitFor(element(by.id('reactionsList')))
@ -340,7 +430,7 @@ describe('Room screen', () => {
it('should remove reaction', async () => { it('should remove reaction', async () => {
await element(by.id('message-reaction-:grinning:')).tap(); await element(by.id('message-reaction-:grinning:')).tap();
await waitFor(element(by.id('message-reaction-:grinning:'))) await waitFor(element(by.id('message-reaction-:grinning:')))
.toBeNotVisible() .not.toExist()
.withTimeout(60000); .withTimeout(60000);
}); });
@ -364,7 +454,6 @@ describe('Room screen', () => {
.toExist() .toExist()
.withTimeout(60000); .withTimeout(60000);
}); });
it('should quote message', async () => { it('should quote message', async () => {
await mockMessage('quote'); await mockMessage('quote');
await element(by[textMatcher](`${data.random}quote`)) await element(by[textMatcher](`${data.random}quote`))
@ -381,34 +470,9 @@ describe('Room screen', () => {
.toExist() .toExist()
.withTimeout(2000); .withTimeout(2000);
await element(by.id('messagebox-send-message')).tap(); await element(by.id('messagebox-send-message')).tap();
// TODO: test if quote was sent // TODO: test if quote was sent
}); });
it('should pin message', async () => {
await mockMessage('pin');
await pinMessage('pin');
await waitFor(element(by[textMatcher](`${data.random}pin`)).atIndex(0))
.toExist()
.withTimeout(5000);
await waitFor(element(by[textMatcher](`${data.users.regular.username} Message pinned`)).atIndex(0))
.toExist()
.withTimeout(5000);
await element(by[textMatcher](`${data.random}pin`))
.atIndex(0)
.longPress();
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(1000);
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[textMatcher]('Unpin')).atIndex(0))
.toExist()
.withTimeout(2000);
await element(by.id('action-sheet-handle')).swipe('down', 'fast', 0.8);
});
it('should delete message', async () => { it('should delete message', async () => {
await mockMessage('delete'); await mockMessage('delete');
await waitFor(element(by[textMatcher](`${data.random}delete`)).atIndex(0)).toBeVisible(); await waitFor(element(by[textMatcher](`${data.random}delete`)).atIndex(0)).toBeVisible();
@ -424,7 +488,6 @@ describe('Room screen', () => {
.toExist() .toExist()
.withTimeout(1000); .withTimeout(1000);
await element(by[textMatcher]('Delete')).atIndex(0).tap(); await element(by[textMatcher]('Delete')).atIndex(0).tap();
const deleteAlertMessage = 'You will not be able to recover this message!'; const deleteAlertMessage = 'You will not be able to recover this message!';
await waitFor(element(by[textMatcher](deleteAlertMessage)).atIndex(0)) await waitFor(element(by[textMatcher](deleteAlertMessage)).atIndex(0))
.toExist() .toExist()

View File

@ -192,8 +192,8 @@ describe('Room info screen', () => {
await element(by.id('room-info-edit-view-announcement')).replaceText('abc'); await element(by.id('room-info-edit-view-announcement')).replaceText('abc');
await element(by.id('room-info-edit-view-password')).replaceText('abc'); await element(by.id('room-info-edit-view-password')).replaceText('abc');
await element(by.id('room-info-edit-view-t')).tap(); await element(by.id('room-info-edit-view-t')).tap();
await element(by.id('room-info-edit-view-list')).swipe('up', 'fast', 0.5); await swipe('up');
// await element(by.id('room-info-edit-view-ro')).longPress(); // https://github.com/facebook/react-native/issues/28032 await element(by.id('room-info-edit-view-ro')).longPress(); // https://github.com/facebook/react-native/issues/28032
await element(by.id('room-info-edit-view-react-when-ro')).tap(); await element(by.id('room-info-edit-view-react-when-ro')).tap();
await swipe('up'); await swipe('up');
await element(by.id('room-info-edit-view-reset')).tap(); await element(by.id('room-info-edit-view-reset')).tap();
@ -206,8 +206,8 @@ describe('Room info screen', () => {
await expect(element(by.id('room-info-edit-view-password'))).toHaveText(''); await expect(element(by.id('room-info-edit-view-password'))).toHaveText('');
// await swipe('down'); // await swipe('down');
await expect(element(by.id('room-info-edit-view-t'))).toHaveToggleValue(true); await expect(element(by.id('room-info-edit-view-t'))).toHaveToggleValue(true);
await expect(element(by.id('room-info-edit-view-ro'))).toHaveToggleValue(true); await expect(element(by.id('room-info-edit-view-ro'))).toHaveToggleValue(false);
await expect(element(by.id('room-info-edit-view-react-when-ro'))).toHaveToggleValue(false); await expect(element(by.id('room-info-edit-view-react-when-ro'))).toBeNotVisible();
await swipe('down'); await swipe('down');
}); });