forked from Ivasoft/mattermost-mobile
New UI for Emoji picker (#6933)
* BottomSheet migration to react-native-bottom-sheet * Refactor Emoji picker to use bottom sheet * Add skin selector * Add Emoji Skin Tone tutorial * add selected indicator to tone selector * feedback review * show tooltip after 750ms * ux feedback review * ux feedback review #2 * Hide emoji picker scroll indicator
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Tutorial} from '@constants';
|
||||
import {GLOBAL_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {logError} from '@utils/log';
|
||||
@@ -22,16 +23,20 @@ export const storeDeviceToken = async (token: string, prepareRecordsOnly = false
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.DEVICE_TOKEN, token, prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeMultiServerTutorial = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.MULTI_SERVER_TUTORIAL, 'true', prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeOnboardingViewedValue = async (value = true) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.ONBOARDING, value, false);
|
||||
};
|
||||
|
||||
export const storeMultiServerTutorial = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(Tutorial.MULTI_SERVER, 'true', prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeProfileLongPressTutorial = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.PROFILE_LONG_PRESS_TUTORIAL, 'true', prepareRecordsOnly);
|
||||
return storeGlobal(Tutorial.PROFILE_LONG_PRESS, 'true', prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeSkinEmojiSelectorTutorial = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(Tutorial.EMOJI_SKIN_SELECTOR, 'true', prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeDontAskForReview = async (prepareRecordsOnly = false) => {
|
||||
|
||||
@@ -178,3 +178,19 @@ export const setDirectChannelVisible = async (serverUrl: string, channelId: stri
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const savePreferredSkinTone = async (serverUrl: string, skinCode: string) => {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const userId = await getCurrentUserId(database);
|
||||
const pref: PreferenceType = {
|
||||
user_id: userId,
|
||||
category: Preferences.CATEGORY_EMOJI,
|
||||
name: Preferences.EMOJI_SKINTONE,
|
||||
value: skinCode,
|
||||
};
|
||||
return savePreference(serverUrl, [pref]);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import SectionIcon from './icon';
|
||||
|
||||
export const SCROLLVIEW_NATIVE_ID = 'emojiSelector';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
bottom: 10,
|
||||
height: 35,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
},
|
||||
background: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
pane: {
|
||||
flexDirection: 'row',
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 10,
|
||||
width: '100%',
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.3),
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderWidth: 1,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
}));
|
||||
|
||||
export type SectionIconType = {
|
||||
key: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
currentIndex: number;
|
||||
sections: SectionIconType[];
|
||||
scrollToIndex: (index: number) => void;
|
||||
}
|
||||
|
||||
const EmojiSectionBar = ({currentIndex, sections, scrollToIndex}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
return (
|
||||
<KeyboardTrackingView
|
||||
scrollViewNativeID={SCROLLVIEW_NATIVE_ID}
|
||||
normalList={true}
|
||||
style={styles.container}
|
||||
testID='emoji_picker.emoji_sections.section_bar'
|
||||
>
|
||||
<View style={styles.background}>
|
||||
<View style={styles.pane}>
|
||||
{sections.map((section, index) => (
|
||||
<SectionIcon
|
||||
currentIndex={currentIndex}
|
||||
key={section.key}
|
||||
icon={section.icon}
|
||||
index={index}
|
||||
scrollToIndex={scrollToIndex}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardTrackingView>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmojiSectionBar;
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import Emoji from '@components/emoji';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
onEmojiPress: (emoji: string) => void;
|
||||
size?: number;
|
||||
style: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const TouchableEmoji = ({name, onEmojiPress, size = 30, style}: Props) => {
|
||||
const onPress = useCallback(preventDoubleTap(() => onEmojiPress(name)), []);
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={onPress}
|
||||
style={style}
|
||||
type={'opacity'}
|
||||
>
|
||||
<Emoji
|
||||
emojiName={name}
|
||||
size={size}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(TouchableEmoji);
|
||||
@@ -12,7 +12,7 @@ import {MAX_ALLOWED_REACTIONS} from '@constants/emoji';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation';
|
||||
import {bottomSheetModalOptions, openAsBottomSheet, showModal, showModalOverCurrentContext} from '@screens/navigation';
|
||||
import {getEmojiFirstAlias} from '@utils/emoji/helpers';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
@@ -61,7 +61,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
});
|
||||
|
||||
const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled, location, postId, reactions, theme}: ReactionsProps) => {
|
||||
const intl = useIntl();
|
||||
const {formatMessage} = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const isTablet = useIsTablet();
|
||||
const pressed = useRef(false);
|
||||
@@ -112,16 +112,14 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
||||
};
|
||||
|
||||
const handleAddReaction = useCallback(preventDoubleTap(() => {
|
||||
const title = intl.formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'});
|
||||
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
const passProps = {
|
||||
closeButton,
|
||||
onEmojiPress: handleAddReactionToPost,
|
||||
};
|
||||
|
||||
showModal(Screens.EMOJI_PICKER, title, passProps);
|
||||
}), [intl, theme]);
|
||||
openAsBottomSheet({
|
||||
closeButtonId: 'close-add-reaction',
|
||||
screen: Screens.EMOJI_PICKER,
|
||||
theme,
|
||||
title: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
|
||||
props: {onEmojiPress: handleAddReactionToPost},
|
||||
});
|
||||
}), [formatMessage, theme]);
|
||||
|
||||
const handleReactionPress = useCallback(async (emoji: string, remove: boolean) => {
|
||||
pressed.current = true;
|
||||
@@ -143,7 +141,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
||||
};
|
||||
|
||||
Keyboard.dismiss();
|
||||
const title = isTablet ? intl.formatMessage({id: 'post.reactions.title', defaultMessage: 'Reactions'}) : '';
|
||||
const title = isTablet ? formatMessage({id: 'post.reactions.title', defaultMessage: 'Reactions'}) : '';
|
||||
|
||||
if (!pressed.current) {
|
||||
if (isTablet) {
|
||||
@@ -152,7 +150,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
||||
showModalOverCurrentContext(screen, passProps, bottomSheetModalOptions(theme));
|
||||
}
|
||||
}
|
||||
}, [intl, isTablet, location, postId, theme]);
|
||||
}, [formatMessage, isTablet, location, postId, theme]);
|
||||
|
||||
let addMoreReactions = null;
|
||||
const {reactionsByName, highlightedReactions} = buildReactionsMap();
|
||||
|
||||
@@ -17,7 +17,7 @@ import * as Screens from '@constants/screens';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation';
|
||||
import {openAsBottomSheet} from '@screens/navigation';
|
||||
import {hasJumboEmojiOnly} from '@utils/emoji/helpers';
|
||||
import {fromAutoResponder, isFromWebhook, isPostFailed, isPostPendingOrFailed, isSystemMessage} from '@utils/post';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
@@ -192,11 +192,13 @@ const Post = ({
|
||||
const passProps = {sourceScreen: location, post, showAddReaction, serverUrl};
|
||||
const title = isTablet ? intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}) : '';
|
||||
|
||||
if (isTablet) {
|
||||
showModal(Screens.POST_OPTIONS, title, passProps, bottomSheetModalOptions(theme, 'close-post-options'));
|
||||
} else {
|
||||
showModalOverCurrentContext(Screens.POST_OPTIONS, passProps, bottomSheetModalOptions(theme));
|
||||
}
|
||||
openAsBottomSheet({
|
||||
closeButtonId: 'close-post-options',
|
||||
screen: Screens.POST_OPTIONS,
|
||||
theme,
|
||||
title,
|
||||
props: passProps,
|
||||
});
|
||||
};
|
||||
|
||||
const [, rerender] = useState(false);
|
||||
|
||||
61
app/components/touchable_emoji/index.tsx
Normal file
61
app/components/touchable_emoji/index.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {StyleProp, View, ViewStyle} from 'react-native';
|
||||
|
||||
import Emoji from '@components/emoji';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
import SkinnedEmoji from './skinned_emoji';
|
||||
|
||||
type Props = {
|
||||
category?: string;
|
||||
name: string;
|
||||
onEmojiPress: (emoji: string) => void;
|
||||
size?: number;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const CATEGORIES_WITH_SKINS = ['people-body'];
|
||||
|
||||
const hitSlop = {top: 10, bottom: 10, left: 10, right: 10};
|
||||
|
||||
const TouchableEmoji = ({category, name, onEmojiPress, size = 30, style}: Props) => {
|
||||
const onPress = useCallback(preventDoubleTap(() => onEmojiPress(name)), []);
|
||||
|
||||
let emoji;
|
||||
if (category && CATEGORIES_WITH_SKINS.includes(category)) {
|
||||
emoji = (
|
||||
<SkinnedEmoji
|
||||
name={name}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
emoji = (
|
||||
<Emoji
|
||||
emojiName={name}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={style}
|
||||
>
|
||||
<TouchableWithFeedback
|
||||
hitSlop={hitSlop}
|
||||
onPress={onPress}
|
||||
style={style}
|
||||
type={'opacity'}
|
||||
>
|
||||
{emoji}
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(TouchableEmoji);
|
||||
34
app/components/touchable_emoji/skinned_emoji.tsx
Normal file
34
app/components/touchable_emoji/skinned_emoji.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
|
||||
import {useEmojiSkinTone} from '@app/hooks/emoji_category_bar';
|
||||
import Emoji from '@components/emoji';
|
||||
import {skinCodes} from '@utils/emoji';
|
||||
import {isValidNamedEmoji} from '@utils/emoji/helpers';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const SkinnedEmoji = ({name, size = 30}: Props) => {
|
||||
const skinTone = useEmojiSkinTone();
|
||||
const emojiName = useMemo(() => {
|
||||
const skinnedEmoji = `${name}_${skinCodes[skinTone]}`;
|
||||
if (skinTone === 'default' || !isValidNamedEmoji(skinnedEmoji, [])) {
|
||||
return name;
|
||||
}
|
||||
return skinnedEmoji;
|
||||
}, [name, skinTone]);
|
||||
|
||||
return (
|
||||
<Emoji
|
||||
emojiName={emojiName}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SkinnedEmoji);
|
||||
@@ -127,7 +127,7 @@ const UserAvatarsStack = ({breakAt = 3, channelId, location, style: baseContaine
|
||||
|
||||
const snapPoints: BottomSheetProps['snapPoints'] = [1, bottomSheetSnapPoint(Math.min(users.length, 5), USER_ROW_HEIGHT, bottom) + TITLE_HEIGHT];
|
||||
if (users.length > 5) {
|
||||
snapPoints.push('90%');
|
||||
snapPoints.push('80%');
|
||||
}
|
||||
|
||||
bottomSheet({
|
||||
|
||||
@@ -78,8 +78,6 @@ export const GLOBAL_IDENTIFIERS = {
|
||||
DONT_ASK_FOR_REVIEW: 'dontAskForReview',
|
||||
FIRST_LAUNCH: 'firstLaunch',
|
||||
LAST_ASK_FOR_REVIEW: 'lastAskForReview',
|
||||
MULTI_SERVER_TUTORIAL: 'multiServerTutorial',
|
||||
PROFILE_LONG_PRESS_TUTORIAL: 'profileLongPressTutorial',
|
||||
ONBOARDING: 'onboarding',
|
||||
};
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import ServerErrors from './server_errors';
|
||||
import SnackBar from './snack_bar';
|
||||
import Sso from './sso';
|
||||
import SupportedServer from './supported_server';
|
||||
import Tutorial from './tutorial';
|
||||
import View from './view';
|
||||
import WebsocketEvents from './websocket';
|
||||
|
||||
@@ -71,6 +72,7 @@ export {
|
||||
SnackBar,
|
||||
Sso,
|
||||
SupportedServer,
|
||||
Tutorial,
|
||||
View,
|
||||
WebsocketEvents,
|
||||
};
|
||||
|
||||
12
app/constants/tutorial.ts
Normal file
12
app/constants/tutorial.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const MULTI_SERVER = 'multiServerTutorial';
|
||||
export const PROFILE_LONG_PRESS = 'profileLongPressTutorial';
|
||||
export const EMOJI_SKIN_SELECTOR = 'emojiSkinSelectorTutorial';
|
||||
|
||||
export default {
|
||||
MULTI_SERVER,
|
||||
PROFILE_LONG_PRESS,
|
||||
EMOJI_SKIN_SELECTOR,
|
||||
};
|
||||
93
app/hooks/emoji_category_bar.ts
Normal file
93
app/hooks/emoji_category_bar.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
export type EmojiCategoryBarIcon = {
|
||||
key: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
type EmojiCategoryBar = {
|
||||
currentIndex: number;
|
||||
selectedIndex?: number;
|
||||
icons?: EmojiCategoryBarIcon[];
|
||||
skinTone: string;
|
||||
};
|
||||
|
||||
const defaultState: EmojiCategoryBar = {
|
||||
icons: undefined,
|
||||
currentIndex: 0,
|
||||
selectedIndex: undefined,
|
||||
skinTone: 'default',
|
||||
};
|
||||
|
||||
const subject: BehaviorSubject<EmojiCategoryBar> = new BehaviorSubject(defaultState);
|
||||
|
||||
const getEmojiCategoryBarState = () => {
|
||||
return subject.value;
|
||||
};
|
||||
|
||||
export const selectEmojiCategoryBarSection = (index?: number) => {
|
||||
const prevState = getEmojiCategoryBarState();
|
||||
subject.next({
|
||||
...prevState,
|
||||
selectedIndex: index,
|
||||
});
|
||||
};
|
||||
|
||||
export const setEmojiCategoryBarSection = (index: number) => {
|
||||
const prevState = getEmojiCategoryBarState();
|
||||
subject.next({
|
||||
...prevState,
|
||||
currentIndex: index,
|
||||
});
|
||||
};
|
||||
|
||||
export const setEmojiCategoryBarIcons = (icons?: EmojiCategoryBarIcon[]) => {
|
||||
const prevState = getEmojiCategoryBarState();
|
||||
subject.next({
|
||||
...prevState,
|
||||
icons,
|
||||
});
|
||||
};
|
||||
|
||||
export const setEmojiSkinTone = (skinTone: string) => {
|
||||
const prevState = getEmojiCategoryBarState();
|
||||
subject.next({
|
||||
...prevState,
|
||||
skinTone,
|
||||
});
|
||||
};
|
||||
|
||||
export const useEmojiCategoryBar = () => {
|
||||
const [state, setState] = useState(defaultState);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = subject.subscribe(setState);
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
subject.next(defaultState);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export const useEmojiSkinTone = () => {
|
||||
const [tone, setTone] = useState(defaultState.skinTone);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = subject.subscribe((state) => {
|
||||
setTone(state.skinTone);
|
||||
});
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return tone;
|
||||
};
|
||||
@@ -31,17 +31,6 @@ export const queryGlobalValue = (key: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const observeMultiServerTutorial = () => {
|
||||
const query = queryGlobalValue(GLOBAL_IDENTIFIERS.MULTI_SERVER_TUTORIAL);
|
||||
if (!query) {
|
||||
return of$(false);
|
||||
}
|
||||
return query.observe().pipe(
|
||||
switchMap((result) => (result.length ? result[0].observe() : of$(false))),
|
||||
switchMap((v) => of$(Boolean(v))),
|
||||
);
|
||||
};
|
||||
|
||||
export const getOnboardingViewed = async (): Promise<boolean> => {
|
||||
try {
|
||||
const {database} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
@@ -52,17 +41,6 @@ export const getOnboardingViewed = async (): Promise<boolean> => {
|
||||
}
|
||||
};
|
||||
|
||||
export const observeProfileLongPresTutorial = () => {
|
||||
const query = queryGlobalValue(GLOBAL_IDENTIFIERS.PROFILE_LONG_PRESS_TUTORIAL);
|
||||
if (!query) {
|
||||
return of$(false);
|
||||
}
|
||||
return query.observe().pipe(
|
||||
switchMap((result) => (result.length ? result[0].observe() : of$(false))),
|
||||
switchMap((v) => of$(Boolean(v))),
|
||||
);
|
||||
};
|
||||
|
||||
export const getLastAskedForReview = async () => {
|
||||
const records = await queryGlobalValue(GLOBAL_IDENTIFIERS.LAST_ASK_FOR_REVIEW)?.fetch();
|
||||
if (!records?.[0]?.value) {
|
||||
@@ -85,3 +63,14 @@ export const getFirstLaunch = async () => {
|
||||
|
||||
return records[0].value;
|
||||
};
|
||||
|
||||
export const observeTutorialWatched = (tutorial: string) => {
|
||||
const query = queryGlobalValue(tutorial);
|
||||
if (!query) {
|
||||
return of$(false);
|
||||
}
|
||||
return query.observe().pipe(
|
||||
switchMap((result) => (result.length ? result[0].observe() : of$(false))),
|
||||
switchMap((v) => of$(Boolean(v))),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import BottomSheetM, {BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetFooterProps} from '@gorhom/bottom-sheet';
|
||||
import React, {ReactNode, useCallback, useEffect, useMemo, useRef} from 'react';
|
||||
import {DeviceEventEmitter, Keyboard, View} from 'react-native';
|
||||
import {DeviceEventEmitter, Handle, InteractionManager, Keyboard, StyleProp, View, ViewStyle} from 'react-native';
|
||||
|
||||
import useNavButtonPressed from '@app/hooks/navigation_button_pressed';
|
||||
import {Events} from '@constants';
|
||||
@@ -24,6 +24,7 @@ export {default as BottomSheetContent, TITLE_HEIGHT} from './content';
|
||||
type Props = {
|
||||
closeButtonId?: string;
|
||||
componentId: string;
|
||||
contentStyle?: StyleProp<ViewStyle>;
|
||||
initialSnapIndex?: number;
|
||||
footerComponent?: React.FC<BottomSheetFooterProps>;
|
||||
renderContent: () => ReactNode;
|
||||
@@ -80,16 +81,18 @@ export const animatedConfig: Omit<WithSpringConfig, 'velocity'> = {
|
||||
const BottomSheet = ({
|
||||
closeButtonId,
|
||||
componentId,
|
||||
contentStyle,
|
||||
initialSnapIndex = 1,
|
||||
footerComponent,
|
||||
renderContent,
|
||||
snapPoints = [1, '50%', '90%'],
|
||||
snapPoints = [1, '50%', '80%'],
|
||||
testID,
|
||||
}: Props) => {
|
||||
const sheetRef = useRef<BottomSheetM>(null);
|
||||
const isTablet = useIsTablet();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const interaction = useRef<Handle>();
|
||||
|
||||
const bottomSheetBackgroundStyle = useMemo(() => [
|
||||
styles.bottomSheetBackground,
|
||||
@@ -112,6 +115,10 @@ const BottomSheet = ({
|
||||
return () => listener.remove();
|
||||
}, [close]);
|
||||
|
||||
const handleAnimationStart = useCallback(() => {
|
||||
interaction.current = InteractionManager.createInteractionHandle();
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (sheetRef.current) {
|
||||
sheetRef.current.close();
|
||||
@@ -120,7 +127,14 @@ const BottomSheet = ({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDismissIfNeeded = useCallback((index: number) => {
|
||||
const handleChange = useCallback((index: number) => {
|
||||
setTimeout(() => {
|
||||
if (interaction.current) {
|
||||
InteractionManager.clearInteractionHandle(interaction.current);
|
||||
interaction.current = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
if (index <= 0) {
|
||||
close();
|
||||
}
|
||||
@@ -147,7 +161,7 @@ const BottomSheet = ({
|
||||
|
||||
const renderContainerContent = () => (
|
||||
<View
|
||||
style={[styles.content, isTablet && styles.contentTablet]}
|
||||
style={[styles.content, isTablet && styles.contentTablet, contentStyle]}
|
||||
testID={`${testID}.screen`}
|
||||
>
|
||||
{renderContent()}
|
||||
@@ -170,12 +184,15 @@ const BottomSheet = ({
|
||||
snapPoints={snapPoints}
|
||||
animateOnMount={true}
|
||||
backdropComponent={renderBackdrop}
|
||||
onChange={handleDismissIfNeeded}
|
||||
onAnimate={handleAnimationStart}
|
||||
onChange={handleChange}
|
||||
animationConfigs={animatedConfig}
|
||||
handleComponent={Indicator}
|
||||
style={styles.bottomSheet}
|
||||
backgroundStyle={bottomSheetBackgroundStyle}
|
||||
footerComponent={footerComponent}
|
||||
keyboardBehavior='extend'
|
||||
keyboardBlurBehavior='restore'
|
||||
>
|
||||
{renderContainerContent()}
|
||||
</BottomSheetM>
|
||||
|
||||
@@ -6,8 +6,8 @@ import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {General} from '@constants';
|
||||
import {observeProfileLongPresTutorial} from '@queries/app/global';
|
||||
import {General, Tutorial} from '@constants';
|
||||
import {observeTutorialWatched} from '@queries/app/global';
|
||||
import {observeConfigValue, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeTeammateNameDisplay} from '@queries/servers/user';
|
||||
|
||||
@@ -24,7 +24,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
teammateNameDisplay: observeTeammateNameDisplay(database),
|
||||
currentUserId: observeCurrentUserId(database),
|
||||
currentTeamId: observeCurrentTeamId(database),
|
||||
tutorialWatched: observeProfileLongPresTutorial(),
|
||||
tutorialWatched: observeTutorialWatched(Tutorial.PROFILE_LONG_PRESS),
|
||||
restrictDirectMessage,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {updateLocalCustomStatus} from '@actions/local/user';
|
||||
import {removeRecentCustomStatus, updateCustomStatus, unsetCustomStatus} from '@actions/remote/user';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TabletTitle from '@components/tablet_title';
|
||||
import {Events, Screens} from '@constants';
|
||||
import {CustomStatusDurationEnum, SET_CUSTOM_STATUS_FAILURE} from '@constants/custom_status';
|
||||
@@ -18,7 +17,7 @@ import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {dismissModal, goToScreen, showModal} from '@screens/navigation';
|
||||
import {dismissModal, goToScreen, openAsBottomSheet, showModal} from '@screens/navigation';
|
||||
import {getCurrentMomentForTimezone, getRoundedTime} from '@utils/helpers';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {mergeNavigationOptions} from '@utils/navigation';
|
||||
@@ -285,12 +284,12 @@ const CustomStatus = ({
|
||||
}, [newStatus, isStatusSet, storedStatus, currentUser]);
|
||||
|
||||
const openEmojiPicker = useCallback(preventDoubleTap(() => {
|
||||
CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor).then((source) => {
|
||||
const screen = Screens.EMOJI_PICKER;
|
||||
const title = intl.formatMessage({id: 'mobile.custom_status.choose_emoji', defaultMessage: 'Choose an emoji'});
|
||||
const passProps = {closeButton: source, onEmojiPress: handleEmojiClick};
|
||||
|
||||
showModal(screen, title, passProps);
|
||||
openAsBottomSheet({
|
||||
closeButtonId: 'close-emoji-picker',
|
||||
screen: Screens.EMOJI_PICKER,
|
||||
theme,
|
||||
title: intl.formatMessage({id: 'mobile.custom_status.choose_emoji', defaultMessage: 'Choose an emoji'}),
|
||||
props: {onEmojiPress: handleEmojiClick},
|
||||
});
|
||||
}), [theme, intl, handleEmojiClick]);
|
||||
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect} from 'react';
|
||||
import {Keyboard} from 'react-native';
|
||||
import React, {useCallback} from 'react';
|
||||
import {DeviceEventEmitter, StyleSheet} from 'react-native';
|
||||
|
||||
import EmojiPicker from '@components/emoji_picker';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {dismissModal, setButtons} from '@screens/navigation';
|
||||
import {Events} from '@constants';
|
||||
import BottomSheet from '@screens/bottom_sheet';
|
||||
|
||||
import Picker from './picker';
|
||||
import PickerFooter from './picker/footer';
|
||||
|
||||
type Props = {
|
||||
componentId: string;
|
||||
onEmojiPress: (emoji: string) => void;
|
||||
closeButton: never;
|
||||
closeButtonId: string;
|
||||
};
|
||||
|
||||
const EMOJI_PICKER_BUTTON = 'close-add-reaction';
|
||||
|
||||
const EmojiPickerScreen = ({closeButton, componentId, onEmojiPress}: Props) => {
|
||||
useEffect(() => {
|
||||
setButtons(componentId, {
|
||||
leftButtons: [
|
||||
{
|
||||
icon: closeButton,
|
||||
id: EMOJI_PICKER_BUTTON,
|
||||
testID: 'close.emoji_picker.button',
|
||||
},
|
||||
],
|
||||
rightButtons: [],
|
||||
});
|
||||
}, []);
|
||||
|
||||
const close = () => {
|
||||
Keyboard.dismiss();
|
||||
dismissModal({componentId});
|
||||
};
|
||||
|
||||
useNavButtonPressed(EMOJI_PICKER_BUTTON, componentId, close, []);
|
||||
const style = StyleSheet.create({
|
||||
contentStyle: {
|
||||
paddingTop: 14,
|
||||
},
|
||||
});
|
||||
|
||||
const EmojiPickerScreen = ({closeButtonId, componentId, onEmojiPress}: Props) => {
|
||||
const handleEmojiPress = useCallback((emoji: string) => {
|
||||
onEmojiPress(emoji);
|
||||
close();
|
||||
DeviceEventEmitter.emit(Events.CLOSE_BOTTOM_SHEET);
|
||||
}, []);
|
||||
|
||||
const renderContent = useCallback(() => {
|
||||
return (
|
||||
<Picker
|
||||
onEmojiPress={handleEmojiPress}
|
||||
testID='emoji_picker'
|
||||
/>
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EmojiPicker
|
||||
onEmojiPress={handleEmojiPress}
|
||||
testID='emoji_picker'
|
||||
<BottomSheet
|
||||
renderContent={renderContent}
|
||||
closeButtonId={closeButtonId}
|
||||
componentId={componentId}
|
||||
contentStyle={style.contentStyle}
|
||||
initialSnapIndex={1}
|
||||
footerComponent={PickerFooter}
|
||||
testID='post_options'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,28 +18,31 @@ type Props = {
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
height: 35,
|
||||
justifyContent: 'center',
|
||||
zIndex: 10,
|
||||
},
|
||||
icon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.4),
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
},
|
||||
selectedContainer: {
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
|
||||
borderRadius: 4,
|
||||
},
|
||||
selected: {
|
||||
color: theme.centerChannelColor,
|
||||
color: theme.buttonBg,
|
||||
},
|
||||
}));
|
||||
|
||||
const SectionIcon = ({currentIndex, icon, index, scrollToIndex, theme}: Props) => {
|
||||
const EmojiCategoryBarIcon = ({currentIndex, icon, index, scrollToIndex, theme}: Props) => {
|
||||
const style = getStyleSheet(theme);
|
||||
const onPress = useCallback(preventDoubleTap(() => scrollToIndex(index)), []);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
style={style.container}
|
||||
style={[style.container, currentIndex === index ? style.selectedContainer : undefined]}
|
||||
>
|
||||
<CompassIcon
|
||||
name={icon}
|
||||
@@ -50,4 +53,4 @@ const SectionIcon = ({currentIndex, icon, index, scrollToIndex, theme}: Props) =
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionIcon;
|
||||
export default EmojiCategoryBarIcon;
|
||||
67
app/screens/emoji_picker/picker/emoji_category_bar/index.tsx
Normal file
67
app/screens/emoji_picker/picker/emoji_category_bar/index.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {selectEmojiCategoryBarSection, useEmojiCategoryBar} from '@hooks/emoji_category_bar';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import EmojiCategoryBarIcon from './icon';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
height: 55,
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 11,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
borderTopWidth: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
onSelect?: (index: number | undefined) => void;
|
||||
}
|
||||
|
||||
const EmojiCategoryBar = ({onSelect}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const {currentIndex, icons} = useEmojiCategoryBar();
|
||||
|
||||
const scrollToIndex = useCallback((index: number) => {
|
||||
if (onSelect) {
|
||||
onSelect(index);
|
||||
return;
|
||||
}
|
||||
|
||||
selectEmojiCategoryBarSection(index);
|
||||
}, []);
|
||||
|
||||
if (!icons) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.container}
|
||||
testID='emoji_picker.category_bar'
|
||||
>
|
||||
{icons.map((icon, index) => (
|
||||
<EmojiCategoryBarIcon
|
||||
currentIndex={currentIndex}
|
||||
key={icon.key}
|
||||
icon={icon.icon}
|
||||
index={index}
|
||||
scrollToIndex={scrollToIndex}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmojiCategoryBar;
|
||||
@@ -1,11 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BottomSheetFlatList} from '@gorhom/bottom-sheet';
|
||||
import Fuse from 'fuse.js';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {FlatList, ListRenderItemInfo, StyleSheet, View} from 'react-native';
|
||||
|
||||
import NoResultsWithTerm from '@components/no_results_with_term';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {getEmojis, searchEmojis} from '@utils/emoji/helpers';
|
||||
|
||||
import EmojiItem from './emoji_item';
|
||||
@@ -14,7 +16,6 @@ import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji
|
||||
|
||||
type Props = {
|
||||
customEmojis: CustomEmojiModel[];
|
||||
keyboardHeight: number;
|
||||
skinTone: string;
|
||||
searchTerm: string;
|
||||
onEmojiPress: (emojiName: string) => void;
|
||||
@@ -28,9 +29,9 @@ const style = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
const EmojiFiltered = ({customEmojis, keyboardHeight, skinTone, searchTerm, onEmojiPress}: Props) => {
|
||||
const EmojiFiltered = ({customEmojis, skinTone, searchTerm, onEmojiPress}: Props) => {
|
||||
const isTablet = useIsTablet();
|
||||
const emojis = useMemo(() => getEmojis(skinTone, customEmojis), [skinTone, customEmojis]);
|
||||
const flatListStyle = useMemo(() => ({flexGrow: 1, paddingBottom: keyboardHeight}), [keyboardHeight]);
|
||||
|
||||
const fuse = useMemo(() => {
|
||||
const options = {findAllMatches: true, ignoreLocation: true, includeMatches: true, shouldSort: false, includeScore: true};
|
||||
@@ -45,6 +46,8 @@ const EmojiFiltered = ({customEmojis, keyboardHeight, skinTone, searchTerm, onEm
|
||||
return searchEmojis(fuse, searchTerm);
|
||||
}, [fuse, searchTerm]);
|
||||
|
||||
const List = useMemo(() => (isTablet ? FlatList : BottomSheetFlatList), [isTablet]);
|
||||
|
||||
const keyExtractor = useCallback((item: string) => item, []);
|
||||
|
||||
const renderEmpty = useCallback(() => {
|
||||
@@ -65,8 +68,7 @@ const EmojiFiltered = ({customEmojis, keyboardHeight, skinTone, searchTerm, onEm
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={flatListStyle}
|
||||
<List
|
||||
data={data}
|
||||
initialNumToRender={30}
|
||||
keyboardDismissMode='interactive'
|
||||
23
app/screens/emoji_picker/picker/filtered/index.ts
Normal file
23
app/screens/emoji_picker/picker/filtered/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
|
||||
import EmojiFiltered from './filtered';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
skinTone: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE).
|
||||
observeWithColumns(['value']).pipe(
|
||||
switchMap((prefs) => of$(prefs?.[0]?.value ?? 'default')),
|
||||
),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(EmojiFiltered));
|
||||
69
app/screens/emoji_picker/picker/footer/index.tsx
Normal file
69
app/screens/emoji_picker/picker/footer/index.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BottomSheetFooter, BottomSheetFooterProps, SHEET_STATE, useBottomSheet, useBottomSheetInternal} from '@gorhom/bottom-sheet';
|
||||
import React, {useCallback} from 'react';
|
||||
import {Platform} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useKeyboardHeight} from '@hooks/device';
|
||||
import {selectEmojiCategoryBarSection} from '@hooks/emoji_category_bar';
|
||||
|
||||
import EmojiCategoryBar from '../emoji_category_bar';
|
||||
|
||||
const PickerFooter = (props: BottomSheetFooterProps) => {
|
||||
const theme = useTheme();
|
||||
const keyboardHeight = useKeyboardHeight();
|
||||
const {animatedSheetState} = useBottomSheetInternal();
|
||||
const {expand} = useBottomSheet();
|
||||
|
||||
const scrollToIndex = useCallback((index: number) => {
|
||||
if (animatedSheetState.value === SHEET_STATE.EXTENDED) {
|
||||
selectEmojiCategoryBarSection(index);
|
||||
return;
|
||||
}
|
||||
expand();
|
||||
|
||||
// @ts-expect-error wait until the bottom sheet is epanded
|
||||
while (animatedSheetState.value !== SHEET_STATE.EXTENDED) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
selectEmojiCategoryBarSection(index);
|
||||
}, []);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
const paddingBottom = withTiming(
|
||||
Platform.OS === 'ios' ? 20 : 0,
|
||||
{duration: 250},
|
||||
);
|
||||
return {backgroundColor: theme.centerChannelBg, paddingBottom};
|
||||
}, [theme]);
|
||||
|
||||
const heightAnimatedStyle = useAnimatedStyle(() => {
|
||||
let height = 55;
|
||||
if (keyboardHeight === 0 && Platform.OS === 'ios') {
|
||||
height += 20;
|
||||
} else if (keyboardHeight) {
|
||||
height = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
height,
|
||||
};
|
||||
}, [keyboardHeight]);
|
||||
|
||||
return (
|
||||
<BottomSheetFooter
|
||||
style={heightAnimatedStyle}
|
||||
{...props}
|
||||
>
|
||||
<Animated.View style={[animatedStyle]}>
|
||||
<EmojiCategoryBar onSelect={scrollToIndex}/>
|
||||
</Animated.View>
|
||||
</BottomSheetFooter>
|
||||
);
|
||||
};
|
||||
|
||||
export default PickerFooter;
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useBottomSheet} from '@gorhom/bottom-sheet';
|
||||
import React, {useCallback} from 'react';
|
||||
import {NativeSyntheticEvent, TextInputFocusEventData} from 'react-native';
|
||||
|
||||
import SearchBar, {SearchProps} from '@components/search';
|
||||
|
||||
const BottomSheetSearch = ({onFocus, ...props}: SearchProps) => {
|
||||
const {expand} = useBottomSheet();
|
||||
|
||||
const handleOnFocus = useCallback((event: NativeSyntheticEvent<TextInputFocusEventData>) => {
|
||||
expand();
|
||||
onFocus?.(event);
|
||||
}, [onFocus, expand]);
|
||||
|
||||
return (
|
||||
<SearchBar
|
||||
onFocus={handleOnFocus}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BottomSheetSearch;
|
||||
85
app/screens/emoji_picker/picker/header/header.tsx
Normal file
85
app/screens/emoji_picker/picker/header/header.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect} from 'react';
|
||||
import {LayoutChangeEvent, StyleSheet, View} from 'react-native';
|
||||
import {useSharedValue} from 'react-native-reanimated';
|
||||
|
||||
import SearchBar, {SearchProps} from '@components/search';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {setEmojiSkinTone} from '@hooks/emoji_category_bar';
|
||||
|
||||
import BottomSheetSearch from './bottom_sheet_search';
|
||||
import SkinToneSelector from './skintone_selector';
|
||||
|
||||
type Props = SearchProps & {
|
||||
skinTone: string;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {flex: 1},
|
||||
row: {flexDirection: 'row'},
|
||||
});
|
||||
|
||||
const PickerHeader = ({skinTone, ...props}: Props) => {
|
||||
const isTablet = useIsTablet();
|
||||
const containerWidth = useSharedValue(0);
|
||||
const isSearching = useSharedValue(false);
|
||||
|
||||
useEffect(() => {
|
||||
const req = requestAnimationFrame(() => {
|
||||
setEmojiSkinTone(skinTone);
|
||||
});
|
||||
|
||||
return () => cancelAnimationFrame(req);
|
||||
}, [skinTone]);
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
isSearching.value = false;
|
||||
}, []);
|
||||
|
||||
const onFocus = useCallback(() => {
|
||||
isSearching.value = true;
|
||||
}, []);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
containerWidth.value = e.nativeEvent.layout.width;
|
||||
}, []);
|
||||
|
||||
let search;
|
||||
if (isTablet) {
|
||||
search = (
|
||||
<SearchBar
|
||||
{...props}
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
search = (
|
||||
<BottomSheetSearch
|
||||
{...props}
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
onLayout={onLayout}
|
||||
style={styles.row}
|
||||
>
|
||||
<View style={styles.flex}>
|
||||
{search}
|
||||
</View>
|
||||
<SkinToneSelector
|
||||
skinTone={skinTone}
|
||||
containerWidth={containerWidth}
|
||||
isSearching={isSearching}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PickerHeader;
|
||||
23
app/screens/emoji_picker/picker/header/index.ts
Normal file
23
app/screens/emoji_picker/picker/header/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
|
||||
import PickerHeader from './header';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
skinTone: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE).
|
||||
observeWithColumns(['value']).pipe(
|
||||
switchMap((prefs) => of$(prefs?.[0]?.value ?? 'default')),
|
||||
),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(PickerHeader));
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {TouchableOpacity} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
collapse: () => void;
|
||||
};
|
||||
|
||||
const hitSlop = {top: 10, bottom: 10, left: 10, right: 10};
|
||||
|
||||
const CloseButton = ({collapse}: Props) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<TouchableOpacity
|
||||
hitSlop={hitSlop}
|
||||
onPress={collapse}
|
||||
>
|
||||
<CompassIcon
|
||||
name='close'
|
||||
size={24}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.56)}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
export default CloseButton;
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
|
||||
import {Tutorial} from '@constants';
|
||||
import {observeTutorialWatched} from '@queries/app/global';
|
||||
|
||||
import SkinToneSelector from './skintone_selector';
|
||||
|
||||
const enhance = withObservables([], () => ({
|
||||
tutorialWatched: observeTutorialWatched(Tutorial.EMOJI_SKIN_SELECTOR),
|
||||
}));
|
||||
|
||||
export default enhance(SkinToneSelector);
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {savePreferredSkinTone} from '@actions/remote/preference';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import TouchableEmoji from '@components/touchable_emoji';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {skinCodes} from '@utils/emoji';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
onSelectSkin: () => void;
|
||||
selected: string;
|
||||
skins: Record<string, string>;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
width: 42,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
selected: {
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
|
||||
borderRadius: 4,
|
||||
},
|
||||
skins: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
textContainer: {
|
||||
marginHorizontal: 16,
|
||||
maxWidth: 57,
|
||||
},
|
||||
text: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 75, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
const SkinSelector = ({onSelectSkin, selected, skins}: Props) => {
|
||||
const isTablet = useIsTablet();
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const handleSelectSkin = useCallback(async (emoji: string) => {
|
||||
const skin = emoji.split('hand_')[1] || 'default';
|
||||
const code = Object.keys(skinCodes).find((key) => skinCodes[key] === skin) || 'default';
|
||||
await savePreferredSkinTone(serverUrl, code);
|
||||
onSelectSkin();
|
||||
}, [serverUrl]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={[styles.textContainer, isTablet && {marginLeft: 0}]}>
|
||||
<FormattedText
|
||||
id='default_skin_tone'
|
||||
defaultMessage='Default Skin Tone'
|
||||
style={styles.text}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.skins, isTablet && {marginRight: 10}]}>
|
||||
{Object.keys(skins).map((key) => {
|
||||
const name = skins[key];
|
||||
return (
|
||||
<View
|
||||
key={name}
|
||||
style={[styles.container, selected === key && styles.selected]}
|
||||
>
|
||||
<TouchableEmoji
|
||||
name={name}
|
||||
size={28}
|
||||
onEmojiPress={handleSelectSkin}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkinSelector;
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {Platform, StyleSheet} from 'react-native';
|
||||
import Animated, {
|
||||
EntryAnimationsValues, ExitAnimationsValues, FadeIn, FadeOut,
|
||||
SharedValue, useAnimatedStyle, withDelay, withTiming,
|
||||
} from 'react-native-reanimated';
|
||||
import Tooltip from 'react-native-walkthrough-tooltip';
|
||||
|
||||
import {storeSkinEmojiSelectorTutorial} from '@actions/app/global';
|
||||
import TouchableEmoji from '@components/touchable_emoji';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {skinCodes} from '@utils/emoji';
|
||||
|
||||
import CloseButton from './close_button';
|
||||
import SkinSelector from './skin_selector';
|
||||
import SkinSelectorTooltip from './tooltip';
|
||||
|
||||
type Props = {
|
||||
containerWidth: SharedValue<number>;
|
||||
isSearching: SharedValue<boolean>;
|
||||
skinTone?: string;
|
||||
tutorialWatched: boolean;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
expanded: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
zIndex: 2,
|
||||
},
|
||||
tooltipStyle: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {width: 0, height: 2},
|
||||
shadowRadius: 2,
|
||||
shadowOpacity: 0.16,
|
||||
},
|
||||
});
|
||||
|
||||
const skins = Object.keys(skinCodes).reduce<Record<string, string>>((result, value) => {
|
||||
const skin = skinCodes[value];
|
||||
if (value === 'default') {
|
||||
result[value] = 'hand';
|
||||
} else {
|
||||
result[value] = `hand_${skin}`;
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
const SkinToneSelector = ({skinTone = 'default', containerWidth, isSearching, tutorialWatched}: Props) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||
const isTablet = useIsTablet();
|
||||
|
||||
const tooltipContentStyle = useMemo(() => ({
|
||||
borderRadius: 8,
|
||||
maxWidth: isTablet ? 352 : undefined,
|
||||
padding: 0,
|
||||
}), [isTablet]);
|
||||
|
||||
const exiting = useCallback((values: ExitAnimationsValues) => {
|
||||
'worklet';
|
||||
const animations = {
|
||||
originX: withTiming(containerWidth.value, {duration: 250}),
|
||||
opacity: withTiming(0, {duration: 250}),
|
||||
};
|
||||
const initialValues = {
|
||||
originX: values.currentOriginX,
|
||||
opacity: 1,
|
||||
};
|
||||
return {
|
||||
initialValues,
|
||||
animations,
|
||||
};
|
||||
}, [containerWidth.value]);
|
||||
|
||||
const entering = useCallback((values: EntryAnimationsValues) => {
|
||||
'worklet';
|
||||
const animations = {
|
||||
originX: withTiming(values.targetOriginX, {duration: 250}),
|
||||
opacity: withTiming(1, {duration: 300}),
|
||||
};
|
||||
const initialValues = {
|
||||
originX: containerWidth.value - 122,
|
||||
opacity: 0,
|
||||
};
|
||||
return {
|
||||
initialValues,
|
||||
animations,
|
||||
};
|
||||
}, [containerWidth.value]);
|
||||
|
||||
const collapse = useCallback(() => {
|
||||
setExpanded(false);
|
||||
}, []);
|
||||
|
||||
const expand = useCallback(() => {
|
||||
setExpanded(true);
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setTooltipVisible(false);
|
||||
storeSkinEmojiSelectorTutorial();
|
||||
}, []);
|
||||
|
||||
const widthAnimatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
width: withDelay(isSearching.value ? 0 : 700, withTiming(isSearching.value ? 0 : 32, {duration: isSearching.value ? 50 : 300})),
|
||||
marginLeft: Platform.OS === 'android' ? 10 : undefined,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const opacityStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: withDelay(isSearching.value ? 0 : 700, withTiming(isSearching.value ? 0 : 1, {duration: isSearching.value ? 50 : 350})),
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => {
|
||||
if (!tutorialWatched) {
|
||||
setTooltipVisible(true);
|
||||
}
|
||||
}, 750);
|
||||
|
||||
return () => clearTimeout(t);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!expanded &&
|
||||
<Tooltip
|
||||
isVisible={tooltipVisible}
|
||||
useInteractionManager={true}
|
||||
contentStyle={tooltipContentStyle}
|
||||
content={<SkinSelectorTooltip onClose={close}/>}
|
||||
placement={isTablet ? 'left' : 'top'}
|
||||
onClose={close}
|
||||
tooltipStyle={styles.tooltipStyle}
|
||||
>
|
||||
<Animated.View
|
||||
style={widthAnimatedStyle}
|
||||
exiting={FadeOut}
|
||||
entering={FadeIn}
|
||||
>
|
||||
<Animated.View style={[styles.container, opacityStyle]}>
|
||||
<TouchableEmoji
|
||||
name={skins[skinTone]}
|
||||
onEmojiPress={expand}
|
||||
size={28}
|
||||
/>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
</Tooltip>
|
||||
}
|
||||
{expanded &&
|
||||
<Animated.View
|
||||
style={styles.expanded}
|
||||
entering={entering}
|
||||
exiting={exiting}
|
||||
>
|
||||
{!isTablet && <CloseButton collapse={collapse}/>}
|
||||
<SkinSelector
|
||||
selected={skinTone}
|
||||
skins={skins}
|
||||
onSelectSkin={collapse}
|
||||
/>
|
||||
{isTablet && <CloseButton collapse={collapse}/>}
|
||||
</Animated.View>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkinToneSelector;
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {Preferences} from '@constants';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const hitSlop = {top: 10, bottom: 10, left: 10, right: 10};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginHorizontal: 24,
|
||||
},
|
||||
close: {
|
||||
flex: 1,
|
||||
alignItems: 'flex-end',
|
||||
marginLeft: 11,
|
||||
},
|
||||
descriptionContainer: {
|
||||
marginBottom: 24,
|
||||
marginTop: 12,
|
||||
},
|
||||
description: {
|
||||
color: Preferences.THEMES.denim.centerChannelColor,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
titleContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
marginTop: 22,
|
||||
},
|
||||
title: {
|
||||
color: Preferences.THEMES.denim.centerChannelColor,
|
||||
...typography('Body', 200, 'SemiBold'),
|
||||
},
|
||||
});
|
||||
|
||||
const SkinSelectorTooltip = ({onClose}: Props) => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.titleContainer}>
|
||||
<FormattedText
|
||||
id='skintone_selector.tooltip.title'
|
||||
defaultMessage='Choose your default skin tone'
|
||||
style={styles.title}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.close}
|
||||
hitSlop={hitSlop}
|
||||
onPress={onClose}
|
||||
>
|
||||
<CompassIcon
|
||||
color={changeOpacity(Preferences.THEMES.denim.centerChannelColor, 0.56)}
|
||||
name='close'
|
||||
size={18}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.descriptionContainer}>
|
||||
<FormattedText
|
||||
id='skintone_selector.tooltip.description'
|
||||
defaultMessage='You can now choose the skin tone you prefer to use for your emojis.'
|
||||
style={styles.description}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkinSelectorTooltip;
|
||||
20
app/screens/emoji_picker/picker/index.ts
Normal file
20
app/screens/emoji_picker/picker/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
|
||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||
import {observeConfigBooleanValue, observeRecentReactions} from '@queries/servers/system';
|
||||
|
||||
import Picker from './picker';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
customEmojisEnabled: observeConfigBooleanValue(database, 'EnableCustomEmoji'),
|
||||
customEmojis: queryAllCustomEmojis(database).observe(),
|
||||
recentEmojis: observeRecentReactions(database),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(Picker));
|
||||
@@ -1,47 +1,29 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import {LayoutChangeEvent, Platform, StyleSheet, View} from 'react-native';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import {searchCustomEmojis} from '@actions/remote/custom_emoji';
|
||||
import SearchBar from '@components/search';
|
||||
import {Preferences} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {debounce} from '@helpers/api/general';
|
||||
import {useKeyboardHeight} from '@hooks/device';
|
||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {observeConfigBooleanValue, observeRecentReactions} from '@queries/servers/system';
|
||||
import {getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||
|
||||
import EmojiFiltered from './filtered';
|
||||
import PickerHeader from './header';
|
||||
import EmojiSections from './sections';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
|
||||
|
||||
export const SCROLLVIEW_NATIVE_ID = 'emojiSelector';
|
||||
const edges: Edge[] = ['bottom', 'left', 'right'];
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
marginHorizontal: 12,
|
||||
},
|
||||
searchBar: {
|
||||
paddingVertical: 5,
|
||||
marginLeft: 12,
|
||||
marginRight: Platform.select({ios: 4, default: 12}),
|
||||
paddingBottom: 5,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -50,22 +32,21 @@ type Props = {
|
||||
customEmojisEnabled: boolean;
|
||||
onEmojiPress: (emoji: string) => void;
|
||||
recentEmojis: string[];
|
||||
skinTone: string;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const EmojiPicker = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmojis, skinTone, testID = ''}: Props) => {
|
||||
const Picker = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmojis, testID = ''}: Props) => {
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const keyboardHeight = useKeyboardHeight();
|
||||
const [width, setWidth] = useState(0);
|
||||
const [searchTerm, setSearchTerm] = useState<string|undefined>();
|
||||
const onLayout = useCallback(({nativeEvent}: LayoutChangeEvent) => setWidth(nativeEvent.layout.width), []);
|
||||
|
||||
const onCancelSearch = useCallback(() => setSearchTerm(undefined), []);
|
||||
|
||||
const onChangeSearchTerm = useCallback((text: string) => {
|
||||
setSearchTerm(text);
|
||||
searchCustom(text);
|
||||
}, []);
|
||||
|
||||
const searchCustom = debounce((text: string) => {
|
||||
if (text && text.length > 1) {
|
||||
searchCustomEmojis(serverUrl, text);
|
||||
@@ -77,8 +58,6 @@ const EmojiPicker = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmo
|
||||
EmojiList = (
|
||||
<EmojiFiltered
|
||||
customEmojis={customEmojis}
|
||||
keyboardHeight={keyboardHeight}
|
||||
skinTone={skinTone}
|
||||
searchTerm={searchTerm}
|
||||
onEmojiPress={onEmojiPress}
|
||||
/>
|
||||
@@ -90,20 +69,17 @@ const EmojiPicker = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmo
|
||||
customEmojisEnabled={customEmojisEnabled}
|
||||
onEmojiPress={onEmojiPress}
|
||||
recentEmojis={recentEmojis}
|
||||
skinTone={skinTone}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
<View
|
||||
style={styles.flex}
|
||||
edges={edges}
|
||||
testID={`${testID}.screen`}
|
||||
>
|
||||
<View style={styles.searchBar}>
|
||||
<SearchBar
|
||||
<PickerHeader
|
||||
autoCapitalize='none'
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
onCancel={onCancelSearch}
|
||||
@@ -112,28 +88,9 @@ const EmojiPicker = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmo
|
||||
value={searchTerm}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={styles.container}
|
||||
onLayout={onLayout}
|
||||
>
|
||||
{Boolean(width) &&
|
||||
<>
|
||||
{EmojiList}
|
||||
</>
|
||||
}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
{EmojiList}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
customEmojisEnabled: observeConfigBooleanValue(database, 'EnableCustomEmoji'),
|
||||
customEmojis: queryAllCustomEmojis(database).observe(),
|
||||
recentEmojis: observeRecentReactions(database),
|
||||
skinTone: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE).
|
||||
observeWithColumns(['value']).pipe(
|
||||
switchMap((prefs) => of$(prefs?.[0]?.value ?? 'default')),
|
||||
),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(EmojiPicker));
|
||||
export default Picker;
|
||||
@@ -1,31 +1,37 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BottomSheetSectionList} from '@gorhom/bottom-sheet';
|
||||
import {chunk} from 'lodash';
|
||||
import React, {useCallback, useMemo, useRef, useState} from 'react';
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {ListRenderItemInfo, NativeScrollEvent, NativeSyntheticEvent, SectionList, SectionListData, StyleSheet, View} from 'react-native';
|
||||
import sectionListGetItemLayout from 'react-native-section-list-get-item-layout';
|
||||
|
||||
import {fetchCustomEmojis} from '@actions/remote/custom_emoji';
|
||||
import TouchableEmoji from '@components/touchable_emoji';
|
||||
import {EMOJIS_PER_PAGE} from '@constants/emoji';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {setEmojiCategoryBarIcons, setEmojiCategoryBarSection, useEmojiCategoryBar} from '@hooks/emoji_category_bar';
|
||||
import {CategoryNames, EmojiIndicesByCategory, CategoryTranslations, CategoryMessage} from '@utils/emoji';
|
||||
import {fillEmoji} from '@utils/emoji/helpers';
|
||||
|
||||
import EmojiSectionBar, {SCROLLVIEW_NATIVE_ID, SectionIconType} from './icons_bar';
|
||||
import EmojiCategoryBar from '../emoji_category_bar';
|
||||
|
||||
import SectionFooter from './section_footer';
|
||||
import SectionHeader, {SECTION_HEADER_HEIGHT} from './section_header';
|
||||
import TouchableEmoji from './touchable_emoji';
|
||||
|
||||
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
|
||||
|
||||
export const EMOJI_SIZE = 30;
|
||||
export const EMOJI_GUTTER = 8;
|
||||
const EMOJI_SIZE = 34;
|
||||
const EMOJIS_PER_ROW = 7;
|
||||
const EMOJIS_PER_ROW_TABLET = 9;
|
||||
const EMOJI_ROW_MARGIN = 12;
|
||||
|
||||
const ICONS: Record<string, string> = {
|
||||
recent: 'clock-outline',
|
||||
'smileys-emotion': 'emoticon-happy-outline',
|
||||
'people-body': 'eye-outline',
|
||||
'people-body': 'account-outline',
|
||||
'animals-nature': 'leaf-outline',
|
||||
'food-drink': 'food-apple',
|
||||
'travel-places': 'airplane-variant',
|
||||
@@ -37,20 +43,27 @@ const ICONS: Record<string, string> = {
|
||||
};
|
||||
|
||||
const categoryToI18n: Record<string, CategoryTranslation> = {};
|
||||
let emojiSectionsByOffset: number[] = [];
|
||||
|
||||
const getItemLayout = sectionListGetItemLayout({
|
||||
getItemHeight: () => (EMOJI_SIZE + (EMOJI_GUTTER * 2)),
|
||||
getItemHeight: () => EMOJI_SIZE + EMOJI_ROW_MARGIN,
|
||||
getSectionHeaderHeight: () => SECTION_HEADER_HEIGHT,
|
||||
sectionOffsetsCallback: (offsetsById) => {
|
||||
emojiSectionsByOffset = offsetsById;
|
||||
},
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create(({
|
||||
flex: {flex: 1},
|
||||
contentContainerStyle: {paddingBottom: 50},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: EMOJI_GUTTER,
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: EMOJI_ROW_MARGIN,
|
||||
},
|
||||
emoji: {
|
||||
height: EMOJI_SIZE + EMOJI_GUTTER,
|
||||
marginHorizontal: 7,
|
||||
width: EMOJI_SIZE + EMOJI_GUTTER,
|
||||
height: EMOJI_SIZE,
|
||||
width: EMOJI_SIZE,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -59,8 +72,6 @@ type Props = {
|
||||
customEmojisEnabled: boolean;
|
||||
onEmojiPress: (emoji: string) => void;
|
||||
recentEmojis: string[];
|
||||
skinTone: string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
CategoryNames.forEach((name: string) => {
|
||||
@@ -73,27 +84,34 @@ CategoryNames.forEach((name: string) => {
|
||||
}
|
||||
});
|
||||
|
||||
const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmojis, skinTone, width}: Props) => {
|
||||
const emptyEmoji: EmojiAlias = {
|
||||
name: '',
|
||||
short_name: '',
|
||||
aliases: [],
|
||||
};
|
||||
|
||||
const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmojis}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const isTablet = useIsTablet();
|
||||
const {currentIndex, selectedIndex} = useEmojiCategoryBar();
|
||||
const list = useRef<SectionList<EmojiSection>>(null);
|
||||
const [sectionIndex, setSectionIndex] = useState(0);
|
||||
const categoryIndex = useRef(currentIndex);
|
||||
const [customEmojiPage, setCustomEmojiPage] = useState(0);
|
||||
const [fetchingCustomEmojis, setFetchingCustomEmojis] = useState(false);
|
||||
const [loadedAllCustomEmojis, setLoadedAllCustomEmojis] = useState(false);
|
||||
const offset = useRef(0);
|
||||
const manualScroll = useRef(false);
|
||||
|
||||
const sections: EmojiSection[] = useMemo(() => {
|
||||
if (!width) {
|
||||
return [];
|
||||
}
|
||||
const chunkSize = Math.floor(width / (EMOJI_SIZE + EMOJI_GUTTER));
|
||||
const emojisPerRow = isTablet ? EMOJIS_PER_ROW_TABLET : EMOJIS_PER_ROW;
|
||||
|
||||
return CategoryNames.map((category) => {
|
||||
const emojiIndices = EmojiIndicesByCategory.get(skinTone)?.get(category);
|
||||
const emojiIndices = EmojiIndicesByCategory.get('default')?.get(category);
|
||||
|
||||
let data: EmojiAlias[][];
|
||||
switch (category) {
|
||||
case 'custom': {
|
||||
const builtInCustom = emojiIndices.map(fillEmoji);
|
||||
const builtInCustom = emojiIndices.map(fillEmoji.bind(null, 'custom'));
|
||||
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
const custom = customEmojisEnabled ? customEmojis.map((ce) => ({
|
||||
@@ -102,7 +120,7 @@ const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentE
|
||||
short_name: '',
|
||||
})) : [];
|
||||
|
||||
data = chunk<EmojiAlias>(builtInCustom.concat(custom), chunkSize);
|
||||
data = chunk<EmojiAlias>(builtInCustom.concat(custom), emojisPerRow);
|
||||
break;
|
||||
}
|
||||
case 'recent':
|
||||
@@ -111,36 +129,34 @@ const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentE
|
||||
aliases: [],
|
||||
name: emoji,
|
||||
short_name: '',
|
||||
})), chunkSize);
|
||||
})), EMOJIS_PER_ROW);
|
||||
break;
|
||||
default:
|
||||
data = chunk(emojiIndices.map(fillEmoji), chunkSize);
|
||||
data = chunk(emojiIndices.map(fillEmoji.bind(null, category)), emojisPerRow);
|
||||
break;
|
||||
}
|
||||
|
||||
for (const d of data) {
|
||||
if (d.length < emojisPerRow) {
|
||||
d.push(
|
||||
...(new Array(emojisPerRow - d.length).fill(emptyEmoji)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...categoryToI18n[category],
|
||||
data,
|
||||
key: category,
|
||||
};
|
||||
}).filter((s: EmojiSection) => s.data.length);
|
||||
}, [skinTone, customEmojis, customEmojisEnabled, width]);
|
||||
}, [customEmojis, customEmojisEnabled, isTablet]);
|
||||
|
||||
const sectionIcons: SectionIconType[] = useMemo(() => {
|
||||
return sections.map((s) => ({
|
||||
useEffect(() => {
|
||||
setEmojiCategoryBarIcons(sections.map((s) => ({
|
||||
key: s.key,
|
||||
icon: s.icon,
|
||||
}));
|
||||
}, [sections]);
|
||||
|
||||
const emojiSectionsByOffset = useMemo(() => {
|
||||
let lastOffset = 0;
|
||||
return sections.map((s) => {
|
||||
const start = lastOffset;
|
||||
const nextOffset = s.data.length * (EMOJI_SIZE + (EMOJI_GUTTER * 2));
|
||||
lastOffset += nextOffset;
|
||||
return start;
|
||||
});
|
||||
})));
|
||||
}, [sections]);
|
||||
|
||||
const onLoadMoreCustomEmojis = useCallback(async () => {
|
||||
@@ -160,24 +176,31 @@ const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentE
|
||||
|
||||
const onScroll = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const {contentOffset} = e.nativeEvent;
|
||||
let nextIndex = emojiSectionsByOffset.findIndex(
|
||||
(offset) => contentOffset.y <= offset,
|
||||
);
|
||||
const direction = contentOffset.y > offset.current ? 'up' : 'down';
|
||||
offset.current = contentOffset.y;
|
||||
|
||||
if (nextIndex === -1) {
|
||||
nextIndex = emojiSectionsByOffset.length - 1;
|
||||
} else if (nextIndex !== 0) {
|
||||
nextIndex -= 1;
|
||||
if (manualScroll.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextIndex !== sectionIndex) {
|
||||
setSectionIndex(nextIndex);
|
||||
const nextIndex = contentOffset.y >= emojiSectionsByOffset[categoryIndex.current + 1] - SECTION_HEADER_HEIGHT ? categoryIndex.current + 1 : categoryIndex.current;
|
||||
const prevIndex = Math.max(0, contentOffset.y <= emojiSectionsByOffset[categoryIndex.current] - SECTION_HEADER_HEIGHT ? categoryIndex.current - 1 : categoryIndex.current);
|
||||
if (nextIndex > categoryIndex.current && direction === 'up') {
|
||||
categoryIndex.current = nextIndex;
|
||||
setEmojiCategoryBarSection(nextIndex);
|
||||
} else if (prevIndex < categoryIndex.current && direction === 'down') {
|
||||
categoryIndex.current = prevIndex;
|
||||
setEmojiCategoryBarSection(prevIndex);
|
||||
}
|
||||
}, [emojiSectionsByOffset, sectionIndex]);
|
||||
}, []);
|
||||
|
||||
const scrollToIndex = (index: number) => {
|
||||
manualScroll.current = true;
|
||||
list.current?.scrollToLocation({sectionIndex: index, itemIndex: 0, animated: false, viewOffset: 0});
|
||||
setSectionIndex(index);
|
||||
setEmojiCategoryBarSection(index);
|
||||
setTimeout(() => {
|
||||
manualScroll.current = false;
|
||||
}, 350);
|
||||
};
|
||||
|
||||
const renderSectionHeader = useCallback(({section}: {section: SectionListData<EmojiAlias[], EmojiSection>}) => {
|
||||
@@ -193,14 +216,22 @@ const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentE
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<EmojiAlias[]>) => {
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
{item.map((emoji: EmojiAlias) => {
|
||||
{item.map((emoji: EmojiAlias, index: number) => {
|
||||
if (!emoji.name && !emoji.short_name) {
|
||||
return (
|
||||
<View
|
||||
key={`empty-${index.toString()}`}
|
||||
style={styles.emoji}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableEmoji
|
||||
key={emoji.name}
|
||||
name={emoji.name}
|
||||
onEmojiPress={onEmojiPress}
|
||||
size={EMOJI_SIZE}
|
||||
style={styles.emoji}
|
||||
category={emoji.category}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -208,16 +239,23 @@ const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentE
|
||||
);
|
||||
}, []);
|
||||
|
||||
const List = useMemo(() => (isTablet ? SectionList : BottomSheetSectionList), [isTablet]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedIndex != null) {
|
||||
scrollToIndex(selectedIndex);
|
||||
}
|
||||
}, [selectedIndex]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionList
|
||||
<View style={styles.flex}>
|
||||
<List
|
||||
|
||||
// @ts-expect-error bottom sheet definition
|
||||
getItemLayout={getItemLayout}
|
||||
initialNumToRender={20}
|
||||
keyboardDismissMode='interactive'
|
||||
keyboardShouldPersistTaps='always'
|
||||
ListFooterComponent={renderFooter}
|
||||
maxToRenderPerBatch={20}
|
||||
nativeID={SCROLLVIEW_NATIVE_ID}
|
||||
onEndReached={onLoadMoreCustomEmojis}
|
||||
onEndReachedThreshold={2}
|
||||
onScroll={onScroll}
|
||||
@@ -225,16 +263,15 @@ const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentE
|
||||
renderItem={renderItem}
|
||||
renderSectionHeader={renderSectionHeader}
|
||||
sections={sections}
|
||||
contentContainerStyle={{paddingBottom: 50}}
|
||||
windowSize={100}
|
||||
contentContainerStyle={styles.contentContainerStyle}
|
||||
stickySectionHeadersEnabled={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
testID='emoji_picker.emoji_sections.section_list'
|
||||
/>
|
||||
<EmojiSectionBar
|
||||
currentIndex={sectionIndex}
|
||||
scrollToIndex={scrollToIndex}
|
||||
sections={sectionIcons}
|
||||
/>
|
||||
</>
|
||||
{isTablet &&
|
||||
<EmojiCategoryBar/>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {View} from 'react-native';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
section: EmojiSection;
|
||||
@@ -23,8 +24,8 @@ const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
},
|
||||
sectionTitle: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
textTransform: 'uppercase',
|
||||
...typography('Heading', 75, 'SemiBold'),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -122,7 +122,7 @@ const Servers = React.forwardRef<ServersRef>((_, ref) => {
|
||||
bottomSheetSnapPoint(Math.min(2.5, registeredServers.current.length), 72, bottom) + TITLE_HEIGHT + BUTTON_HEIGHT,
|
||||
];
|
||||
if (registeredServers.current.length > 1) {
|
||||
snapPoints.push('90%');
|
||||
snapPoints.push('80%');
|
||||
}
|
||||
|
||||
const closeButtonId = 'close-your-servers';
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
|
||||
import {Tutorial} from '@constants';
|
||||
import {PUSH_PROXY_STATUS_UNKNOWN} from '@constants/push_proxy';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {observeMultiServerTutorial} from '@queries/app/global';
|
||||
import {observeTutorialWatched} from '@queries/app/global';
|
||||
import {observePushVerificationStatus} from '@queries/servers/system';
|
||||
|
||||
import ServerItem from './server_item';
|
||||
@@ -16,7 +17,7 @@ import type ServersModel from '@typings/database/models/app/servers';
|
||||
const enhance = withObservables(['highlight'], ({highlight, server}: {highlight: boolean; server: ServersModel}) => {
|
||||
let tutorialWatched = of$(false);
|
||||
if (highlight) {
|
||||
tutorialWatched = observeMultiServerTutorial();
|
||||
tutorialWatched = observeTutorialWatched(Tutorial.MULTI_SERVER);
|
||||
}
|
||||
|
||||
const serverDatabase = DatabaseManager.serverDatabases[server.url]?.database;
|
||||
|
||||
@@ -82,7 +82,7 @@ const TeamPickerIcon = ({size = 24, divider = false, setTeamId, teams, teamId}:
|
||||
];
|
||||
|
||||
if (teams.length > 3) {
|
||||
snapPoints.push('90%');
|
||||
snapPoints.push('80%');
|
||||
}
|
||||
|
||||
bottomSheet({
|
||||
|
||||
@@ -84,7 +84,7 @@ const PostOptions = ({
|
||||
items.push(bottomSheetSnapPoint(optionsCount, ITEM_HEIGHT, bottom) + (canAddReaction ? REACTION_PICKER_HEIGHT + REACTION_PICKER_MARGIN : 0));
|
||||
|
||||
if (shouldShowBindings) {
|
||||
items.push('90%');
|
||||
items.push('80%');
|
||||
}
|
||||
|
||||
return items;
|
||||
@@ -103,7 +103,7 @@ const PostOptions = ({
|
||||
postId={post.id}
|
||||
/>
|
||||
}
|
||||
{canReply && sourceScreen !== Screens.THREAD &&
|
||||
{canReply &&
|
||||
<ReplyOption
|
||||
bottomSheetId={Screens.POST_OPTIONS}
|
||||
post={post}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {useIntl} from 'react-intl';
|
||||
import {useWindowDimensions, View} from 'react-native';
|
||||
|
||||
import {addReaction} from '@actions/remote/reactions';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Screens} from '@constants';
|
||||
import {
|
||||
LARGE_CONTAINER_SIZE,
|
||||
@@ -19,7 +18,7 @@ import {
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {dismissBottomSheet, showModal} from '@screens/navigation';
|
||||
import {dismissBottomSheet, openAsBottomSheet} from '@screens/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import PickReaction from './pick_reaction';
|
||||
@@ -61,14 +60,14 @@ const ReactionBar = ({bottomSheetId, recentEmojis = [], postId}: QuickReactionPr
|
||||
|
||||
const openEmojiPicker = useCallback(async () => {
|
||||
await dismissBottomSheet(bottomSheetId);
|
||||
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
const screen = Screens.EMOJI_PICKER;
|
||||
const title = intl.formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'});
|
||||
const passProps = {closeButton, onEmojiPress: handleEmojiPress};
|
||||
|
||||
showModal(screen, title, passProps);
|
||||
}, [bottomSheetId, intl, theme]);
|
||||
openAsBottomSheet({
|
||||
closeButtonId: 'close-add-reaction',
|
||||
screen: Screens.EMOJI_PICKER,
|
||||
theme,
|
||||
title: intl.formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
|
||||
props: {onEmojiPress: handleEmojiPress},
|
||||
});
|
||||
}, [handleEmojiPress, intl, theme]);
|
||||
|
||||
let containerSize = LARGE_CONTAINER_SIZE;
|
||||
let iconSize = LARGE_ICON_SIZE;
|
||||
|
||||
@@ -85,7 +85,7 @@ const Reactions = ({initialEmoji, location, reactions}: Props) => {
|
||||
closeButtonId='close-post-reactions'
|
||||
componentId={Screens.REACTIONS}
|
||||
initialSnapIndex={1}
|
||||
snapPoints={[1, '50%', '90%']}
|
||||
snapPoints={[1, '50%', '80%']}
|
||||
testID='reactions'
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -108,7 +108,7 @@ export function getEmojiName(emoji: string, customEmojiNames: string[]) {
|
||||
if (matchUnicodeEmoji) {
|
||||
const index = EmojiIndicesByUnicode.get(matchUnicodeEmoji[0]);
|
||||
if (index != null) {
|
||||
return fillEmoji(Emojis[index]).name;
|
||||
return fillEmoji('', Emojis[index]).name;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -310,11 +310,12 @@ export const isCustomEmojiEnabled = (config: ClientConfig | SystemModel) => {
|
||||
return config?.EnableCustomEmoji === 'true';
|
||||
};
|
||||
|
||||
export function fillEmoji(index: number) {
|
||||
export function fillEmoji(category: string, index: number) {
|
||||
const emoji = Emojis[index];
|
||||
return {
|
||||
name: 'short_name' in emoji ? emoji.short_name : emoji.name,
|
||||
aliases: 'short_names' in emoji ? emoji.short_names : [],
|
||||
category,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -231,6 +231,7 @@
|
||||
"custom_status.suggestions.working_from_home": "Working from home",
|
||||
"date_separator.today": "Today",
|
||||
"date_separator.yesterday": "Yesterday",
|
||||
"default_skin_tone": "Default Skin Tone",
|
||||
"display_settings.clock.military": "24-hour",
|
||||
"display_settings.clock.standard": "12-hour",
|
||||
"display_settings.clockDisplay": "Clock Display",
|
||||
@@ -851,6 +852,8 @@
|
||||
"share_feedback.button.yes": "Yes",
|
||||
"share_feedback.subtitle": "We'd love to hear how we can make your experience better.",
|
||||
"share_feedback.title": "Would you share your feedback?",
|
||||
"skintone_selector.tooltip.description": "You can now choose the skin tone you prefer to use for your emojis.",
|
||||
"skintone_selector.tooltip.title": "Choose your default skin tone",
|
||||
"smobile.search.recent_title": "Recent searches in {teamName}",
|
||||
"snack.bar.favorited.channel": "This channel was favorited",
|
||||
"snack.bar.link.copied": "Link copied to clipboard",
|
||||
|
||||
32
package-lock.json
generated
32
package-lock.json
generated
@@ -91,6 +91,7 @@
|
||||
"react-native-svg": "13.6.0",
|
||||
"react-native-vector-icons": "9.2.0",
|
||||
"react-native-video": "5.2.1",
|
||||
"react-native-walkthrough-tooltip": "1.4.0",
|
||||
"react-native-webrtc": "github:mattermost/react-native-webrtc",
|
||||
"react-native-webview": "11.26.0",
|
||||
"react-syntax-highlighter": "15.5.0",
|
||||
@@ -18170,6 +18171,11 @@
|
||||
"react": ">=16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
||||
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
|
||||
},
|
||||
"node_modules/react-freeze": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.3.tgz",
|
||||
@@ -18853,6 +18859,18 @@
|
||||
"shaka-player": "^2.5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-walkthrough-tooltip": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-walkthrough-tooltip/-/react-native-walkthrough-tooltip-1.4.0.tgz",
|
||||
"integrity": "sha512-nyBuynmCuzPKJN9NRGhSzCutGOk7/WqszGtX01gjDtlRfoJ25cSM0h+TEi/lsLbSCTEdB+e65qOTvoVhIq2gzA==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.6.1",
|
||||
"react-fast-compare": "^2.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8.24 <18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-webrtc": {
|
||||
"version": "1.75.3",
|
||||
"resolved": "git+ssh://git@github.com/mattermost/react-native-webrtc.git#7f765758f2f67e467ebd224c1c4d00a475e400d3",
|
||||
@@ -35647,6 +35665,11 @@
|
||||
"@babel/runtime": "^7.12.5"
|
||||
}
|
||||
},
|
||||
"react-fast-compare": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
||||
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
|
||||
},
|
||||
"react-freeze": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.3.tgz",
|
||||
@@ -36239,6 +36262,15 @@
|
||||
"shaka-player": "^2.5.9"
|
||||
}
|
||||
},
|
||||
"react-native-walkthrough-tooltip": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-walkthrough-tooltip/-/react-native-walkthrough-tooltip-1.4.0.tgz",
|
||||
"integrity": "sha512-nyBuynmCuzPKJN9NRGhSzCutGOk7/WqszGtX01gjDtlRfoJ25cSM0h+TEi/lsLbSCTEdB+e65qOTvoVhIq2gzA==",
|
||||
"requires": {
|
||||
"prop-types": "^15.6.1",
|
||||
"react-fast-compare": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"react-native-webrtc": {
|
||||
"version": "git+ssh://git@github.com/mattermost/react-native-webrtc.git#7f765758f2f67e467ebd224c1c4d00a475e400d3",
|
||||
"from": "react-native-webrtc@github:mattermost/react-native-webrtc",
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
"react-native-svg": "13.6.0",
|
||||
"react-native-vector-icons": "9.2.0",
|
||||
"react-native-video": "5.2.1",
|
||||
"react-native-walkthrough-tooltip": "1.4.0",
|
||||
"react-native-webrtc": "github:mattermost/react-native-webrtc",
|
||||
"react-native-webview": "11.26.0",
|
||||
"react-syntax-highlighter": "15.5.0",
|
||||
@@ -228,6 +229,9 @@
|
||||
},
|
||||
"detox": {
|
||||
"jest": "^29.1.0"
|
||||
},
|
||||
"react-native-walkthrough-tooltip": {
|
||||
"@types/react": "^18.0.26"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
diff --git a/node_modules/react-native-section-list-get-item-layout/dist/index.d.ts b/node_modules/react-native-section-list-get-item-layout/dist/index.d.ts
|
||||
index 8f066b8..6fc9768 100644
|
||||
--- a/node_modules/react-native-section-list-get-item-layout/dist/index.d.ts
|
||||
+++ b/node_modules/react-native-section-list-get-item-layout/dist/index.d.ts
|
||||
@@ -8,8 +8,9 @@ export interface Parameters {
|
||||
getSectionHeaderHeight?: (sectionIndex: number) => number;
|
||||
getSectionFooterHeight?: (sectionIndex: number) => number;
|
||||
listHeaderHeight?: number | (() => number);
|
||||
+ sectionOffsetsCallback?: (sectionOffsets: number[]) => void;
|
||||
}
|
||||
-declare const _default: ({ getItemHeight, getSeparatorHeight, getSectionHeaderHeight, getSectionFooterHeight, listHeaderHeight, }: Parameters) => (data: {
|
||||
+declare const _default: ({ getItemHeight, getSeparatorHeight, getSectionHeaderHeight, getSectionFooterHeight, listHeaderHeight, sectionOffsetsCallback }: Parameters) => (data: {
|
||||
title: string;
|
||||
data: any[];
|
||||
}[], index: number) => {
|
||||
diff --git a/node_modules/react-native-section-list-get-item-layout/dist/index.js b/node_modules/react-native-section-list-get-item-layout/dist/index.js
|
||||
index e7f6635..bf3da2f 100644
|
||||
--- a/node_modules/react-native-section-list-get-item-layout/dist/index.js
|
||||
+++ b/node_modules/react-native-section-list-get-item-layout/dist/index.js
|
||||
@@ -2,10 +2,12 @@
|
||||
exports.__esModule = true;
|
||||
exports["default"] = (function (_a) {
|
||||
var getItemHeight = _a.getItemHeight, _b = _a.getSeparatorHeight, getSeparatorHeight = _b === void 0 ? function () { return 0; } : _b, _c = _a.getSectionHeaderHeight, getSectionHeaderHeight = _c === void 0 ? function () { return 0; } : _c, _d = _a.getSectionFooterHeight, getSectionFooterHeight = _d === void 0 ? function () { return 0; } : _d, _e = _a.listHeaderHeight, listHeaderHeight = _e === void 0 ? 0 : _e;
|
||||
+ var callback = _a.sectionOffsetsCallback;
|
||||
return function (data, index) {
|
||||
var i = 0;
|
||||
var sectionIndex = 0;
|
||||
var elementPointer = { type: 'SECTION_HEADER' };
|
||||
+ var offsetById = [];
|
||||
var offset = typeof listHeaderHeight === 'function'
|
||||
? listHeaderHeight()
|
||||
: listHeaderHeight;
|
||||
@@ -15,6 +17,7 @@ exports["default"] = (function (_a) {
|
||||
var sectionData = data[sectionIndex].data;
|
||||
offset += getSectionHeaderHeight(sectionIndex);
|
||||
// If this section is empty, we go right to the footer...
|
||||
+ offsetById[sectionIndex] = offset;
|
||||
if (sectionData.length === 0) {
|
||||
elementPointer = { type: 'SECTION_FOOTER' };
|
||||
// ...otherwise we make elementPointer point at the first row in this section
|
||||
@@ -61,6 +64,9 @@ exports["default"] = (function (_a) {
|
||||
default:
|
||||
throw new Error('Unknown elementPointer.type');
|
||||
}
|
||||
+ if (callback) {
|
||||
+ callback(offsetById);
|
||||
+ }
|
||||
return { length: length, offset: offset, index: index };
|
||||
};
|
||||
});
|
||||
38
patches/react-native-walkthrough-tooltip+1.4.0.patch
Normal file
38
patches/react-native-walkthrough-tooltip+1.4.0.patch
Normal file
@@ -0,0 +1,38 @@
|
||||
diff --git a/node_modules/react-native-walkthrough-tooltip/src/tooltip.d.ts b/node_modules/react-native-walkthrough-tooltip/src/tooltip.d.ts
|
||||
index 5a7ef59..305d794 100644
|
||||
--- a/node_modules/react-native-walkthrough-tooltip/src/tooltip.d.ts
|
||||
+++ b/node_modules/react-native-walkthrough-tooltip/src/tooltip.d.ts
|
||||
@@ -129,6 +129,8 @@ declare module 'react-native-walkthrough-tooltip' {
|
||||
|
||||
/** Will use given component instead of default react-native Modal component **/
|
||||
modalComponent?: object;
|
||||
+
|
||||
+ children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
diff --git a/node_modules/react-native-walkthrough-tooltip/src/tooltip.js b/node_modules/react-native-walkthrough-tooltip/src/tooltip.js
|
||||
index db612fd..0262d6d 100644
|
||||
--- a/node_modules/react-native-walkthrough-tooltip/src/tooltip.js
|
||||
+++ b/node_modules/react-native-walkthrough-tooltip/src/tooltip.js
|
||||
@@ -212,7 +212,7 @@ class Tooltip extends Component {
|
||||
this.setState(
|
||||
{
|
||||
windowDims: dims.window,
|
||||
- contentSize: new Size(0, 0),
|
||||
+ // contentSize: new Size(0, 0),
|
||||
adjustedContentSize: new Size(0, 0),
|
||||
anchorPoint: new Point(0, 0),
|
||||
tooltipOrigin: new Point(0, 0),
|
||||
@@ -268,9 +268,9 @@ class Tooltip extends Component {
|
||||
this.childWrapper.current &&
|
||||
typeof this.childWrapper.current.measure === 'function'
|
||||
) {
|
||||
- this.childWrapper.current.measure(
|
||||
+ this.childWrapper.current.measureInWindow(
|
||||
(x, y, width, height, pageX, pageY) => {
|
||||
- const childRect = new Rect(pageX, pageY, width, height);
|
||||
+ const childRect = new Rect(x, y, width, height);
|
||||
if (
|
||||
Object.values(childRect).every(value => value !== undefined)
|
||||
) {
|
||||
1
types/screens/emoji_selector.d.ts
vendored
1
types/screens/emoji_selector.d.ts
vendored
@@ -5,6 +5,7 @@ type EmojiAlias = {
|
||||
aliases: string [];
|
||||
name: string;
|
||||
short_name: string;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
type EmojiSection = {
|
||||
|
||||
Reference in New Issue
Block a user