[IMPROVE] Redesign emoji picker (#4328)
Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
parent
e946128a49
commit
88fa0fc58d
|
@ -3,7 +3,13 @@ import { Provider } from 'react-redux';
|
|||
|
||||
import { themes } from '../app/lib/constants';
|
||||
import MessageContext from '../app/containers/message/Context';
|
||||
import { selectServerRequest } from '../app/actions/server';
|
||||
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 = [
|
||||
Story => (
|
||||
|
@ -15,7 +21,7 @@ export const decorators = [
|
|||
username: 'diego.mello',
|
||||
token: 'abc'
|
||||
},
|
||||
baseUrl: 'https://open.rocket.chat',
|
||||
baseUrl,
|
||||
onPress: () => {},
|
||||
onLongPress: () => {},
|
||||
reactionInit: () => {},
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
// 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 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
|
@ -18,10 +18,10 @@ const HANDLE_HEIGHT = isIOS ? 40 : 56;
|
|||
const MIN_SNAP_HEIGHT = 16;
|
||||
const CANCEL_HEIGHT = 64;
|
||||
|
||||
const ANIMATION_DURATION = 250;
|
||||
export const ACTION_SHEET_ANIMATION_DURATION = 250;
|
||||
|
||||
const ANIMATION_CONFIG = {
|
||||
duration: ANIMATION_DURATION,
|
||||
duration: ACTION_SHEET_ANIMATION_DURATION,
|
||||
// https://easings.net/#easeInOutCubic
|
||||
easing: Easing.bezier(0.645, 0.045, 0.355, 1.0)
|
||||
};
|
||||
|
@ -140,7 +140,7 @@ const ActionSheet = React.memo(
|
|||
style={{ ...styles.container, ...bottomSheet }}
|
||||
backgroundStyle={{ backgroundColor: colors.focusedBackground }}
|
||||
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}
|
||||
{...androidTablet}
|
||||
>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export * from './Provider';
|
||||
export * from './ActionSheet';
|
||||
|
|
|
@ -43,9 +43,7 @@ const Avatar = React.memo(
|
|||
|
||||
let image;
|
||||
if (emoji) {
|
||||
image = (
|
||||
<Emoji baseUrl={server} getCustomEmoji={getCustomEmoji} isMessageContainsOnlyEmoji literal={emoji} style={avatarStyle} />
|
||||
);
|
||||
image = <Emoji getCustomEmoji={getCustomEmoji} isMessageContainsOnlyEmoji literal={emoji} style={avatarStyle} />;
|
||||
} else {
|
||||
let uri = avatar;
|
||||
if (!isStatic) {
|
||||
|
|
|
@ -1,24 +1,30 @@
|
|||
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(
|
||||
({ baseUrl, emoji, style }: ICustomEmoji) => (
|
||||
({ emoji, style }: ICustomEmojiProps) => {
|
||||
const baseUrl = useAppSelector(state => state.share.server.server || state.server.server);
|
||||
return (
|
||||
<FastImage
|
||||
style={style}
|
||||
source={{
|
||||
uri: `${baseUrl}/emoji-custom/${encodeURIComponent(emoji.content || emoji.name)}.${emoji.extension}`,
|
||||
uri: `${baseUrl}/emoji-custom/${encodeURIComponent(emoji.name)}.${emoji.extension}`,
|
||||
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;
|
||||
|
|
|
@ -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} />;
|
||||
};
|
|
@ -1,75 +1,45 @@
|
|||
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 styles from './styles';
|
||||
import CustomEmoji from './CustomEmoji';
|
||||
import { EMOJI_BUTTON_SIZE } from './styles';
|
||||
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) => {
|
||||
if (emoji && emoji.isCustom) {
|
||||
return (
|
||||
<CustomEmoji
|
||||
style={[styles.customCategoryEmoji, { height: size - 16, width: size - 16 }]}
|
||||
emoji={emoji}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}>
|
||||
{shortnameToUnicode(`:${emoji}:`)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
const EmojiCategory = ({ onEmojiSelected, emojis }: IEmojiCategoryProps): React.ReactElement | null => {
|
||||
const { width } = useWindowDimensions();
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
const numColumns = Math.trunc(width / EMOJI_BUTTON_SIZE);
|
||||
const marginHorizontal = (width % EMOJI_BUTTON_SIZE) / 2;
|
||||
|
||||
render() {
|
||||
const { emojis, width } = this.props;
|
||||
const renderItem = ({ item }: { item: IEmoji }) => <PressableEmoji emoji={item} onPress={onEmojiSelected} />;
|
||||
|
||||
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
|
||||
// needed to update the numColumns when the width changes
|
||||
key={`emoji-category-${width}`}
|
||||
// @ts-ignore
|
||||
keyExtractor={item => (item && item.isCustom && item.content) || item}
|
||||
keyExtractor={item => (typeof item === 'string' ? item : item.name)}
|
||||
data={emojis}
|
||||
extraData={this.props}
|
||||
renderItem={({ item }) => this.renderItem(item)}
|
||||
renderItem={renderItem}
|
||||
numColumns={numColumns}
|
||||
initialNumToRender={45}
|
||||
removeClippedSubviews
|
||||
contentContainerStyle={{ marginHorizontal }}
|
||||
{...scrollPersistTaps}
|
||||
keyboardDismissMode={'none'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default EmojiCategory;
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -1,56 +1,42 @@
|
|||
import React from 'react';
|
||||
import { StyleProp, Text, TextStyle, TouchableOpacity, View } from 'react-native';
|
||||
import { Pressable, View } from 'react-native';
|
||||
|
||||
import styles from './styles';
|
||||
import { themes } from '../../lib/constants';
|
||||
import { TSupportedThemes } from '../../theme';
|
||||
import { useTheme } from '../../theme';
|
||||
import { ITabBarProps } from './interfaces';
|
||||
import { isIOS } from '../../lib/methods/helpers';
|
||||
import { CustomIcon } from '../CustomIcon';
|
||||
|
||||
interface ITabBarProps {
|
||||
goToPage?: (page: number) => void;
|
||||
activeTab?: number;
|
||||
tabs?: string[];
|
||||
tabEmojiStyle: StyleProp<TextStyle>;
|
||||
theme: TSupportedThemes;
|
||||
}
|
||||
|
||||
export default class TabBar extends React.Component<ITabBarProps> {
|
||||
shouldComponentUpdate(nextProps: ITabBarProps) {
|
||||
const { activeTab, theme } = this.props;
|
||||
if (nextProps.activeTab !== activeTab) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.theme !== theme) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { tabs, goToPage, tabEmojiStyle, activeTab, theme } = this.props;
|
||||
const TabBar = ({ activeTab, tabs, goToPage }: ITabBarProps): React.ReactElement => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<View style={styles.tabsContainer}>
|
||||
{tabs?.map((tab, i) => (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
<Pressable
|
||||
key={tab}
|
||||
onPress={() => {
|
||||
if (goToPage) {
|
||||
goToPage(i);
|
||||
onPress={() => goToPage?.(i)}
|
||||
testID={`emoji-picker-tab-${tab}`}
|
||||
android_ripple={{ color: colors.bannerBackground }}
|
||||
style={({ pressed }: { pressed: boolean }) => [
|
||||
styles.tab,
|
||||
{
|
||||
backgroundColor: isIOS && pressed ? colors.bannerBackground : 'transparent'
|
||||
}
|
||||
}}
|
||||
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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default TabBar;
|
||||
|
|
|
@ -1,155 +1,44 @@
|
|||
import React, { Component } from 'react';
|
||||
import { StyleProp, TextStyle, View } from 'react-native';
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
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 EmojiCategory from './EmojiCategory';
|
||||
import Footer from './Footer';
|
||||
import styles from './styles';
|
||||
import categories from './categories';
|
||||
import database from '../../lib/database';
|
||||
import { emojisByCategory } from './emojis';
|
||||
import protectedFunction from '../../lib/methods/helpers/protectedFunction';
|
||||
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
|
||||
import log from '../../lib/methods/helpers/log';
|
||||
import { themes } from '../../lib/constants';
|
||||
import { TSupportedThemes } from '../../theme';
|
||||
import { IEmoji, TGetCustomEmoji, IApplicationState, ICustomEmojis, TFrequentlyUsedEmojiModel } from '../../definitions';
|
||||
import { categories, emojisByCategory } from '../../lib/constants';
|
||||
import { useTheme } from '../../theme';
|
||||
import { IEmoji, ICustomEmojis } from '../../definitions';
|
||||
import { useAppSelector, useFrequentlyUsedEmoji } from '../../lib/hooks';
|
||||
import { addFrequentlyUsed } from '../../lib/methods';
|
||||
import { IEmojiPickerProps, EventTypes } from './interfaces';
|
||||
|
||||
interface IEmojiPickerProps {
|
||||
isMessageContainsOnlyEmoji?: boolean;
|
||||
getCustomEmoji?: TGetCustomEmoji;
|
||||
baseUrl: string;
|
||||
customEmojis: ICustomEmojis;
|
||||
style?: StyleProp<ImageStyle>;
|
||||
theme: TSupportedThemes;
|
||||
onEmojiSelected: (emoji: string, shortname?: string) => void;
|
||||
tabEmojiStyle?: StyleProp<TextStyle>;
|
||||
}
|
||||
const EmojiPicker = ({
|
||||
onItemClicked,
|
||||
isEmojiKeyboard = false,
|
||||
searching = false,
|
||||
searchedEmojis = []
|
||||
}: IEmojiPickerProps): React.ReactElement | null => {
|
||||
const { colors } = useTheme();
|
||||
const { frequentlyUsed, loaded } = useFrequentlyUsedEmoji();
|
||||
|
||||
interface IEmojiPickerState {
|
||||
frequentlyUsed: (string | { content?: string; extension?: string; isCustom: boolean })[];
|
||||
customEmojis: any;
|
||||
show: boolean;
|
||||
width: number | null;
|
||||
}
|
||||
|
||||
class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
|
||||
constructor(props: IEmojiPickerProps) {
|
||||
super(props);
|
||||
const customEmojis = Object.keys(props.customEmojis)
|
||||
.filter(item => item === props.customEmojis[item].name)
|
||||
const allCustomEmojis: ICustomEmojis = useAppSelector(
|
||||
state => state.customEmojis,
|
||||
() => true
|
||||
);
|
||||
const customEmojis = Object.keys(allCustomEmojis)
|
||||
.filter(item => item === allCustomEmojis[item].name)
|
||||
.map(item => ({
|
||||
content: props.customEmojis[item].name,
|
||||
extension: props.customEmojis[item].extension,
|
||||
isCustom: true
|
||||
name: allCustomEmojis[item].name,
|
||||
extension: allCustomEmojis[item].extension
|
||||
}));
|
||||
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);
|
||||
}
|
||||
const handleEmojiSelect = (emoji: IEmoji) => {
|
||||
onItemClicked(EventTypes.EMOJI_PRESSED, emoji);
|
||||
addFrequentlyUsed(emoji);
|
||||
};
|
||||
|
||||
_addFrequentlyUsed = protectedFunction(async (emoji: IEmoji) => {
|
||||
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;
|
||||
|
||||
const renderCategory = (category: keyof typeof emojisByCategory, i: number, label: string) => {
|
||||
let emojis = [];
|
||||
if (i === 0) {
|
||||
emojis = frequentlyUsed;
|
||||
|
@ -158,49 +47,40 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
|
|||
} else {
|
||||
emojis = emojisByCategory[category];
|
||||
}
|
||||
return (
|
||||
<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) {
|
||||
if (!emojis.length) {
|
||||
return null;
|
||||
}
|
||||
return <EmojiCategory emojis={emojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} tabLabel={label} />;
|
||||
};
|
||||
|
||||
if (!loaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View onLayout={this.onLayout} style={{ flex: 1 }}>
|
||||
<View style={styles.emojiPickerContainer}>
|
||||
{searching ? (
|
||||
<EmojiCategory emojis={searchedEmojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} />
|
||||
) : (
|
||||
<ScrollableTabView
|
||||
renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} theme={theme} />}
|
||||
renderTabBar={() => <TabBar />}
|
||||
contentProps={{
|
||||
keyboardShouldPersistTaps: 'always',
|
||||
keyboardDismissMode: 'none'
|
||||
}}
|
||||
style={{ backgroundColor: themes[theme].focusedBackground }}
|
||||
style={{ backgroundColor: colors.messageboxBackground }}
|
||||
>
|
||||
{categories.tabs.map((tab: any, i) =>
|
||||
i === 0 && frequentlyUsed.length === 0
|
||||
? null // when no frequentlyUsed don't show the tab
|
||||
: this.renderCategory(tab.category, i, tab.tabLabel)
|
||||
)}
|
||||
{categories.tabs.map((tab: any, i) => renderCategory(tab.category, i, tab.tabLabel))}
|
||||
</ScrollableTabView>
|
||||
)}
|
||||
{isEmojiKeyboard && (
|
||||
<Footer
|
||||
onSearchPressed={() => onItemClicked(EventTypes.SEARCH_PRESSED)}
|
||||
onBackspacePressed={() => onItemClicked(EventTypes.BACKSPACE_PRESSED)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: IApplicationState) => ({
|
||||
customEmojis: state.customEmojis,
|
||||
baseUrl: state.share.server.server || state.server.server
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(EmojiPicker);
|
||||
export default EmojiPicker;
|
||||
|
|
|
@ -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[];
|
||||
}
|
|
@ -2,20 +2,23 @@ import { StyleSheet } from 'react-native';
|
|||
|
||||
import sharedStyles from '../../views/Styles';
|
||||
|
||||
export const EMOJI_BUTTON_SIZE = 44;
|
||||
export const EMOJI_SIZE = EMOJI_BUTTON_SIZE - 16;
|
||||
|
||||
export default StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
tabsContainer: {
|
||||
height: 45,
|
||||
flexDirection: 'row',
|
||||
paddingTop: 5
|
||||
height: EMOJI_BUTTON_SIZE,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
tab: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingBottom: 10
|
||||
paddingVertical: 10,
|
||||
width: EMOJI_BUTTON_SIZE
|
||||
},
|
||||
tabEmoji: {
|
||||
fontSize: 20,
|
||||
|
@ -33,7 +36,6 @@ export default StyleSheet.create({
|
|||
left: 0,
|
||||
right: 0,
|
||||
height: 2,
|
||||
backgroundColor: 'rgba(0,0,0,0.05)',
|
||||
bottom: 0
|
||||
},
|
||||
categoryContainer: {
|
||||
|
@ -49,10 +51,34 @@ export default StyleSheet.create({
|
|||
},
|
||||
categoryEmoji: {
|
||||
...sharedStyles.textAlignCenter,
|
||||
textAlignVertical: 'center',
|
||||
fontSize: EMOJI_SIZE,
|
||||
backgroundColor: 'transparent',
|
||||
color: '#ffffff'
|
||||
},
|
||||
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 }
|
||||
});
|
||||
|
|
|
@ -1,32 +1,29 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { FlatList, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { TSupportedThemes, useTheme } from '../../theme';
|
||||
import { themes } from '../../lib/constants';
|
||||
import { CustomIcon } from '../CustomIcon';
|
||||
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
|
||||
import { addFrequentlyUsed } from '../../lib/methods';
|
||||
import { useFrequentlyUsedEmoji } from '../../lib/hooks';
|
||||
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||
import database from '../../lib/database';
|
||||
import { useDimensions } from '../../dimensions';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { TAnyMessageModel, TFrequentlyUsedEmojiModel } from '../../definitions';
|
||||
import { IEmoji, TAnyMessageModel } from '../../definitions';
|
||||
import Touch from '../Touch';
|
||||
|
||||
type TItem = TFrequentlyUsedEmojiModel | string;
|
||||
|
||||
export interface IHeader {
|
||||
handleReaction: (emoji: TItem, message: TAnyMessageModel) => void;
|
||||
server: string;
|
||||
handleReaction: (emoji: IEmoji, message: TAnyMessageModel) => void;
|
||||
message: TAnyMessageModel;
|
||||
isMasterDetail: boolean;
|
||||
}
|
||||
|
||||
type TOnReaction = ({ emoji }: { emoji: TItem }) => void;
|
||||
type TOnReaction = ({ emoji }: { emoji: IEmoji }) => void;
|
||||
|
||||
interface THeaderItem {
|
||||
item: TItem;
|
||||
item: IEmoji;
|
||||
onReaction: TOnReaction;
|
||||
server: string;
|
||||
theme: TSupportedThemes;
|
||||
}
|
||||
|
||||
|
@ -64,30 +61,19 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
const keyExtractor = (item: TItem) => {
|
||||
const emojiModel = item as TFrequentlyUsedEmojiModel;
|
||||
return (emojiModel.id ? emojiModel.content : item) as string;
|
||||
};
|
||||
|
||||
const DEFAULT_EMOJIS = ['clap', '+1', 'heart_eyes', 'grinning', 'thinking_face', 'smiley'];
|
||||
|
||||
const HeaderItem = ({ item, onReaction, server, theme }: THeaderItem) => {
|
||||
const emojiModel = item as TFrequentlyUsedEmojiModel;
|
||||
const emoji = (emojiModel.id ? emojiModel.content : item) as string;
|
||||
return (
|
||||
const HeaderItem = ({ item, onReaction, theme }: THeaderItem) => (
|
||||
<Touch
|
||||
testID={`message-actions-emoji-${emoji}`}
|
||||
onPress={() => onReaction({ emoji: `:${emoji}:` })}
|
||||
testID={`message-actions-emoji-${item}`}
|
||||
onPress={() => onReaction({ emoji: item })}
|
||||
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
|
||||
>
|
||||
{emojiModel?.isCustom ? (
|
||||
<CustomEmoji style={styles.customEmoji} emoji={emojiModel} baseUrl={server} />
|
||||
{typeof item === 'string' ? (
|
||||
<Text style={styles.headerIcon}>{shortnameToUnicode(`:${item}:`)}</Text>
|
||||
) : (
|
||||
<Text style={styles.headerIcon}>{shortnameToUnicode(`:${emoji}:`)}</Text>
|
||||
<CustomEmoji style={styles.customEmoji} emoji={item} />
|
||||
)}
|
||||
</Touch>
|
||||
);
|
||||
};
|
||||
);
|
||||
|
||||
const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => (
|
||||
<Touch
|
||||
|
@ -99,49 +85,35 @@ const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => (
|
|||
</Touch>
|
||||
);
|
||||
|
||||
const Header = React.memo(({ handleReaction, server, message, isMasterDetail }: IHeader) => {
|
||||
const [items, setItems] = useState<TItem[]>([]);
|
||||
const Header = React.memo(({ handleReaction, message, isMasterDetail }: IHeader) => {
|
||||
const { width, height } = useDimensions();
|
||||
const { theme } = useTheme();
|
||||
|
||||
// TODO: create custom hook to re-render based on screen size
|
||||
const setEmojis = async () => {
|
||||
try {
|
||||
const db = database.active;
|
||||
const freqEmojiCollection = db.get('frequently_used_emojis');
|
||||
let freqEmojis: TItem[] = await freqEmojiCollection.query().fetch();
|
||||
|
||||
const { frequentlyUsed, loaded } = useFrequentlyUsedEmoji(true);
|
||||
const isLandscape = width > height;
|
||||
const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;
|
||||
const quantity = size / (ITEM_SIZE + ITEM_MARGIN * 2) - 1;
|
||||
const quantity = Math.trunc(size / (ITEM_SIZE + ITEM_MARGIN * 2) - 1);
|
||||
|
||||
freqEmojis = freqEmojis.concat(DEFAULT_EMOJIS).slice(0, quantity);
|
||||
setItems(freqEmojis);
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
const onReaction: TOnReaction = ({ emoji }) => {
|
||||
handleReaction(emoji, message);
|
||||
addFrequentlyUsed(emoji);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setEmojis();
|
||||
}, []);
|
||||
|
||||
const onReaction: TOnReaction = ({ emoji }) => handleReaction(emoji, message);
|
||||
|
||||
const renderItem = ({ item }: { item: TItem }) => (
|
||||
<HeaderItem item={item} onReaction={onReaction} server={server} theme={theme} />
|
||||
);
|
||||
const renderItem = ({ item }: { item: IEmoji }) => <HeaderItem item={item} onReaction={onReaction} theme={theme} />;
|
||||
|
||||
const renderFooter = () => <HeaderFooter onReaction={onReaction} theme={theme} />;
|
||||
|
||||
if (!loaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}>
|
||||
<FlatList
|
||||
data={items}
|
||||
data={frequentlyUsed.slice(0, quantity)}
|
||||
renderItem={renderItem}
|
||||
ListFooterComponent={renderFooter}
|
||||
style={{ backgroundColor: themes[theme].focusedBackground }}
|
||||
keyExtractor={keyExtractor}
|
||||
keyExtractor={item => (typeof item === 'string' ? item : item.name)}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
scrollEnabled={false}
|
||||
horizontal
|
||||
|
|
|
@ -12,10 +12,10 @@ import { getMessageTranslation } from '../message/utils';
|
|||
import { LISTENER } from '../Toast';
|
||||
import EventEmitter from '../../lib/methods/helpers/events';
|
||||
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 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 { hasPermission } from '../../lib/methods/helpers';
|
||||
import { Services } from '../../lib/services';
|
||||
|
@ -26,7 +26,7 @@ export interface IMessageActionsProps {
|
|||
user: Pick<ILoggedUser, 'id'>;
|
||||
editInit: (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;
|
||||
isMasterDetail: boolean;
|
||||
isReadOnly: boolean;
|
||||
|
@ -37,7 +37,6 @@ export interface IMessageActionsProps {
|
|||
Message_AllowPinning?: boolean;
|
||||
Message_AllowStarring?: boolean;
|
||||
Message_Read_Receipt_Store_Users?: boolean;
|
||||
server: string;
|
||||
editMessagePermission?: string[];
|
||||
deleteMessagePermission?: string[];
|
||||
forceDeleteMessagePermission?: string[];
|
||||
|
@ -60,7 +59,6 @@ const MessageActions = React.memo(
|
|||
onReactionPress,
|
||||
replyInit,
|
||||
isReadOnly,
|
||||
server,
|
||||
Message_AllowDeleting,
|
||||
Message_AllowDeleting_BlockDeleteInMinutes,
|
||||
Message_AllowEditing,
|
||||
|
@ -261,12 +259,10 @@ const MessageActions = React.memo(
|
|||
const handleReaction: IHeader['handleReaction'] = (shortname, message) => {
|
||||
logEvent(events.ROOM_MSG_ACTION_REACTION);
|
||||
if (shortname) {
|
||||
// TODO: evaluate unification with IEmoji
|
||||
onReactionPress(shortname as any, message.id);
|
||||
onReactionPress(shortname, message.id);
|
||||
} else {
|
||||
reactionInit(message);
|
||||
setTimeout(() => reactionInit(message), ACTION_SHEET_ANIMATION_DURATION);
|
||||
}
|
||||
// close actionSheet when click at header
|
||||
hideActionSheet();
|
||||
};
|
||||
|
||||
|
@ -460,7 +456,7 @@ const MessageActions = React.memo(
|
|||
headerHeight: HEADER_HEIGHT,
|
||||
customHeader:
|
||||
!isReadOnly || room.reactWhenReadOnly ? (
|
||||
<Header server={server} handleReaction={handleReaction} isMasterDetail={isMasterDetail} message={message} />
|
||||
<Header handleReaction={handleReaction} isMasterDetail={isMasterDetail} message={message} />
|
||||
) : null
|
||||
});
|
||||
};
|
||||
|
|
|
@ -6,22 +6,31 @@ import { Provider } from 'react-redux';
|
|||
import store from '../../lib/store';
|
||||
import EmojiPicker from '../EmojiPicker';
|
||||
import styles from './styles';
|
||||
import { themes } from '../../lib/constants';
|
||||
import { TSupportedThemes } from '../../theme';
|
||||
import { ThemeContext, TSupportedThemes } from '../../theme';
|
||||
import { EventTypes } from '../EmojiPicker/interfaces';
|
||||
import { IEmoji } from '../../definitions';
|
||||
import { colors } from '../../lib/constants';
|
||||
|
||||
const EmojiKeyboard = ({ theme }: { theme: TSupportedThemes }) => {
|
||||
const onEmojiSelected = (emoji: string) => {
|
||||
KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji });
|
||||
const onItemClicked = (eventType: EventTypes, emoji?: IEmoji) => {
|
||||
KeyboardRegistry.onItemSelected('EmojiKeyboard', { eventType, emoji });
|
||||
};
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
theme,
|
||||
colors: colors[theme]
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={[styles.emojiKeyboardContainer, { borderTopColor: themes[theme].borderColor }]}
|
||||
style={[styles.emojiKeyboardContainer, { borderTopColor: colors[theme].borderColor }]}
|
||||
testID='messagebox-keyboard-emoji'
|
||||
>
|
||||
<EmojiPicker onEmojiSelected={onEmojiSelected} theme={theme} />
|
||||
<EmojiPicker onItemClicked={onItemClicked} isEmojiKeyboard={true} />
|
||||
</View>
|
||||
</ThemeContext.Provider>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -1,10 +1,9 @@
|
|||
import React, { useContext } from 'react';
|
||||
import React from 'react';
|
||||
import { Text } from 'react-native';
|
||||
|
||||
import { IEmoji } from '../../../definitions/IEmoji';
|
||||
import shortnameToUnicode from '../../../lib/methods/helpers/shortnameToUnicode';
|
||||
import CustomEmoji from '../../EmojiPicker/CustomEmoji';
|
||||
import MessageboxContext from '../Context';
|
||||
import styles from '../styles';
|
||||
|
||||
interface IMessageBoxMentionEmoji {
|
||||
|
@ -12,13 +11,10 @@ interface IMessageBoxMentionEmoji {
|
|||
}
|
||||
|
||||
const MentionEmoji = ({ item }: IMessageBoxMentionEmoji) => {
|
||||
const context = useContext(MessageboxContext);
|
||||
const { baseUrl } = context;
|
||||
|
||||
if (item.name) {
|
||||
return <CustomEmoji style={styles.mentionItemCustomEmoji} emoji={item} baseUrl={baseUrl} />;
|
||||
}
|
||||
if (typeof item === 'string') {
|
||||
return <Text style={styles.mentionItemEmoji}>{shortnameToUnicode(`:${item}:`)}</Text>;
|
||||
}
|
||||
return <CustomEmoji style={styles.mentionItemCustomEmoji} emoji={item} />;
|
||||
};
|
||||
|
||||
export default MentionEmoji;
|
||||
|
|
|
@ -4,5 +4,6 @@ export const MENTIONS_TRACKING_TYPE_COMMANDS = '/';
|
|||
export const MENTIONS_TRACKING_TYPE_ROOMS = '#';
|
||||
export const MENTIONS_TRACKING_TYPE_CANNED = '!';
|
||||
export const MENTIONS_COUNT_TO_DISPLAY = 4;
|
||||
export const MAX_EMOJIS_TO_DISPLAY = 20;
|
||||
|
||||
export const TIMEOUT_CLOSE_EMOJI = 300;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { KeyboardAccessoryView } from 'react-native-ui-lib/keyboard';
|
||||
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 styles from './styles';
|
||||
import database from '../../lib/database';
|
||||
import { emojis } from '../EmojiPicker/emojis';
|
||||
import log, { events, logEvent } from '../../lib/methods/helpers/log';
|
||||
import RecordAudio from './RecordAudio';
|
||||
import I18n from '../../i18n';
|
||||
import ReplyPreview from './ReplyPreview';
|
||||
import { themes } from '../../lib/constants';
|
||||
import { themes, emojis } from '../../lib/constants';
|
||||
import LeftButtons from './LeftButtons';
|
||||
import RightButtons from './RightButtons';
|
||||
import { canUploadFile } from '../../lib/methods/helpers/media';
|
||||
|
@ -51,7 +50,8 @@ import {
|
|||
TGetCustomEmoji,
|
||||
TSubscriptionModel,
|
||||
TThreadModel,
|
||||
IMessage
|
||||
IMessage,
|
||||
IEmoji
|
||||
} from '../../definitions';
|
||||
import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types';
|
||||
import { getPermalinkMessage, search, sendFileMessage } from '../../lib/methods';
|
||||
|
@ -59,6 +59,9 @@ import { hasPermission, debounce, isAndroid, isIOS, isTablet, compareServerVersi
|
|||
import { Services } from '../../lib/services';
|
||||
import { TSupportedThemes } from '../../theme';
|
||||
import { ChatsStackParamList } from '../../stacks/types';
|
||||
import { EventTypes } from '../EmojiPicker/interfaces';
|
||||
import EmojiSearchbar from './EmojiSearchbar';
|
||||
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
|
||||
|
||||
require('./EmojiKeyboard');
|
||||
|
||||
|
@ -129,6 +132,7 @@ interface IMessageBoxState {
|
|||
tshow: boolean;
|
||||
mentionLoading: boolean;
|
||||
permissionToUpload: boolean;
|
||||
showEmojiSearchbar: boolean;
|
||||
}
|
||||
|
||||
class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
||||
|
@ -183,7 +187,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
command: {},
|
||||
tshow: this.sendThreadToChannel,
|
||||
mentionLoading: false,
|
||||
permissionToUpload: true
|
||||
permissionToUpload: true,
|
||||
showEmojiSearchbar: false
|
||||
};
|
||||
this.text = '';
|
||||
this.selection = { start: 0, end: 0 };
|
||||
|
@ -209,6 +214,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
...videoPickerConfig,
|
||||
...libPickerLabels
|
||||
};
|
||||
|
||||
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
|
||||
}
|
||||
|
||||
get sendThreadToChannel() {
|
||||
|
@ -326,7 +333,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
tshow,
|
||||
mentionLoading,
|
||||
trackingType,
|
||||
permissionToUpload
|
||||
permissionToUpload,
|
||||
showEmojiSearchbar
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
|
@ -346,6 +354,9 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
if (nextState.showEmojiKeyboard !== showEmojiKeyboard) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.showEmojiSearchbar !== showEmojiSearchbar) {
|
||||
return true;
|
||||
}
|
||||
if (!isFocused()) {
|
||||
return false;
|
||||
}
|
||||
|
@ -438,6 +449,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
if (isTablet) {
|
||||
EventEmiter.removeListener(KEY_COMMAND, this.handleCommands);
|
||||
}
|
||||
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
|
||||
}
|
||||
|
||||
setOptions = async () => {
|
||||
|
@ -519,7 +531,10 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
}, 100);
|
||||
|
||||
onKeyboardResigned = () => {
|
||||
const { showEmojiSearchbar } = this.state;
|
||||
if (!showEmojiSearchbar) {
|
||||
this.closeEmoji();
|
||||
}
|
||||
};
|
||||
|
||||
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 { emoji } = params;
|
||||
let newText = '';
|
||||
|
||||
// if messagebox has an active cursor
|
||||
const { start, end } = this.selection;
|
||||
const cursor = Math.max(start, end);
|
||||
newText = `${text.substr(0, cursor)}${emoji}${text.substr(cursor)}`;
|
||||
const newCursor = cursor + emoji.length;
|
||||
let newCursor;
|
||||
|
||||
switch (eventType) {
|
||||
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) => {
|
||||
|
@ -621,16 +668,20 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
this.setState({ mentions: res, mentionLoading: false });
|
||||
}, 300);
|
||||
|
||||
getEmojis = debounce(async (keyword: any) => {
|
||||
const db = database.active;
|
||||
const customEmojisCollection = db.get('custom_emojis');
|
||||
getCustomEmojis = async (keyword: any, count: number) => {
|
||||
const likeString = sanitizeLikeString(keyword);
|
||||
const whereClause = [];
|
||||
if (likeString) {
|
||||
whereClause.push(Q.where('name', Q.like(`${likeString}%`)));
|
||||
}
|
||||
let customEmojis = await customEmojisCollection.query(...whereClause).fetch();
|
||||
customEmojis = customEmojis.slice(0, MENTIONS_COUNT_TO_DISPLAY);
|
||||
const db = database.active;
|
||||
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 mergedEmojis = [...customEmojis, ...filteredEmojis].slice(0, MENTIONS_COUNT_TO_DISPLAY);
|
||||
this.setState({ mentions: mergedEmojis || [], mentionLoading: false });
|
||||
|
@ -881,7 +932,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
|
||||
openEmoji = () => {
|
||||
logEvent(events.ROOM_OPEN_EMOJI);
|
||||
this.setState({ showEmojiKeyboard: true });
|
||||
this.setState({ showEmojiKeyboard: true, showEmojiSearchbar: false });
|
||||
this.stopTrackingMention();
|
||||
};
|
||||
|
||||
recordingCallback = (recording: any) => {
|
||||
|
@ -903,7 +955,13 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
};
|
||||
|
||||
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) => {
|
||||
|
@ -926,7 +984,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
|
||||
this.clearInput();
|
||||
this.debouncedOnChangeText.stop();
|
||||
this.closeEmoji();
|
||||
this.closeEmojiKeyboardAndFocus();
|
||||
this.stopTrackingMention();
|
||||
this.handleTyping(false);
|
||||
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 = () => {
|
||||
const {
|
||||
recording,
|
||||
showEmojiKeyboard,
|
||||
showEmojiSearchbar,
|
||||
showSend,
|
||||
mentions,
|
||||
trackingType,
|
||||
|
@ -1149,11 +1231,11 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
const textInputAndButtons = !recording ? (
|
||||
<>
|
||||
<LeftButtons
|
||||
showEmojiKeyboard={showEmojiKeyboard}
|
||||
showEmojiKeyboard={showEmojiKeyboard || showEmojiSearchbar}
|
||||
editing={editing}
|
||||
editCancel={this.editCancel}
|
||||
openEmoji={this.openEmoji}
|
||||
closeEmoji={this.closeEmoji}
|
||||
closeEmoji={this.closeEmojiKeyboardAndFocus}
|
||||
/>
|
||||
<TextInput
|
||||
ref={component => (this.component = component)}
|
||||
|
@ -1197,6 +1279,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
{recordAudio}
|
||||
</View>
|
||||
{this.renderSendToChannel()}
|
||||
{this.renderEmojiSearchbar()}
|
||||
</View>
|
||||
{children}
|
||||
</>
|
||||
|
@ -1224,7 +1307,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
kbComponent={showEmojiKeyboard ? 'EmojiKeyboard' : null}
|
||||
kbInitialProps={{ theme }}
|
||||
onKeyboardResigned={this.onKeyboardResigned}
|
||||
onItemSelected={this.onEmojiSelected}
|
||||
onItemSelected={this.onKeyboardItemSelected}
|
||||
trackInteractive
|
||||
requiresSameParentToManageScrollView
|
||||
addBottomView
|
||||
|
|
|
@ -105,7 +105,7 @@ export default StyleSheet.create({
|
|||
},
|
||||
emojiKeyboardContainer: {
|
||||
flex: 1,
|
||||
borderTopWidth: StyleSheet.hairlineWidth
|
||||
borderTopWidth: 1
|
||||
},
|
||||
slash: {
|
||||
height: 30,
|
||||
|
|
|
@ -23,7 +23,6 @@ interface IAllTabProps {
|
|||
const AllReactionsListItem = ({ item, getCustomEmoji }: IAllReactionsListItemProps) => {
|
||||
const { colors } = useTheme();
|
||||
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 count = item.usernames.length;
|
||||
|
||||
|
@ -50,7 +49,6 @@ const AllReactionsListItem = ({ item, getCustomEmoji }: IAllReactionsListItemPro
|
|||
content={item.emoji}
|
||||
standardEmojiStyle={styles.allTabStandardEmojiStyle}
|
||||
customEmojiStyle={styles.allTabCustomEmojiStyle}
|
||||
baseUrl={server}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
/>
|
||||
<View style={styles.textContainer}>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
|
||||
import { TGetCustomEmoji, IEmoji } from '../../definitions';
|
||||
import { TGetCustomEmoji, ICustomEmoji } from '../../definitions';
|
||||
import ReactionsList from '.';
|
||||
import { mockedStore as store } from '../../reducers/mockedStore';
|
||||
import { updateSettings } from '../../actions/settings';
|
||||
|
@ -11,7 +11,7 @@ const getCustomEmoji: TGetCustomEmoji = content => {
|
|||
marioparty: { name: content, extension: 'gif' },
|
||||
react_rocket: { name: content, extension: 'png' },
|
||||
nyan_rocket: { name: content, extension: 'png' }
|
||||
}[content] as IEmoji;
|
||||
}[content] as ICustomEmoji;
|
||||
return customEmoji;
|
||||
};
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import { TGetCustomEmoji } from '../../definitions/IEmoji';
|
|||
import I18n from '../../i18n';
|
||||
import styles, { MIN_TAB_WIDTH } from './styles';
|
||||
import { useDimensions, useOrientation } from '../../dimensions';
|
||||
import { useAppSelector } from '../../lib/hooks';
|
||||
|
||||
interface ITabBarItem {
|
||||
getCustomEmoji: TGetCustomEmoji;
|
||||
|
@ -25,7 +24,6 @@ interface IReactionsTabBar {
|
|||
|
||||
const TabBarItem = ({ tab, index, goToPage, getCustomEmoji }: ITabBarItem) => {
|
||||
const { colors } = useTheme();
|
||||
const server = useAppSelector(state => state.server.server);
|
||||
return (
|
||||
<Pressable
|
||||
key={tab.emoji}
|
||||
|
@ -46,7 +44,6 @@ const TabBarItem = ({ tab, index, goToPage, getCustomEmoji }: ITabBarItem) => {
|
|||
content={tab.emoji}
|
||||
standardEmojiStyle={styles.standardEmojiStyle}
|
||||
customEmojiStyle={styles.customEmojiStyle}
|
||||
baseUrl={server}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
/>
|
||||
<Text style={[styles.reactionCount, { color: colors.auxiliaryTintColor }]}>{tab.usernames.length}</Text>
|
||||
|
|
|
@ -10,33 +10,24 @@ interface IEmoji {
|
|||
literal: string;
|
||||
isMessageContainsOnlyEmoji: boolean;
|
||||
getCustomEmoji?: Function;
|
||||
baseUrl: string;
|
||||
customEmojis?: any;
|
||||
style?: object;
|
||||
onEmojiSelected?: Function;
|
||||
tabEmojiStyle?: object;
|
||||
}
|
||||
|
||||
const Emoji = React.memo(
|
||||
({ literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl, customEmojis = true, style = {} }: IEmoji) => {
|
||||
const Emoji = React.memo(({ literal, isMessageContainsOnlyEmoji, getCustomEmoji, customEmojis = true, style = {} }: IEmoji) => {
|
||||
const { colors } = useTheme();
|
||||
const emojiUnicode = shortnameToUnicode(literal);
|
||||
const emoji: any = getCustomEmoji && getCustomEmoji(literal.replace(/:/g, ''));
|
||||
if (emoji && customEmojis) {
|
||||
return (
|
||||
<CustomEmoji
|
||||
baseUrl={baseUrl}
|
||||
style={[isMessageContainsOnlyEmoji ? styles.customEmojiBig : styles.customEmoji, style]}
|
||||
emoji={emoji}
|
||||
/>
|
||||
);
|
||||
return <CustomEmoji style={[isMessageContainsOnlyEmoji ? styles.customEmojiBig : styles.customEmoji, style]} emoji={emoji} />;
|
||||
}
|
||||
return (
|
||||
<Text style={[{ color: colors.bodyText }, isMessageContainsOnlyEmoji ? styles.textBig : styles.text, style]}>
|
||||
{emojiUnicode}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export default Emoji;
|
||||
|
|
|
@ -4,7 +4,7 @@ import { NavigationContainer } from '@react-navigation/native';
|
|||
|
||||
import Markdown, { MarkdownPreview } from '.';
|
||||
import { themes } from '../../lib/constants';
|
||||
import { TGetCustomEmoji, IEmoji } from '../../definitions/IEmoji';
|
||||
import { TGetCustomEmoji, ICustomEmoji } from '../../definitions/IEmoji';
|
||||
|
||||
const theme = 'light';
|
||||
|
||||
|
@ -16,7 +16,6 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
const baseUrl = 'https://open.rocket.chat';
|
||||
const longText =
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
|
||||
const lineBreakText = `a
|
||||
|
@ -34,7 +33,7 @@ const getCustomEmoji: TGetCustomEmoji = content => {
|
|||
marioparty: { name: content, extension: 'gif' },
|
||||
react_rocket: { name: content, extension: 'png' },
|
||||
nyan_rocket: { name: content, extension: 'png' }
|
||||
}[content] as IEmoji;
|
||||
}[content] as ICustomEmoji;
|
||||
return customEmoji;
|
||||
};
|
||||
|
||||
|
@ -108,13 +107,8 @@ export const Emoji = () => (
|
|||
<View style={styles.container}>
|
||||
<Markdown msg='Unicode: 😃😇👍' theme={theme} />
|
||||
<Markdown msg='Shortnames: :joy::+1:' theme={theme} />
|
||||
<Markdown
|
||||
msg='Custom emojis: :react_rocket: :nyan_rocket: :marioparty:'
|
||||
theme={theme}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
<Markdown msg='😃 :+1: :marioparty:' theme={theme} getCustomEmoji={getCustomEmoji} baseUrl={baseUrl} />
|
||||
<Markdown msg='Custom emojis: :react_rocket: :nyan_rocket: :marioparty:' theme={theme} getCustomEmoji={getCustomEmoji} />
|
||||
<Markdown msg='😃 :+1: :marioparty:' theme={theme} getCustomEmoji={getCustomEmoji} />
|
||||
</View>
|
||||
);
|
||||
|
||||
|
|
|
@ -33,7 +33,6 @@ interface IMarkdownProps {
|
|||
md?: MarkdownAST;
|
||||
mentions?: IUserMention[];
|
||||
getCustomEmoji?: TGetCustomEmoji;
|
||||
baseUrl?: string;
|
||||
username?: string;
|
||||
tmid?: string;
|
||||
numberOfLines?: number;
|
||||
|
@ -235,13 +234,12 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
|
|||
};
|
||||
|
||||
renderEmoji = ({ literal }: TLiteral) => {
|
||||
const { getCustomEmoji, baseUrl = '', customEmojis, style } = this.props;
|
||||
const { getCustomEmoji, customEmojis, style } = this.props;
|
||||
return (
|
||||
<MarkdownEmoji
|
||||
literal={literal}
|
||||
isMessageContainsOnlyEmoji={this.isMessageContainsOnlyEmoji}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
baseUrl={baseUrl}
|
||||
customEmojis={customEmojis}
|
||||
style={style}
|
||||
/>
|
||||
|
@ -312,18 +310,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
msg,
|
||||
md,
|
||||
mentions,
|
||||
channels,
|
||||
navToRoomInfo,
|
||||
useRealName,
|
||||
username = '',
|
||||
getCustomEmoji,
|
||||
baseUrl = '',
|
||||
onLinkPress
|
||||
} = this.props;
|
||||
const { msg, md, mentions, channels, navToRoomInfo, useRealName, username = '', getCustomEmoji, onLinkPress } = this.props;
|
||||
|
||||
if (!msg) {
|
||||
return null;
|
||||
|
@ -333,7 +320,6 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
|
|||
return (
|
||||
<NewMarkdown
|
||||
username={username}
|
||||
baseUrl={baseUrl}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
useRealName={useRealName}
|
||||
tokens={md}
|
||||
|
|
|
@ -3,7 +3,6 @@ import { Text } from 'react-native';
|
|||
import { Emoji as EmojiProps } from '@rocket.chat/message-parser';
|
||||
|
||||
import shortnameToUnicode from '../../../lib/methods/helpers/shortnameToUnicode';
|
||||
import { themes } from '../../../lib/constants';
|
||||
import { useTheme } from '../../../theme';
|
||||
import styles from '../styles';
|
||||
import CustomEmoji from '../../EmojiPicker/CustomEmoji';
|
||||
|
@ -15,21 +14,21 @@ interface IEmojiProps {
|
|||
}
|
||||
|
||||
const Emoji = ({ block, isBigEmoji }: IEmojiProps) => {
|
||||
const { theme } = useTheme();
|
||||
const { baseUrl, getCustomEmoji } = useContext(MarkdownContext);
|
||||
const { colors } = useTheme();
|
||||
const { getCustomEmoji } = useContext(MarkdownContext);
|
||||
|
||||
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 emojiUnicode = shortnameToUnicode(emojiToken);
|
||||
const emoji = getCustomEmoji?.(block.value?.value);
|
||||
|
||||
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 (
|
||||
<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}
|
||||
</Text>
|
||||
);
|
||||
|
|
|
@ -7,7 +7,6 @@ interface IMarkdownContext {
|
|||
channels?: IUserChannel[];
|
||||
useRealName?: boolean;
|
||||
username?: string;
|
||||
baseUrl?: string;
|
||||
navToRoomInfo?: Function;
|
||||
getCustomEmoji?: Function;
|
||||
onLinkPress?: Function;
|
||||
|
@ -18,7 +17,6 @@ const defaultState = {
|
|||
channels: [],
|
||||
useRealName: false,
|
||||
username: '',
|
||||
baseUrl: '',
|
||||
navToRoomInfo: () => {}
|
||||
};
|
||||
|
||||
|
|
|
@ -34,11 +34,8 @@ const getCustomEmoji = (content: string) => {
|
|||
}[content];
|
||||
return customEmoji;
|
||||
};
|
||||
const baseUrl = 'https://open.rocket.chat';
|
||||
|
||||
const NewMarkdown = ({ ...props }) => (
|
||||
<NewMarkdownComponent baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} username='rocket.cat' {...props} />
|
||||
);
|
||||
const NewMarkdown = ({ ...props }) => <NewMarkdownComponent getCustomEmoji={getCustomEmoji} username='rocket.cat' {...props} />;
|
||||
|
||||
const simpleTextMsg = [
|
||||
{
|
||||
|
@ -340,7 +337,7 @@ const emojiTokens = [
|
|||
export const Emoji = () => (
|
||||
<View style={styles.container}>
|
||||
<NewMarkdown tokens={bigEmojiTokens} />
|
||||
<NewMarkdown tokens={emojiTokens} getCustomEmoji={getCustomEmoji} baseUrl={baseUrl} />
|
||||
<NewMarkdown tokens={emojiTokens} getCustomEmoji={getCustomEmoji} />
|
||||
</View>
|
||||
);
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ interface IBodyProps {
|
|||
navToRoomInfo?: Function;
|
||||
useRealName?: boolean;
|
||||
username: string;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
const Body = ({
|
||||
|
@ -35,7 +34,6 @@ const Body = ({
|
|||
username,
|
||||
navToRoomInfo,
|
||||
getCustomEmoji,
|
||||
baseUrl,
|
||||
onLinkPress
|
||||
}: IBodyProps): React.ReactElement | null => {
|
||||
if (isEmpty(tokens)) {
|
||||
|
@ -51,7 +49,6 @@ const Body = ({
|
|||
username,
|
||||
navToRoomInfo,
|
||||
getCustomEmoji,
|
||||
baseUrl,
|
||||
onLinkPress
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -283,7 +283,6 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
|
|||
<Markdown
|
||||
msg={description}
|
||||
style={[isReply && style]}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
theme={theme}
|
||||
|
|
|
@ -30,7 +30,7 @@ export const Item = () => (
|
|||
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>
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -82,7 +82,7 @@ interface IMessageReply {
|
|||
const Fields = React.memo(
|
||||
({ attachment, getCustomEmoji }: IMessageFields) => {
|
||||
const { theme } = useTheme();
|
||||
const { baseUrl, user } = useContext(MessageContext);
|
||||
const { user } = useContext(MessageContext);
|
||||
|
||||
if (!attachment.fields) {
|
||||
return null;
|
||||
|
@ -97,7 +97,6 @@ const Fields = React.memo(
|
|||
</Text>
|
||||
<Markdown
|
||||
msg={field?.value || ''}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
theme={theme}
|
||||
|
|
|
@ -16,7 +16,7 @@ import { MessageTypesValues } from '../../definitions';
|
|||
const Content = React.memo(
|
||||
(props: IMessageContent) => {
|
||||
const { theme } = useTheme();
|
||||
const { baseUrl, user, onLinkPress } = useContext(MessageContext);
|
||||
const { user, onLinkPress } = useContext(MessageContext);
|
||||
|
||||
if (props.isInfo) {
|
||||
// @ts-ignore
|
||||
|
@ -54,7 +54,6 @@ const Content = React.memo(
|
|||
<Markdown
|
||||
msg={props.msg}
|
||||
md={props.md}
|
||||
baseUrl={baseUrl}
|
||||
getCustomEmoji={props.getCustomEmoji}
|
||||
enableMessageParser={user.enableMessageParserEarlyAdoption}
|
||||
username={user.username}
|
||||
|
|
|
@ -6,11 +6,11 @@ import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
|||
import { IMessageEmoji } from './interfaces';
|
||||
|
||||
const Emoji = React.memo(
|
||||
({ content, baseUrl, standardEmojiStyle, customEmojiStyle, getCustomEmoji }: IMessageEmoji) => {
|
||||
({ content, standardEmojiStyle, customEmojiStyle, getCustomEmoji }: IMessageEmoji) => {
|
||||
const parsedContent = content.replace(/^:|:$/g, '');
|
||||
const emoji = getCustomEmoji(parsedContent);
|
||||
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>;
|
||||
},
|
||||
|
|
|
@ -81,7 +81,6 @@ const ImageContainer = React.memo(
|
|||
<Markdown
|
||||
msg={file.description}
|
||||
style={[isReply && style]}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
theme={theme}
|
||||
|
|
|
@ -47,7 +47,7 @@ const AddReaction = React.memo(({ theme }: { theme: TSupportedThemes }) => {
|
|||
});
|
||||
|
||||
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;
|
||||
return (
|
||||
<Touchable
|
||||
|
@ -67,7 +67,6 @@ const Reaction = React.memo(({ reaction, getCustomEmoji, theme }: IMessageReacti
|
|||
content={reaction.emoji}
|
||||
standardEmojiStyle={styles.reactionEmoji}
|
||||
customEmojiStyle={styles.reactionCustomEmoji}
|
||||
baseUrl={baseUrl}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
/>
|
||||
<Text style={[styles.reactionCount, { color: themes[theme].tintColor }]}>{reaction.usernames.length}</Text>
|
||||
|
|
|
@ -121,7 +121,7 @@ const Description = React.memo(
|
|||
getCustomEmoji: TGetCustomEmoji;
|
||||
theme: TSupportedThemes;
|
||||
}) => {
|
||||
const { baseUrl, user } = useContext(MessageContext);
|
||||
const { user } = useContext(MessageContext);
|
||||
const text = attachment.text || attachment.title;
|
||||
|
||||
if (!text) {
|
||||
|
@ -132,7 +132,6 @@ const Description = React.memo(
|
|||
<Markdown
|
||||
msg={text}
|
||||
style={[{ color: themes[theme].auxiliaryTintColor, fontSize: 14 }]}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
theme={theme}
|
||||
|
@ -177,7 +176,7 @@ const Fields = React.memo(
|
|||
theme: TSupportedThemes;
|
||||
getCustomEmoji: TGetCustomEmoji;
|
||||
}) => {
|
||||
const { baseUrl, user } = useContext(MessageContext);
|
||||
const { user } = useContext(MessageContext);
|
||||
|
||||
if (!attachment.fields) {
|
||||
return null;
|
||||
|
@ -188,13 +187,7 @@ const Fields = React.memo(
|
|||
{attachment.fields.map(field => (
|
||||
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
|
||||
<Text style={[styles.fieldTitle, { color: themes[theme].bodyText }]}>{field.title}</Text>
|
||||
<Markdown
|
||||
msg={field?.value || ''}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
theme={theme}
|
||||
/>
|
||||
<Markdown msg={field?.value || ''} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
@ -278,13 +271,7 @@ const Reply = React.memo(
|
|||
) : null}
|
||||
</View>
|
||||
</Touchable>
|
||||
<Markdown
|
||||
msg={attachment.description}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
theme={theme}
|
||||
/>
|
||||
<Markdown msg={attachment.description} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -79,7 +79,6 @@ const Video = React.memo(
|
|||
<>
|
||||
<Markdown
|
||||
msg={file.description}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
style={[isReply && style]}
|
||||
|
|
|
@ -67,7 +67,6 @@ export interface IMessageContent {
|
|||
|
||||
export interface IMessageEmoji {
|
||||
content: string;
|
||||
baseUrl: string;
|
||||
standardEmojiStyle: { fontSize: number };
|
||||
customEmojiStyle: StyleProp<ImageStyle>;
|
||||
getCustomEmoji: TGetCustomEmoji;
|
||||
|
|
|
@ -1,43 +1,35 @@
|
|||
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;
|
||||
name: string;
|
||||
extension: string;
|
||||
extension?: string;
|
||||
isCustom: boolean;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface ICustomEmojis {
|
||||
[key: string]: Pick<IEmoji, 'name' | 'extension'>;
|
||||
}
|
||||
type TBasicEmoji = string;
|
||||
|
||||
export interface ICustomEmoji {
|
||||
baseUrl?: string;
|
||||
emoji: IEmoji;
|
||||
style: StyleProp<ImageStyle>;
|
||||
name: string;
|
||||
extension: string;
|
||||
}
|
||||
|
||||
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 {
|
||||
_id: string;
|
||||
name?: string;
|
||||
name: string;
|
||||
aliases?: string[];
|
||||
extension: string;
|
||||
_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;
|
||||
|
|
|
@ -480,6 +480,7 @@
|
|||
"Search_Messages": "Search Messages",
|
||||
"Search": "Search",
|
||||
"Search_by": "Search by",
|
||||
"Search_emoji": "Search emoji",
|
||||
"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.",
|
||||
"Seconds": "{{second}} seconds",
|
||||
|
|
|
@ -457,6 +457,7 @@
|
|||
"Search_Messages": "Buscar Mensagens",
|
||||
"Search": "Buscar",
|
||||
"Search_by": "Buscar por",
|
||||
"Search_emoji": "Buscar emoji",
|
||||
"Search_global_users": "Busca por usuários globais",
|
||||
"Search_global_users_description": "Caso ativado, busca por usuários de outras empresas ou servidores.",
|
||||
"Security_and_privacy": "Segurança e privacidade",
|
||||
|
|
|
@ -70,6 +70,7 @@ export const colors = {
|
|||
collapsibleQuoteBorder: '#CBCED1',
|
||||
collapsibleChevron: '#6C727A',
|
||||
cancelButton: '#E4E7EA',
|
||||
textInputSecondaryBackground: '#E4E7EA',
|
||||
...mentions
|
||||
},
|
||||
dark: {
|
||||
|
@ -122,6 +123,7 @@ export const colors = {
|
|||
collapsibleQuoteBorder: '#CBCED1',
|
||||
collapsibleChevron: '#6C727A',
|
||||
cancelButton: '#E4E7EA',
|
||||
textInputSecondaryBackground: '#030b1b', // backgroundColor
|
||||
...mentions
|
||||
},
|
||||
black: {
|
||||
|
@ -174,6 +176,7 @@ export const colors = {
|
|||
collapsibleQuoteBorder: '#CBCED1',
|
||||
collapsibleChevron: '#6C727A',
|
||||
cancelButton: '#E4E7EA',
|
||||
textInputSecondaryBackground: '#000000', // backgroundColor
|
||||
...mentions
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,44 +1,44 @@
|
|||
const list = ['frequentlyUsed', 'custom', 'people', 'nature', 'food', 'activity', 'travel', 'objects', 'symbols', 'flags'];
|
||||
const tabs = [
|
||||
{
|
||||
tabLabel: '🕒',
|
||||
tabLabel: 'clock',
|
||||
category: list[0]
|
||||
},
|
||||
{
|
||||
tabLabel: '🚀',
|
||||
tabLabel: 'rocket',
|
||||
category: list[1]
|
||||
},
|
||||
{
|
||||
tabLabel: '😃',
|
||||
tabLabel: 'emoji',
|
||||
category: list[2]
|
||||
},
|
||||
{
|
||||
tabLabel: '🐶',
|
||||
tabLabel: 'leaf',
|
||||
category: list[3]
|
||||
},
|
||||
{
|
||||
tabLabel: '🍔',
|
||||
tabLabel: 'burger',
|
||||
category: list[4]
|
||||
},
|
||||
{
|
||||
tabLabel: '⚽',
|
||||
tabLabel: 'basketball',
|
||||
category: list[5]
|
||||
},
|
||||
{
|
||||
tabLabel: '🚌',
|
||||
tabLabel: 'airplane',
|
||||
category: list[6]
|
||||
},
|
||||
{
|
||||
tabLabel: '💡',
|
||||
tabLabel: 'lamp-bulb',
|
||||
category: list[7]
|
||||
},
|
||||
{
|
||||
tabLabel: '💛',
|
||||
tabLabel: 'percentage',
|
||||
category: list[8]
|
||||
},
|
||||
{
|
||||
tabLabel: '🏁',
|
||||
tabLabel: 'flag',
|
||||
category: list[9]
|
||||
}
|
||||
];
|
||||
export default { list, tabs };
|
||||
export const categories = { list, tabs };
|
|
@ -2813,3 +2813,5 @@ export const emojis = [
|
|||
'flag_tc',
|
||||
'flag_mf'
|
||||
];
|
||||
|
||||
export const DEFAULT_EMOJIS = ['clap', 'thumbsup', 'heart_eyes', 'grinning', 'thinking', 'smiley'];
|
|
@ -0,0 +1,2 @@
|
|||
export * from './emojis';
|
||||
export * from './categories';
|
|
@ -1,5 +1,6 @@
|
|||
export * from './colors';
|
||||
export * from './constantDisplayMode';
|
||||
export * from './emojis';
|
||||
export * from './environment';
|
||||
export * from './keys';
|
||||
export * from './links';
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export * from './useAppSelector';
|
||||
export * from './usePermissions';
|
||||
export * from './useFrequentlyUsedEmoji';
|
||||
|
|
|
@ -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 };
|
||||
};
|
|
@ -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];
|
||||
};
|
|
@ -187,6 +187,7 @@ export default {
|
|||
ROOM_SEND_MESSAGE: 'room_send_message',
|
||||
ROOM_ENCRYPTED_PRESS: 'room_encrypted_press',
|
||||
ROOM_OPEN_EMOJI: 'room_open_emoji',
|
||||
ROOM_CLOSE_EMOJI: 'room_close_emoji',
|
||||
ROOM_AUDIO_RECORD: 'room_audio_record',
|
||||
ROOM_AUDIO_RECORD_F: 'room_audio_record_f',
|
||||
ROOM_AUDIO_FINISH: 'room_audio_finish',
|
||||
|
@ -238,6 +239,13 @@ export default {
|
|||
ROOM_MENTION_GO_USER_INFO: 'room_mention_go_user_info',
|
||||
COMMAND_RUN: 'command_run',
|
||||
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
|
||||
RA_JITSI_VIDEO: 'ra_jitsi_video',
|
||||
|
|
|
@ -3,6 +3,7 @@ export * from './callJitsi';
|
|||
export * from './canOpenRoom';
|
||||
export * from './clearCache';
|
||||
export * from './enterpriseModules';
|
||||
export * from './emojis';
|
||||
export * from './getCustomEmojis';
|
||||
export * from './getPermalinks';
|
||||
export * from './getPermissions';
|
||||
|
|
|
@ -23,13 +23,14 @@ import { IRoomInfoParam } from '../SearchMessagesView';
|
|||
import {
|
||||
IApplicationState,
|
||||
TMessageModel,
|
||||
IEmoji,
|
||||
ISubscription,
|
||||
SubscriptionType,
|
||||
IAttachment,
|
||||
IMessage,
|
||||
TAnyMessageModel,
|
||||
IUrl
|
||||
IUrl,
|
||||
TGetCustomEmoji,
|
||||
ICustomEmoji
|
||||
} from '../../definitions';
|
||||
import { Services } from '../../lib/services';
|
||||
|
||||
|
@ -45,7 +46,7 @@ interface IMessagesViewProps {
|
|||
StackNavigationProp<MasterDetailInsideStackParamList>
|
||||
>;
|
||||
route: RouteProp<ChatsStackParamList, 'MessagesView'>;
|
||||
customEmojis: { [key: string]: IEmoji };
|
||||
customEmojis: { [key: string]: ICustomEmoji };
|
||||
theme: TSupportedThemes;
|
||||
showActionSheet: (params: { options: string[]; hasCancel: boolean }) => void;
|
||||
useRealName: boolean;
|
||||
|
@ -297,7 +298,7 @@ class MessagesView extends React.Component<IMessagesViewProps, IMessagesViewStat
|
|||
}
|
||||
};
|
||||
|
||||
getCustomEmoji = (name: string) => {
|
||||
getCustomEmoji: TGetCustomEmoji = name => {
|
||||
const { customEmojis } = this.props;
|
||||
const emoji = customEmojis[name];
|
||||
if (emoji) {
|
||||
|
|
|
@ -1,85 +1,52 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import Modal from 'react-native-modal';
|
||||
|
||||
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 { IApplicationState } from '../../definitions';
|
||||
|
||||
const margin = isAndroid ? 40 : 20;
|
||||
const maxSize = 400;
|
||||
import { IEmoji } from '../../definitions';
|
||||
import { EventTypes } from '../../containers/EmojiPicker/interfaces';
|
||||
import { searchEmojis } from '../../lib/methods';
|
||||
import { useDebounce } from '../../lib/methods/helpers/debounce';
|
||||
import { EmojiSearch } from '../../containers/EmojiPicker/EmojiSearch';
|
||||
import { events, logEvent } from '../../lib/methods/helpers/log';
|
||||
|
||||
interface IReactionPickerProps {
|
||||
message?: any;
|
||||
show: boolean;
|
||||
isMasterDetail: boolean;
|
||||
reactionClose: () => void;
|
||||
onEmojiSelected: (shortname: string, id: string) => void;
|
||||
width: number;
|
||||
height: number;
|
||||
theme: TSupportedThemes;
|
||||
onEmojiSelected: (emoji: IEmoji, id: string) => void;
|
||||
}
|
||||
|
||||
class ReactionPicker extends React.Component<IReactionPickerProps> {
|
||||
shouldComponentUpdate(nextProps: IReactionPickerProps) {
|
||||
const { show, width, height } = this.props;
|
||||
return nextProps.show !== show || width !== nextProps.width || height !== nextProps.height;
|
||||
}
|
||||
const ReactionPicker = ({ onEmojiSelected, message, reactionClose }: IReactionPickerProps): React.ReactElement => {
|
||||
const [searchedEmojis, setSearchedEmojis] = React.useState<IEmoji[]>([]);
|
||||
const [searching, setSearching] = React.useState<boolean>(false);
|
||||
|
||||
onEmojiSelected = (emoji: string, shortname?: string) => {
|
||||
// standard emojis: `emoji` is unicode and `shortname` is :joy:
|
||||
// custom emojis: only `emoji` is returned with shortname type (:joy:)
|
||||
// to set reactions, we need shortname type
|
||||
const { onEmojiSelected, message } = this.props;
|
||||
if (message) {
|
||||
onEmojiSelected(shortname || emoji, message.id);
|
||||
}
|
||||
const handleTextChange = useDebounce((text: string) => {
|
||||
setSearching(text !== '');
|
||||
handleSearchEmojis(text);
|
||||
}, 300);
|
||||
|
||||
const handleSearchEmojis = async (text: string) => {
|
||||
logEvent(events.REACTION_PICKER_SEARCH_EMOJIS);
|
||||
const emojis = await searchEmojis(text);
|
||||
setSearchedEmojis(emojis);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { width, height, show, reactionClose, isMasterDetail, theme } = this.props;
|
||||
|
||||
let widthStyle = width - margin;
|
||||
let heightStyle = Math.min(width, height) - margin * 2;
|
||||
|
||||
if (isMasterDetail) {
|
||||
widthStyle = maxSize;
|
||||
heightStyle = maxSize;
|
||||
const handleEmojiSelect = (_eventType: EventTypes, emoji?: IEmoji) => {
|
||||
logEvent(events.REACTION_PICKER_EMOJI_SELECTED);
|
||||
if (message && emoji) {
|
||||
onEmojiSelected(emoji, message.id);
|
||||
}
|
||||
reactionClose();
|
||||
};
|
||||
|
||||
return show ? (
|
||||
<Modal
|
||||
isVisible={show}
|
||||
style={{ alignItems: 'center' }}
|
||||
onBackdropPress={reactionClose}
|
||||
onBackButtonPress={reactionClose}
|
||||
animationIn='fadeIn'
|
||||
animationOut='fadeOut'
|
||||
backdropOpacity={themes[theme].backdropOpacity}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.reactionPickerContainer,
|
||||
{
|
||||
width: widthStyle,
|
||||
height: heightStyle
|
||||
}
|
||||
]}
|
||||
testID='reaction-picker'
|
||||
>
|
||||
<EmojiPicker theme={theme} onEmojiSelected={this.onEmojiSelected} />
|
||||
return (
|
||||
<View style={styles.reactionPickerContainer} testID='reaction-picker'>
|
||||
<View style={styles.reactionSearchContainer}>
|
||||
<EmojiSearch onChangeText={handleTextChange} bottomSheet />
|
||||
</View>
|
||||
</Modal>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
<EmojiPicker onItemClicked={handleEmojiSelect} searching={searching} searchedEmojis={searchedEmojis} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: IApplicationState) => ({
|
||||
isMasterDetail: state.app.isMasterDetail
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(withTheme(ReactionPicker));
|
||||
export default ReactionPicker;
|
||||
|
|
|
@ -74,8 +74,9 @@ import {
|
|||
TMessageModel,
|
||||
TSubscriptionModel,
|
||||
TThreadModel,
|
||||
ICustomEmojis,
|
||||
IEmoji,
|
||||
ICustomEmojis
|
||||
TGetCustomEmoji
|
||||
} from '../../definitions';
|
||||
import { E2E_MESSAGE_TYPE, E2E_STATUS, MESSAGE_TYPE_ANY_LOAD, MessageTypeLoad, themes } from '../../lib/constants';
|
||||
import { TListRef } from './List/List';
|
||||
|
@ -112,7 +113,6 @@ const stateAttrsUpdate = [
|
|||
'loading',
|
||||
'editing',
|
||||
'replying',
|
||||
'reacting',
|
||||
'readOnly',
|
||||
'member',
|
||||
'canForwardGuest',
|
||||
|
@ -164,7 +164,6 @@ interface IRoomViewProps extends IActionSheetProvider, IBaseScreen<ChatsStackPar
|
|||
isMasterDetail: boolean;
|
||||
replyBroadcast: Function;
|
||||
width: number;
|
||||
height: number;
|
||||
insets: EdgeInsets;
|
||||
transferLivechatGuestPermission?: 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;
|
||||
replying: boolean;
|
||||
replyWithMention: boolean;
|
||||
reacting: boolean;
|
||||
readOnly: boolean;
|
||||
unreadsCount: number | null;
|
||||
roomUserId?: string | null;
|
||||
|
@ -274,7 +272,6 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
|
|||
editing: false,
|
||||
replying: !!selectedMessage,
|
||||
replyWithMention: false,
|
||||
reacting: false,
|
||||
readOnly: false,
|
||||
unreadsCount: null,
|
||||
roomUserId,
|
||||
|
@ -828,12 +825,27 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
|
|||
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) => {
|
||||
this.setState({ selectedMessage: message, reacting: true });
|
||||
this.messagebox?.current?.closeEmojiAndAction(() => {
|
||||
this.setState({ selectedMessage: message }, this.showReactionPicker);
|
||||
});
|
||||
};
|
||||
|
||||
onReactionClose = () => {
|
||||
this.setState({ selectedMessage: undefined, reacting: false });
|
||||
const { hideActionSheet } = this.props;
|
||||
this.setState({ selectedMessage: undefined }, hideActionSheet);
|
||||
};
|
||||
|
||||
onMessageLongPress = (message: TAnyMessageModel) => {
|
||||
|
@ -850,8 +862,14 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
|
|||
navigation.navigate('AttachmentView', { attachment });
|
||||
};
|
||||
|
||||
onReactionPress = async (shortname: string, messageId: string) => {
|
||||
onReactionPress = async (emoji: IEmoji, messageId: string) => {
|
||||
try {
|
||||
let shortname = '';
|
||||
if (typeof emoji === 'string') {
|
||||
shortname = emoji;
|
||||
} else {
|
||||
shortname = emoji.name;
|
||||
}
|
||||
await Services.setReaction(shortname, messageId);
|
||||
this.onReactionClose();
|
||||
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 emoji = customEmojis[name];
|
||||
if (emoji) {
|
||||
|
@ -1489,8 +1507,8 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
|
|||
|
||||
render() {
|
||||
console.count(`${this.constructor.name}.render calls`);
|
||||
const { room, selectedMessage, loading, reacting } = this.state;
|
||||
const { user, baseUrl, theme, navigation, Hide_System_Messages, width, height, serverVersion } = this.props;
|
||||
const { room, loading } = this.state;
|
||||
const { user, baseUrl, theme, navigation, Hide_System_Messages, width, serverVersion } = this.props;
|
||||
const { rid, t } = room;
|
||||
let sysMes;
|
||||
let bannerClosed;
|
||||
|
@ -1521,15 +1539,6 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
|
|||
/>
|
||||
{this.renderFooter()}
|
||||
{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} />
|
||||
<JoinCode ref={this.joinCode} onJoin={this.onJoin} rid={rid} t={t} theme={theme} />
|
||||
</SafeAreaView>
|
||||
|
|
|
@ -14,10 +14,13 @@ export default StyleSheet.create({
|
|||
alignItems: 'center',
|
||||
marginVertical: 15
|
||||
},
|
||||
reactionSearchContainer: {
|
||||
marginHorizontal: 12,
|
||||
marginBottom: 8
|
||||
},
|
||||
reactionPickerContainer: {
|
||||
borderRadius: 4,
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden'
|
||||
flex: 1,
|
||||
flexDirection: 'column'
|
||||
},
|
||||
bannerContainer: {
|
||||
paddingVertical: 12,
|
||||
|
@ -64,5 +67,14 @@ export default StyleSheet.create({
|
|||
previewMode: {
|
||||
fontSize: 16,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
searchbarContainer: {
|
||||
height: 56,
|
||||
marginBottom: 8,
|
||||
paddingHorizontal: 12
|
||||
},
|
||||
reactionPickerSearchbar: {
|
||||
paddingHorizontal: 20,
|
||||
minHeight: 48
|
||||
}
|
||||
});
|
||||
|
|
|
@ -31,11 +31,12 @@ import {
|
|||
IUser,
|
||||
TMessageModel,
|
||||
IUrl,
|
||||
IEmoji,
|
||||
IAttachment,
|
||||
ISubscription,
|
||||
SubscriptionType,
|
||||
TSubscriptionModel
|
||||
TSubscriptionModel,
|
||||
TGetCustomEmoji,
|
||||
ICustomEmoji
|
||||
} from '../../definitions';
|
||||
import { Services } from '../../lib/services';
|
||||
|
||||
|
@ -68,7 +69,7 @@ interface ISearchMessagesViewProps extends INavigationOption {
|
|||
baseUrl: string;
|
||||
serverVersion: string;
|
||||
customEmojis: {
|
||||
[key: string]: IEmoji;
|
||||
[key: string]: ICustomEmoji;
|
||||
};
|
||||
theme: TSupportedThemes;
|
||||
useRealName: boolean;
|
||||
|
@ -201,7 +202,7 @@ class SearchMessagesView extends React.Component<ISearchMessagesViewProps, ISear
|
|||
await this.getMessages(searchText, true);
|
||||
}, 1000);
|
||||
|
||||
getCustomEmoji = (name: string) => {
|
||||
getCustomEmoji: TGetCustomEmoji = name => {
|
||||
const { customEmojis } = this.props;
|
||||
const emoji = customEmojis[name];
|
||||
if (emoji) {
|
||||
|
|
|
@ -107,7 +107,9 @@ describe('Profile screen', () => {
|
|||
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-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 waitFor(element(by.id('profile-view-enter-password-sheet')))
|
||||
.toBeVisible()
|
||||
|
|
|
@ -8,8 +8,6 @@ import {
|
|||
tapBack,
|
||||
sleep,
|
||||
searchRoom,
|
||||
starMessage,
|
||||
pinMessage,
|
||||
dismissReviewNag,
|
||||
tryTapping,
|
||||
platformTypes,
|
||||
|
@ -63,9 +61,7 @@ describe('Room screen', () => {
|
|||
});
|
||||
|
||||
it('should have open emoji button', async () => {
|
||||
if (device.getPlatform() === 'android') {
|
||||
await expect(element(by.id('messagebox-open-emoji'))).toExist();
|
||||
}
|
||||
});
|
||||
|
||||
it('should have message input', async () => {
|
||||
|
@ -89,24 +85,110 @@ describe('Room screen', () => {
|
|||
await expect(element(by[textMatcher](`${data.random}message`)).atIndex(0)).toExist();
|
||||
});
|
||||
|
||||
it('should show/hide emoji keyboard', async () => {
|
||||
if (device.getPlatform() === 'android') {
|
||||
describe('Emoji Keyboard', () => {
|
||||
it('should open emoji keyboard, select an emoji and send it', 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('messagebox-close-emoji'))).toExist();
|
||||
await expect(element(by.id('messagebox-open-emoji'))).toBeNotVisible();
|
||||
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-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 waitFor(element(by.id('messagebox-keyboard-emoji')))
|
||||
.toBeNotVisible()
|
||||
.not.toBeVisible()
|
||||
.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 () => {
|
||||
await element(by.id('messagebox-input')).clearText();
|
||||
await element(by.id('messagebox-input')).typeText(':joy');
|
||||
await sleep(300);
|
||||
await waitFor(element(by.id('messagebox-container')))
|
||||
|
@ -223,10 +305,8 @@ describe('Room screen', () => {
|
|||
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||
await element(by[textMatcher]('Permalink')).atIndex(0).tap();
|
||||
|
||||
// TODO: test clipboard
|
||||
});
|
||||
|
||||
it('should copy message', async () => {
|
||||
await element(by[textMatcher](`${data.random}message`))
|
||||
.atIndex(0)
|
||||
|
@ -237,28 +317,9 @@ describe('Room screen', () => {
|
|||
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||
await element(by[textMatcher]('Copy')).atIndex(0).tap();
|
||||
|
||||
// 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 () => {
|
||||
await waitFor(element(by[textMatcher](`${data.random}message`)))
|
||||
.toExist()
|
||||
|
@ -275,19 +336,47 @@ describe('Room screen', () => {
|
|||
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||
await element(by.id('add-reaction')).tap();
|
||||
await waitFor(element(by.id('reaction-picker')))
|
||||
.toBeVisible()
|
||||
await waitFor(element(by.id('emoji-picker-tab-emoji')))
|
||||
.toExist()
|
||||
.withTimeout(2000);
|
||||
await element(by.id('reaction-picker-😃')).tap();
|
||||
await waitFor(element(by.id('reaction-picker-grinning')))
|
||||
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 1);
|
||||
await element(by.id('emoji-picker-tab-emoji')).tap();
|
||||
await waitFor(element(by.id('emoji-grinning')))
|
||||
.toExist()
|
||||
.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:')))
|
||||
.toExist()
|
||||
.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 () => {
|
||||
await element(by[textMatcher](`${data.random}message`))
|
||||
.atIndex(0)
|
||||
|
@ -297,37 +386,38 @@ describe('Room screen', () => {
|
|||
.withTimeout(2000);
|
||||
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||
await waitFor(element(by.id('message-actions-emoji-+1')))
|
||||
await waitFor(element(by.id('message-actions-emoji-upside_down')))
|
||||
.toBeVisible()
|
||||
.withTimeout(2000);
|
||||
await element(by.id('message-actions-emoji-+1')).tap();
|
||||
await waitFor(element(by.id('message-reaction-:+1:')))
|
||||
await element(by.id('message-actions-emoji-upside_down')).tap();
|
||||
await waitFor(element(by.id('message-reaction-:upside_down:')))
|
||||
.toBeVisible()
|
||||
.withTimeout(60000);
|
||||
});
|
||||
|
||||
it('should show reaction picker on add reaction button pressed and have frequently used emoji', async () => {
|
||||
await element(by.id('message-add-reaction')).tap();
|
||||
await waitFor(element(by.id('reaction-picker')))
|
||||
await waitFor(element(by.id('action-sheet')))
|
||||
.toExist()
|
||||
.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()
|
||||
.withTimeout(2000);
|
||||
await element(by.id('reaction-picker-😃')).tap();
|
||||
await waitFor(element(by.id('reaction-picker-grimacing')))
|
||||
await element(by.id('emoji-picker-tab-emoji')).tap();
|
||||
await waitFor(element(by.id('emoji-wink')))
|
||||
.toExist()
|
||||
.withTimeout(2000);
|
||||
await element(by.id('reaction-picker-grimacing')).tap();
|
||||
await waitFor(element(by.id('message-reaction-:grimacing:')))
|
||||
.withTimeout(10000);
|
||||
await element(by.id('emoji-wink')).tap();
|
||||
await waitFor(element(by.id('message-reaction-:wink:')))
|
||||
.toExist()
|
||||
.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 () => {
|
||||
await element(by.id('message-reaction-:grinning:')).longPress();
|
||||
await waitFor(element(by.id('reactionsList')))
|
||||
|
@ -340,7 +430,7 @@ describe('Room screen', () => {
|
|||
it('should remove reaction', async () => {
|
||||
await element(by.id('message-reaction-:grinning:')).tap();
|
||||
await waitFor(element(by.id('message-reaction-:grinning:')))
|
||||
.toBeNotVisible()
|
||||
.not.toExist()
|
||||
.withTimeout(60000);
|
||||
});
|
||||
|
||||
|
@ -364,7 +454,6 @@ describe('Room screen', () => {
|
|||
.toExist()
|
||||
.withTimeout(60000);
|
||||
});
|
||||
|
||||
it('should quote message', async () => {
|
||||
await mockMessage('quote');
|
||||
await element(by[textMatcher](`${data.random}quote`))
|
||||
|
@ -381,34 +470,9 @@ describe('Room screen', () => {
|
|||
.toExist()
|
||||
.withTimeout(2000);
|
||||
await element(by.id('messagebox-send-message')).tap();
|
||||
|
||||
// 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 () => {
|
||||
await mockMessage('delete');
|
||||
await waitFor(element(by[textMatcher](`${data.random}delete`)).atIndex(0)).toBeVisible();
|
||||
|
@ -424,7 +488,6 @@ describe('Room screen', () => {
|
|||
.toExist()
|
||||
.withTimeout(1000);
|
||||
await element(by[textMatcher]('Delete')).atIndex(0).tap();
|
||||
|
||||
const deleteAlertMessage = 'You will not be able to recover this message!';
|
||||
await waitFor(element(by[textMatcher](deleteAlertMessage)).atIndex(0))
|
||||
.toExist()
|
||||
|
|
|
@ -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-password')).replaceText('abc');
|
||||
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 element(by.id('room-info-edit-view-ro')).longPress(); // https://github.com/facebook/react-native/issues/28032
|
||||
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-react-when-ro')).tap();
|
||||
await swipe('up');
|
||||
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 swipe('down');
|
||||
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-react-when-ro'))).toHaveToggleValue(false);
|
||||
await expect(element(by.id('room-info-edit-view-ro'))).toHaveToggleValue(false);
|
||||
await expect(element(by.id('room-info-edit-view-react-when-ro'))).toBeNotVisible();
|
||||
await swipe('down');
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue