diff --git a/app/actions/app/global.ts b/app/actions/app/global.ts
index e0c5840c30..370231841d 100644
--- a/app/actions/app/global.ts
+++ b/app/actions/app/global.ts
@@ -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) => {
diff --git a/app/actions/remote/preference.ts b/app/actions/remote/preference.ts
index 8d7c79f818..3180e476a5 100644
--- a/app/actions/remote/preference.ts
+++ b/app/actions/remote/preference.ts
@@ -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};
+ }
+};
diff --git a/app/components/emoji_picker/sections/icons_bar/index.tsx b/app/components/emoji_picker/sections/icons_bar/index.tsx
deleted file mode 100644
index 71f008fefd..0000000000
--- a/app/components/emoji_picker/sections/icons_bar/index.tsx
+++ /dev/null
@@ -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 (
-
-
-
- {sections.map((section, index) => (
-
- ))}
-
-
-
- );
-};
-
-export default EmojiSectionBar;
diff --git a/app/components/emoji_picker/sections/touchable_emoji.tsx b/app/components/emoji_picker/sections/touchable_emoji.tsx
deleted file mode 100644
index 8be3899de2..0000000000
--- a/app/components/emoji_picker/sections/touchable_emoji.tsx
+++ /dev/null
@@ -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;
-}
-
-const TouchableEmoji = ({name, onEmojiPress, size = 30, style}: Props) => {
- const onPress = useCallback(preventDoubleTap(() => onEmojiPress(name)), []);
-
- return (
-
-
-
- );
-};
-
-export default React.memo(TouchableEmoji);
diff --git a/app/components/post_list/post/body/reactions/reactions.tsx b/app/components/post_list/post/body/reactions/reactions.tsx
index 420bafe013..8bbd1a55c0 100644
--- a/app/components/post_list/post/body/reactions/reactions.tsx
+++ b/app/components/post_list/post/body/reactions/reactions.tsx
@@ -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();
diff --git a/app/components/post_list/post/post.tsx b/app/components/post_list/post/post.tsx
index 4186340ba4..f6fb599be9 100644
--- a/app/components/post_list/post/post.tsx
+++ b/app/components/post_list/post/post.tsx
@@ -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);
diff --git a/app/components/touchable_emoji/index.tsx b/app/components/touchable_emoji/index.tsx
new file mode 100644
index 0000000000..e892bc2003
--- /dev/null
+++ b/app/components/touchable_emoji/index.tsx
@@ -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;
+}
+
+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 = (
+
+ );
+ } else {
+ emoji = (
+
+ );
+ }
+
+ return (
+
+
+ {emoji}
+
+
+ );
+};
+
+export default React.memo(TouchableEmoji);
diff --git a/app/components/touchable_emoji/skinned_emoji.tsx b/app/components/touchable_emoji/skinned_emoji.tsx
new file mode 100644
index 0000000000..0fdedda8bd
--- /dev/null
+++ b/app/components/touchable_emoji/skinned_emoji.tsx
@@ -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 (
+
+ );
+};
+
+export default React.memo(SkinnedEmoji);
diff --git a/app/components/user_avatars_stack/index.tsx b/app/components/user_avatars_stack/index.tsx
index d6c096aec1..1e084d8546 100644
--- a/app/components/user_avatars_stack/index.tsx
+++ b/app/components/user_avatars_stack/index.tsx
@@ -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({
diff --git a/app/constants/database.ts b/app/constants/database.ts
index 67bab033ca..000c7d8384 100644
--- a/app/constants/database.ts
+++ b/app/constants/database.ts
@@ -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',
};
diff --git a/app/constants/index.ts b/app/constants/index.ts
index bd7d174354..8483e6d24e 100644
--- a/app/constants/index.ts
+++ b/app/constants/index.ts
@@ -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,
};
diff --git a/app/constants/tutorial.ts b/app/constants/tutorial.ts
new file mode 100644
index 0000000000..c830f519ad
--- /dev/null
+++ b/app/constants/tutorial.ts
@@ -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,
+};
diff --git a/app/hooks/emoji_category_bar.ts b/app/hooks/emoji_category_bar.ts
new file mode 100644
index 0000000000..f969732a38
--- /dev/null
+++ b/app/hooks/emoji_category_bar.ts
@@ -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 = 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;
+};
diff --git a/app/queries/app/global.ts b/app/queries/app/global.ts
index f362d09993..311d9b3205 100644
--- a/app/queries/app/global.ts
+++ b/app/queries/app/global.ts
@@ -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 => {
try {
const {database} = DatabaseManager.getAppDatabaseAndOperator();
@@ -52,17 +41,6 @@ export const getOnboardingViewed = async (): Promise => {
}
};
-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))),
+ );
+};
diff --git a/app/screens/bottom_sheet/index.tsx b/app/screens/bottom_sheet/index.tsx
index 260acd2bcb..1a64f68309 100644
--- a/app/screens/bottom_sheet/index.tsx
+++ b/app/screens/bottom_sheet/index.tsx
@@ -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;
initialSnapIndex?: number;
footerComponent?: React.FC;
renderContent: () => ReactNode;
@@ -80,16 +81,18 @@ export const animatedConfig: Omit = {
const BottomSheet = ({
closeButtonId,
componentId,
+ contentStyle,
initialSnapIndex = 1,
footerComponent,
renderContent,
- snapPoints = [1, '50%', '90%'],
+ snapPoints = [1, '50%', '80%'],
testID,
}: Props) => {
const sheetRef = useRef(null);
const isTablet = useIsTablet();
const theme = useTheme();
const styles = getStyleSheet(theme);
+ const interaction = useRef();
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 = () => (
{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()}
diff --git a/app/screens/create_direct_message/index.ts b/app/screens/create_direct_message/index.ts
index 8403c44540..fbc6836359 100644
--- a/app/screens/create_direct_message/index.ts
+++ b/app/screens/create_direct_message/index.ts
@@ -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,
};
});
diff --git a/app/screens/custom_status/custom_status.tsx b/app/screens/custom_status/custom_status.tsx
index d5f1c55b35..367f587c2c 100644
--- a/app/screens/custom_status/custom_status.tsx
+++ b/app/screens/custom_status/custom_status.tsx
@@ -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]);
diff --git a/app/screens/emoji_picker/index.tsx b/app/screens/emoji_picker/index.tsx
index 612ddd8a7c..002c524ea6 100644
--- a/app/screens/emoji_picker/index.tsx
+++ b/app/screens/emoji_picker/index.tsx
@@ -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 (
+
+ );
}, []);
return (
-
);
};
diff --git a/app/components/emoji_picker/sections/icons_bar/icon.tsx b/app/screens/emoji_picker/picker/emoji_category_bar/icon.tsx
similarity index 70%
rename from app/components/emoji_picker/sections/icons_bar/icon.tsx
rename to app/screens/emoji_picker/picker/emoji_category_bar/icon.tsx
index c2191ffc79..bd1d94a9d8 100644
--- a/app/components/emoji_picker/sections/icons_bar/icon.tsx
+++ b/app/screens/emoji_picker/picker/emoji_category_bar/icon.tsx
@@ -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 (
({
+ 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 (
+
+ {icons.map((icon, index) => (
+
+ ))}
+
+ );
+};
+
+export default EmojiCategoryBar;
diff --git a/app/components/emoji_picker/filtered/emoji_item.tsx b/app/screens/emoji_picker/picker/filtered/emoji_item.tsx
similarity index 100%
rename from app/components/emoji_picker/filtered/emoji_item.tsx
rename to app/screens/emoji_picker/picker/filtered/emoji_item.tsx
diff --git a/app/components/emoji_picker/filtered/index.tsx b/app/screens/emoji_picker/picker/filtered/filtered.tsx
similarity index 87%
rename from app/components/emoji_picker/filtered/index.tsx
rename to app/screens/emoji_picker/picker/filtered/filtered.tsx
index 50ecf9d15a..4d12a25bc1 100644
--- a/app/components/emoji_picker/filtered/index.tsx
+++ b/app/screens/emoji_picker/picker/filtered/filtered.tsx
@@ -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 (
- ({
+ skinTone: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE).
+ observeWithColumns(['value']).pipe(
+ switchMap((prefs) => of$(prefs?.[0]?.value ?? 'default')),
+ ),
+}));
+
+export default withDatabase(enhanced(EmojiFiltered));
diff --git a/app/screens/emoji_picker/picker/footer/index.tsx b/app/screens/emoji_picker/picker/footer/index.tsx
new file mode 100644
index 0000000000..95130ab318
--- /dev/null
+++ b/app/screens/emoji_picker/picker/footer/index.tsx
@@ -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 (
+
+
+
+
+
+ );
+};
+
+export default PickerFooter;
diff --git a/app/screens/emoji_picker/picker/header/bottom_sheet_search.tsx b/app/screens/emoji_picker/picker/header/bottom_sheet_search.tsx
new file mode 100644
index 0000000000..4710997052
--- /dev/null
+++ b/app/screens/emoji_picker/picker/header/bottom_sheet_search.tsx
@@ -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) => {
+ expand();
+ onFocus?.(event);
+ }, [onFocus, expand]);
+
+ return (
+
+ );
+};
+
+export default BottomSheetSearch;
diff --git a/app/screens/emoji_picker/picker/header/header.tsx b/app/screens/emoji_picker/picker/header/header.tsx
new file mode 100644
index 0000000000..8d5e02e20d
--- /dev/null
+++ b/app/screens/emoji_picker/picker/header/header.tsx
@@ -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 = (
+
+ );
+ } else {
+ search = (
+
+ );
+ }
+
+ return (
+
+
+ {search}
+
+
+
+ );
+};
+
+export default PickerHeader;
diff --git a/app/screens/emoji_picker/picker/header/index.ts b/app/screens/emoji_picker/picker/header/index.ts
new file mode 100644
index 0000000000..51b8c3ceff
--- /dev/null
+++ b/app/screens/emoji_picker/picker/header/index.ts
@@ -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));
diff --git a/app/screens/emoji_picker/picker/header/skintone_selector/close_button.tsx b/app/screens/emoji_picker/picker/header/skintone_selector/close_button.tsx
new file mode 100644
index 0000000000..6dcf9fee88
--- /dev/null
+++ b/app/screens/emoji_picker/picker/header/skintone_selector/close_button.tsx
@@ -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 (
+
+
+
+ );
+};
+
+export default CloseButton;
diff --git a/app/screens/emoji_picker/picker/header/skintone_selector/index.ts b/app/screens/emoji_picker/picker/header/skintone_selector/index.ts
new file mode 100644
index 0000000000..9db386bdff
--- /dev/null
+++ b/app/screens/emoji_picker/picker/header/skintone_selector/index.ts
@@ -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);
diff --git a/app/screens/emoji_picker/picker/header/skintone_selector/skin_selector.tsx b/app/screens/emoji_picker/picker/header/skintone_selector/skin_selector.tsx
new file mode 100644
index 0000000000..41bf06fd42
--- /dev/null
+++ b/app/screens/emoji_picker/picker/header/skintone_selector/skin_selector.tsx
@@ -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;
+}
+
+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 (
+ <>
+
+
+
+
+ {Object.keys(skins).map((key) => {
+ const name = skins[key];
+ return (
+
+
+
+ );
+ })}
+
+ >
+ );
+};
+
+export default SkinSelector;
diff --git a/app/screens/emoji_picker/picker/header/skintone_selector/skintone_selector.tsx b/app/screens/emoji_picker/picker/header/skintone_selector/skintone_selector.tsx
new file mode 100644
index 0000000000..5f6ffdbe68
--- /dev/null
+++ b/app/screens/emoji_picker/picker/header/skintone_selector/skintone_selector.tsx
@@ -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;
+ isSearching: SharedValue;
+ 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>((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 &&
+ }
+ placement={isTablet ? 'left' : 'top'}
+ onClose={close}
+ tooltipStyle={styles.tooltipStyle}
+ >
+
+
+
+
+
+
+ }
+ {expanded &&
+
+ {!isTablet && }
+
+ {isTablet && }
+
+ }
+ >
+ );
+};
+
+export default SkinToneSelector;
diff --git a/app/screens/emoji_picker/picker/header/skintone_selector/tooltip.tsx b/app/screens/emoji_picker/picker/header/skintone_selector/tooltip.tsx
new file mode 100644
index 0000000000..d89e7d33ca
--- /dev/null
+++ b/app/screens/emoji_picker/picker/header/skintone_selector/tooltip.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default SkinSelectorTooltip;
diff --git a/app/screens/emoji_picker/picker/index.ts b/app/screens/emoji_picker/picker/index.ts
new file mode 100644
index 0000000000..3c50d8443d
--- /dev/null
+++ b/app/screens/emoji_picker/picker/index.ts
@@ -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));
diff --git a/app/components/emoji_picker/index.tsx b/app/screens/emoji_picker/picker/picker.tsx
similarity index 50%
rename from app/components/emoji_picker/index.tsx
rename to app/screens/emoji_picker/picker/picker.tsx
index d2d18a4c7e..cea2250358 100644
--- a/app/components/emoji_picker/index.tsx
+++ b/app/screens/emoji_picker/picker/picker.tsx
@@ -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();
- 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 = (
@@ -90,20 +69,17 @@ const EmojiPicker = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmo
customEmojisEnabled={customEmojisEnabled}
onEmojiPress={onEmojiPress}
recentEmojis={recentEmojis}
- skinTone={skinTone}
- width={width}
/>
);
}
return (
-
-
-
- {Boolean(width) &&
- <>
- {EmojiList}
- >
- }
-
-
+ {EmojiList}
+
);
};
-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;
diff --git a/app/components/emoji_picker/sections/index.tsx b/app/screens/emoji_picker/picker/sections/index.tsx
similarity index 59%
rename from app/components/emoji_picker/sections/index.tsx
rename to app/screens/emoji_picker/picker/sections/index.tsx
index 396852a5f3..d4bebf5fef 100644
--- a/app/components/emoji_picker/sections/index.tsx
+++ b/app/screens/emoji_picker/picker/sections/index.tsx
@@ -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 = {
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 = {
};
const categoryToI18n: Record = {};
+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>(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(builtInCustom.concat(custom), chunkSize);
+ data = chunk(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) => {
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}) => {
@@ -193,14 +216,22 @@ const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentE
const renderItem = useCallback(({item}: ListRenderItemInfo) => {
return (
- {item.map((emoji: EmojiAlias) => {
+ {item.map((emoji: EmojiAlias, index: number) => {
+ if (!emoji.name && !emoji.short_name) {
+ return (
+
+ );
+ }
+
return (
);
})}
@@ -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 (
- <>
-
+
-
- >
+ {isTablet &&
+
+ }
+
);
};
diff --git a/app/components/emoji_picker/sections/section_footer.tsx b/app/screens/emoji_picker/picker/sections/section_footer.tsx
similarity index 100%
rename from app/components/emoji_picker/sections/section_footer.tsx
rename to app/screens/emoji_picker/picker/sections/section_footer.tsx
diff --git a/app/components/emoji_picker/sections/section_header.tsx b/app/screens/emoji_picker/picker/sections/section_header.tsx
similarity index 90%
rename from app/components/emoji_picker/sections/section_header.tsx
rename to app/screens/emoji_picker/picker/sections/section_header.tsx
index 2239809585..85ce128d53 100644
--- a/app/components/emoji_picker/sections/section_header.tsx
+++ b/app/screens/emoji_picker/picker/sections/section_header.tsx
@@ -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'),
},
};
});
diff --git a/app/screens/home/channel_list/servers/index.tsx b/app/screens/home/channel_list/servers/index.tsx
index 4675f6db9a..d6dff531fc 100644
--- a/app/screens/home/channel_list/servers/index.tsx
+++ b/app/screens/home/channel_list/servers/index.tsx
@@ -122,7 +122,7 @@ const Servers = React.forwardRef((_, 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';
diff --git a/app/screens/home/channel_list/servers/servers_list/server_item/index.ts b/app/screens/home/channel_list/servers/servers_list/server_item/index.ts
index 7c7583302f..a46828e719 100644
--- a/app/screens/home/channel_list/servers/servers_list/server_item/index.ts
+++ b/app/screens/home/channel_list/servers/servers_list/server_item/index.ts
@@ -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;
diff --git a/app/screens/home/search/team_picker_icon.tsx b/app/screens/home/search/team_picker_icon.tsx
index 19c6cdd7a1..9a9fa52e9e 100644
--- a/app/screens/home/search/team_picker_icon.tsx
+++ b/app/screens/home/search/team_picker_icon.tsx
@@ -82,7 +82,7 @@ const TeamPickerIcon = ({size = 24, divider = false, setTeamId, teams, teamId}:
];
if (teams.length > 3) {
- snapPoints.push('90%');
+ snapPoints.push('80%');
}
bottomSheet({
diff --git a/app/screens/post_options/post_options.tsx b/app/screens/post_options/post_options.tsx
index 38e316ac85..bc4572a1d9 100644
--- a/app/screens/post_options/post_options.tsx
+++ b/app/screens/post_options/post_options.tsx
@@ -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 &&
{
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;
diff --git a/app/screens/reactions/reactions.tsx b/app/screens/reactions/reactions.tsx
index 00798c62d1..630ff9c0f9 100644
--- a/app/screens/reactions/reactions.tsx
+++ b/app/screens/reactions/reactions.tsx
@@ -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'
/>
);
diff --git a/app/utils/emoji/helpers.ts b/app/utils/emoji/helpers.ts
index 60f7302b6c..148d82e6b6 100644
--- a/app/utils/emoji/helpers.ts
+++ b/app/utils/emoji/helpers.ts
@@ -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,
};
}
diff --git a/app/utils/emoji/index.ts b/app/utils/emoji/index.ts
index fb9d7481a3..73d3be1dfb 100644
--- a/app/utils/emoji/index.ts
+++ b/app/utils/emoji/index.ts
@@ -29,7 +29,7 @@ export const AllEmojiIndicesByCategory = new Map([["smileys-emotion",[0,1,2,3,4,
export const EmojiIndicesByCategoryAndSkin = new Map([['default', new Map([["people-body",[151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,181,182,183,184,185,188,189,190,191,192,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,401,402,403,404,405,406,408,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458]]])], ['1F3FB', new Map([["people-body",[1810,1815,1820,1825,1830,1835,1840,1845,1850,1855,1860,1865,1870,1875,1880,1885,1890,1895,1900,1905,1910,1915,1920,1925,1930,1935,1940,1945,1950,1955,1960,1965,1970,1975,1980,1985,1990,1995,2000,2005,2010,2015,2020,2025,2030,2035,2040,2045,2050,2055,2060,2065,2070,2075,2080,2085,2090,2095,2100,2105,2110,2115,2120,2125,2130,2135,2140,2145,2150,2155,2160,2165,2170,2175,2180,2185,2190,2195,2200,2205,2210,2215,2220,2225,2230,2231,2232,2233,2250,2255,2256,2257,2258,2275,2280,2281,2282,2283,2300,2305,2310,2315,2320,2325,2330,2335,2340,2345,2350,2355,2360,2365,2370,2375,2380,2385,2390,2395,2400,2405,2410,2415,2420,2425,2430,2435,2440,2445,2450,2455,2460,2465,2470,2475,2480,2485,2490,2495,2500,2505,2510,2515,2520,2525,2530,2535,2540,2545,2550,2555,2560,2565,2570,2575,2580,2585,2590,2595,2600,2605,2610,2615,2620,2625,2630,2635,2640,2645,2650,2655,2660,2665,2670,2675,2680,2685,2690,2695,2700,2705,2710,2715,2720,2725,2730,2735,2740,2745,2750,2755,2760,2765,2770,2775,2780,2785,2790,2795,2800,2805,2810,2815,2820,2825,2830,2835,2840,2845,2850,2855,2860,2865,2870,2875,2880,2885,2890,2895,2900,2905,2910,2915,2920,2925,2930,2935,2940,2945,2950,2955,2960,2965,2970,2975,2980,2985,2990,2995,3000,3005,3010,3015,3020,3025,3030,3035,3040,3041,3042,3043,3044,3065,3070,3075,3080,3085,3090,3095,3100,3105,3110,3115,3120,3125,3130,3135,3140,3145,3150,3155,3160,3165,3170,3175,3180,3185,3190,3195,3200,3205,3210,3215,3220,3225,3230,3235,3240,3245,3250,3255,3260,3265,3270,3275,3280,3285,3290,3295]]])], ['1F3FC', new Map([["people-body",[1811,1816,1821,1826,1831,1836,1841,1846,1851,1856,1861,1866,1871,1876,1881,1886,1891,1896,1901,1906,1911,1916,1921,1926,1931,1936,1941,1946,1951,1956,1961,1966,1971,1976,1981,1986,1991,1996,2001,2006,2011,2016,2021,2026,2031,2036,2041,2046,2051,2056,2061,2066,2071,2076,2081,2086,2091,2096,2101,2106,2111,2116,2121,2126,2131,2136,2141,2146,2151,2156,2161,2166,2171,2176,2181,2186,2191,2196,2201,2206,2211,2216,2221,2226,2234,2235,2236,2237,2251,2259,2260,2261,2262,2276,2284,2285,2286,2287,2301,2306,2311,2316,2321,2326,2331,2336,2341,2346,2351,2356,2361,2366,2371,2376,2381,2386,2391,2396,2401,2406,2411,2416,2421,2426,2431,2436,2441,2446,2451,2456,2461,2466,2471,2476,2481,2486,2491,2496,2501,2506,2511,2516,2521,2526,2531,2536,2541,2546,2551,2556,2561,2566,2571,2576,2581,2586,2591,2596,2601,2606,2611,2616,2621,2626,2631,2636,2641,2646,2651,2656,2661,2666,2671,2676,2681,2686,2691,2696,2701,2706,2711,2716,2721,2726,2731,2736,2741,2746,2751,2756,2761,2766,2771,2776,2781,2786,2791,2796,2801,2806,2811,2816,2821,2826,2831,2836,2841,2846,2851,2856,2861,2866,2871,2876,2881,2886,2891,2896,2901,2906,2911,2916,2921,2926,2931,2936,2941,2946,2951,2956,2961,2966,2971,2976,2981,2986,2991,2996,3001,3006,3011,3016,3021,3026,3031,3036,3045,3046,3047,3048,3049,3066,3071,3076,3081,3086,3091,3096,3101,3106,3111,3116,3121,3126,3131,3136,3141,3146,3151,3156,3161,3166,3171,3176,3181,3186,3191,3196,3201,3206,3211,3216,3221,3226,3231,3236,3241,3246,3251,3256,3261,3266,3271,3276,3281,3286,3291,3296]]])], ['1F3FD', new Map([["people-body",[1812,1817,1822,1827,1832,1837,1842,1847,1852,1857,1862,1867,1872,1877,1882,1887,1892,1897,1902,1907,1912,1917,1922,1927,1932,1937,1942,1947,1952,1957,1962,1967,1972,1977,1982,1987,1992,1997,2002,2007,2012,2017,2022,2027,2032,2037,2042,2047,2052,2057,2062,2067,2072,2077,2082,2087,2092,2097,2102,2107,2112,2117,2122,2127,2132,2137,2142,2147,2152,2157,2162,2167,2172,2177,2182,2187,2192,2197,2202,2207,2212,2217,2222,2227,2238,2239,2240,2241,2252,2263,2264,2265,2266,2277,2288,2289,2290,2291,2302,2307,2312,2317,2322,2327,2332,2337,2342,2347,2352,2357,2362,2367,2372,2377,2382,2387,2392,2397,2402,2407,2412,2417,2422,2427,2432,2437,2442,2447,2452,2457,2462,2467,2472,2477,2482,2487,2492,2497,2502,2507,2512,2517,2522,2527,2532,2537,2542,2547,2552,2557,2562,2567,2572,2577,2582,2587,2592,2597,2602,2607,2612,2617,2622,2627,2632,2637,2642,2647,2652,2657,2662,2667,2672,2677,2682,2687,2692,2697,2702,2707,2712,2717,2722,2727,2732,2737,2742,2747,2752,2757,2762,2767,2772,2777,2782,2787,2792,2797,2802,2807,2812,2817,2822,2827,2832,2837,2842,2847,2852,2857,2862,2867,2872,2877,2882,2887,2892,2897,2902,2907,2912,2917,2922,2927,2932,2937,2942,2947,2952,2957,2962,2967,2972,2977,2982,2987,2992,2997,3002,3007,3012,3017,3022,3027,3032,3037,3050,3051,3052,3053,3054,3067,3072,3077,3082,3087,3092,3097,3102,3107,3112,3117,3122,3127,3132,3137,3142,3147,3152,3157,3162,3167,3172,3177,3182,3187,3192,3197,3202,3207,3212,3217,3222,3227,3232,3237,3242,3247,3252,3257,3262,3267,3272,3277,3282,3287,3292,3297]]])], ['1F3FE', new Map([["people-body",[1813,1818,1823,1828,1833,1838,1843,1848,1853,1858,1863,1868,1873,1878,1883,1888,1893,1898,1903,1908,1913,1918,1923,1928,1933,1938,1943,1948,1953,1958,1963,1968,1973,1978,1983,1988,1993,1998,2003,2008,2013,2018,2023,2028,2033,2038,2043,2048,2053,2058,2063,2068,2073,2078,2083,2088,2093,2098,2103,2108,2113,2118,2123,2128,2133,2138,2143,2148,2153,2158,2163,2168,2173,2178,2183,2188,2193,2198,2203,2208,2213,2218,2223,2228,2242,2243,2244,2245,2253,2267,2268,2269,2270,2278,2292,2293,2294,2295,2303,2308,2313,2318,2323,2328,2333,2338,2343,2348,2353,2358,2363,2368,2373,2378,2383,2388,2393,2398,2403,2408,2413,2418,2423,2428,2433,2438,2443,2448,2453,2458,2463,2468,2473,2478,2483,2488,2493,2498,2503,2508,2513,2518,2523,2528,2533,2538,2543,2548,2553,2558,2563,2568,2573,2578,2583,2588,2593,2598,2603,2608,2613,2618,2623,2628,2633,2638,2643,2648,2653,2658,2663,2668,2673,2678,2683,2688,2693,2698,2703,2708,2713,2718,2723,2728,2733,2738,2743,2748,2753,2758,2763,2768,2773,2778,2783,2788,2793,2798,2803,2808,2813,2818,2823,2828,2833,2838,2843,2848,2853,2858,2863,2868,2873,2878,2883,2888,2893,2898,2903,2908,2913,2918,2923,2928,2933,2938,2943,2948,2953,2958,2963,2968,2973,2978,2983,2988,2993,2998,3003,3008,3013,3018,3023,3028,3033,3038,3055,3056,3057,3058,3059,3068,3073,3078,3083,3088,3093,3098,3103,3108,3113,3118,3123,3128,3133,3138,3143,3148,3153,3158,3163,3168,3173,3178,3183,3188,3193,3198,3203,3208,3213,3218,3223,3228,3233,3238,3243,3248,3253,3258,3263,3268,3273,3278,3283,3288,3293,3298]]])], ['1F3FF', new Map([["people-body",[1814,1819,1824,1829,1834,1839,1844,1849,1854,1859,1864,1869,1874,1879,1884,1889,1894,1899,1904,1909,1914,1919,1924,1929,1934,1939,1944,1949,1954,1959,1964,1969,1974,1979,1984,1989,1994,1999,2004,2009,2014,2019,2024,2029,2034,2039,2044,2049,2054,2059,2064,2069,2074,2079,2084,2089,2094,2099,2104,2109,2114,2119,2124,2129,2134,2139,2144,2149,2154,2159,2164,2169,2174,2179,2184,2189,2194,2199,2204,2209,2214,2219,2224,2229,2246,2247,2248,2249,2254,2271,2272,2273,2274,2279,2296,2297,2298,2299,2304,2309,2314,2319,2324,2329,2334,2339,2344,2349,2354,2359,2364,2369,2374,2379,2384,2389,2394,2399,2404,2409,2414,2419,2424,2429,2434,2439,2444,2449,2454,2459,2464,2469,2474,2479,2484,2489,2494,2499,2504,2509,2514,2519,2524,2529,2534,2539,2544,2549,2554,2559,2564,2569,2574,2579,2584,2589,2594,2599,2604,2609,2614,2619,2624,2629,2634,2639,2644,2649,2654,2659,2664,2669,2674,2679,2684,2689,2694,2699,2704,2709,2714,2719,2724,2729,2734,2739,2744,2749,2754,2759,2764,2769,2774,2779,2784,2789,2794,2799,2804,2809,2814,2819,2824,2829,2834,2839,2844,2849,2854,2859,2864,2869,2874,2879,2884,2889,2894,2899,2904,2909,2914,2919,2924,2929,2934,2939,2944,2949,2954,2959,2964,2969,2974,2979,2984,2989,2994,2999,3004,3009,3014,3019,3024,3029,3034,3039,3060,3061,3062,3063,3064,3069,3074,3079,3084,3089,3094,3099,3104,3109,3114,3119,3124,3129,3134,3139,3144,3149,3154,3159,3164,3169,3174,3179,3184,3189,3194,3199,3204,3209,3214,3219,3224,3229,3234,3239,3244,3249,3254,3259,3264,3269,3274,3279,3284,3289,3294,3299]]])]]);
export const EmojiIndicesByCategoryNoSkin = new Map([["smileys-emotion",[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150]],["people-body",[180,186,187,193,194,195,196,197,198,199,200,201,362,363,364,365,366,367,398,399,400,407,409,438,439,440,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497]],["component",[498,499,500,501,502]],["animals-nature",[503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642]],["food-drink",[643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771]],["travel-places",[772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986]],["activities",[987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070]],["objects",[1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320]],["symbols",[1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540]],["flags",[1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621,1622,1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665,1666,1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687,1688,1689,1690,1691,1692,1693,1694,1695,1696,1697,1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712,1713,1714,1715,1716,1717,1718,1719,1720,1721,1722,1723,1724,1725,1726,1727,1728,1729,1730,1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748,1749,1750,1751,1752,1753,1754,1755,1756,1757,1758,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806,1807,1808,1809]],["custom",[3300]]]);
-export const skinCodes = {"1F3FB":"light_skin_tone","1F3FC":"medium_light_skin_tone","1F3FD":"medium_skin_tone","1F3FE":"medium_dark_skin_tone","1F3FF":"dark_skin_tone","default":"default"};
+export const skinCodes: Record = {"1F3FF":"dark_skin_tone","1F3FE":"medium_dark_skin_tone","1F3FD":"medium_skin_tone","1F3FC":"medium_light_skin_tone","1F3FB":"light_skin_tone","default":"default"};
export const EMOJI_DEFAULT_SKIN = 'default';
// Generate the list of indices that belong to each category by an specified skin
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index f14927ad01..565aca508b 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -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",
diff --git a/package-lock.json b/package-lock.json
index 42c30f11fe..91620ba23c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 8481c1584f..ec95a39221 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
}
diff --git a/patches/react-native-section-list-get-item-layout+2.2.3.patch b/patches/react-native-section-list-get-item-layout+2.2.3.patch
new file mode 100644
index 0000000000..6e9428be98
--- /dev/null
+++ b/patches/react-native-section-list-get-item-layout+2.2.3.patch
@@ -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 };
+ };
+ });
diff --git a/patches/react-native-walkthrough-tooltip+1.4.0.patch b/patches/react-native-walkthrough-tooltip+1.4.0.patch
new file mode 100644
index 0000000000..38e9501d4f
--- /dev/null
+++ b/patches/react-native-walkthrough-tooltip+1.4.0.patch
@@ -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)
+ ) {
diff --git a/types/screens/emoji_selector.d.ts b/types/screens/emoji_selector.d.ts
index 6613fbae05..2914fc0aec 100644
--- a/types/screens/emoji_selector.d.ts
+++ b/types/screens/emoji_selector.d.ts
@@ -5,6 +5,7 @@ type EmojiAlias = {
aliases: string [];
name: string;
short_name: string;
+ category?: string;
}
type EmojiSection = {