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