From a4f171a12d0c5a1830ad71551a584df7028db10b Mon Sep 17 00:00:00 2001
From: Danish Ahmed Mirza <77742477+try-catch-stack@users.noreply.github.com>
Date: Wed, 3 Aug 2022 22:02:16 +0530
Subject: [PATCH] [NEW] Redesign reactions list (#4346)
---
app/containers/ActionSheet/ActionSheet.tsx | 2 +
app/containers/ActionSheet/Provider.tsx | 1 +
app/containers/ReactionsList.tsx | 143 ++++++++++++++++++
app/containers/ReactionsModal.tsx | 166 ---------------------
app/definitions/index.ts | 1 +
app/views/RoomView/index.tsx | 38 ++---
6 files changed, 167 insertions(+), 184 deletions(-)
create mode 100644 app/containers/ReactionsList.tsx
delete mode 100644 app/containers/ReactionsModal.tsx
diff --git a/app/containers/ActionSheet/ActionSheet.tsx b/app/containers/ActionSheet/ActionSheet.tsx
index fb00d5267..e114b8e1c 100644
--- a/app/containers/ActionSheet/ActionSheet.tsx
+++ b/app/containers/ActionSheet/ActionSheet.tsx
@@ -140,6 +140,8 @@ const ActionSheet = React.memo(
style={{ ...styles.container, ...bottomSheet }}
backgroundStyle={{ backgroundColor: colors.focusedBackground }}
onChange={index => index === -1 && onClose()}
+ // We need this to allow horizontal swipe gestures inside bottom sheet like in reaction picker
+ enableContentPanningGesture={data?.enableContentPanningGesture ?? true}
{...androidTablet}>
diff --git a/app/containers/ActionSheet/Provider.tsx b/app/containers/ActionSheet/Provider.tsx
index b7caf6416..4b21726bc 100644
--- a/app/containers/ActionSheet/Provider.tsx
+++ b/app/containers/ActionSheet/Provider.tsx
@@ -22,6 +22,7 @@ export type TActionSheetOptions = {
children?: React.ReactElement | null;
snaps?: (string | number)[];
onClose?: () => void;
+ enableContentPanningGesture?: boolean;
};
export interface IActionSheetProvider {
showActionSheet: (item: TActionSheetOptions) => void;
diff --git a/app/containers/ReactionsList.tsx b/app/containers/ReactionsList.tsx
new file mode 100644
index 000000000..10a1b3c80
--- /dev/null
+++ b/app/containers/ReactionsList.tsx
@@ -0,0 +1,143 @@
+import React from 'react';
+import { StyleSheet, Text, Pressable, View, ScrollView } from 'react-native';
+import ScrollableTabView from 'react-native-scrollable-tab-view';
+import { FlatList } from 'react-native-gesture-handler';
+
+import Emoji from './message/Emoji';
+import { useTheme } from '../theme';
+import { TGetCustomEmoji } from '../definitions/IEmoji';
+import { IReaction } from '../definitions';
+import Avatar from './Avatar';
+import sharedStyles from '../views/Styles';
+
+const MIN_TAB_WIDTH = 70;
+
+const styles = StyleSheet.create({
+ reactionsListContainer: { height: '100%', width: '100%' },
+ tabBarItem: {
+ paddingHorizontal: 10,
+ paddingBottom: 10,
+ justifyContent: 'center',
+ alignItems: 'center',
+ flexDirection: 'row'
+ },
+ reactionCount: { marginLeft: 5 },
+ emojiName: { margin: 10 },
+ userItemContainer: { marginHorizontal: 10, marginVertical: 5, flexDirection: 'row' },
+ usernameContainer: { marginHorizontal: 10, justifyContent: 'center' },
+ usernameText: { fontSize: 17, ...sharedStyles.textMedium },
+ standardEmojiStyle: { fontSize: 20, color: '#fff' },
+ customEmojiStyle: { width: 25, height: 25 }
+});
+
+interface IReactionsListBase {
+ baseUrl: string;
+ getCustomEmoji: TGetCustomEmoji;
+}
+
+interface IReactionsListProps extends IReactionsListBase {
+ reactions?: IReaction[];
+ width: number;
+}
+
+interface ITabBarItem extends IReactionsListBase {
+ tab: IReaction;
+ index: number;
+ goToPage?: (index: number) => void;
+}
+interface IReactionsTabBar extends IReactionsListBase {
+ activeTab?: number;
+ tabs?: IReaction[];
+ goToPage?: (index: number) => void;
+ width: number;
+}
+
+const TabBarItem = ({ tab, index, goToPage, baseUrl, getCustomEmoji }: ITabBarItem) => {
+ const { colors } = useTheme();
+ return (
+ {
+ goToPage?.(index);
+ }}
+ style={({ pressed }: { pressed: boolean }) => ({
+ opacity: pressed ? 0.7 : 1
+ })}>
+
+
+ {tab.usernames.length}
+
+
+ );
+};
+
+const ReactionsTabBar = ({ tabs, activeTab, goToPage, baseUrl, getCustomEmoji, width }: IReactionsTabBar) => {
+ const tabWidth = tabs && Math.max(width / tabs.length, MIN_TAB_WIDTH);
+ const { colors } = useTheme();
+ return (
+
+
+ {tabs?.map((tab, index) => {
+ const isActiveTab = activeTab === index;
+ return (
+
+
+
+ );
+ })}
+
+
+ );
+};
+
+const UsersList = ({ tabLabel }: { tabLabel: IReaction }) => {
+ const { colors } = useTheme();
+ const { emoji, usernames } = tabLabel;
+ return (
+ (
+
+ {emoji}
+
+ )}
+ renderItem={({ item }) => (
+
+
+
+ {item}
+
+
+ )}
+ keyExtractor={item => item}
+ />
+ );
+};
+
+const ReactionsList = ({ reactions, baseUrl, getCustomEmoji, width }: IReactionsListProps): React.ReactElement => {
+ // sorting reactions in descending order on the basic of number of users reacted
+ const sortedReactions = reactions?.sort((reaction1, reaction2) => reaction2.usernames.length - reaction1.usernames.length);
+
+ return (
+
+ }>
+ {sortedReactions?.map(reaction => (
+
+ ))}
+
+
+ );
+};
+
+export default ReactionsList;
diff --git a/app/containers/ReactionsModal.tsx b/app/containers/ReactionsModal.tsx
deleted file mode 100644
index b74707f04..000000000
--- a/app/containers/ReactionsModal.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-import React from 'react';
-import { FlatList, StyleSheet, Text, View } from 'react-native';
-import Modal from 'react-native-modal';
-import Touchable from 'react-native-platform-touchable';
-
-import Emoji from './message/Emoji';
-import I18n from '../i18n';
-import { CustomIcon } from './CustomIcon';
-import sharedStyles from '../views/Styles';
-import { themes } from '../lib/constants';
-import { TSupportedThemes, useTheme, withTheme } from '../theme';
-import { TGetCustomEmoji } from '../definitions/IEmoji';
-import { TMessageModel, ILoggedUser } from '../definitions';
-import SafeAreaView from './SafeAreaView';
-
-const styles = StyleSheet.create({
- safeArea: {
- backgroundColor: 'transparent'
- },
- titleContainer: {
- alignItems: 'center',
- paddingVertical: 10
- },
- title: {
- fontSize: 16,
- ...sharedStyles.textSemibold,
- ...sharedStyles.textAlignCenter
- },
- reactCount: {
- fontSize: 13,
- ...sharedStyles.textRegular
- },
- peopleReacted: {
- fontSize: 14,
- ...sharedStyles.textMedium
- },
- peopleItemContainer: {
- flex: 1,
- flexDirection: 'column',
- justifyContent: 'center'
- },
- emojiContainer: {
- width: 50,
- height: 50,
- alignItems: 'center',
- justifyContent: 'center'
- },
- itemContainer: {
- height: 50,
- flexDirection: 'row'
- },
- listContainer: {
- flex: 1
- },
- closeButton: {
- position: 'absolute',
- left: 0,
- top: 10
- }
-});
-const standardEmojiStyle = { fontSize: 20 };
-const customEmojiStyle = { width: 20, height: 20 };
-
-interface ISharedFields {
- user?: Pick;
- baseUrl: string;
- getCustomEmoji: TGetCustomEmoji;
-}
-
-interface IItem extends ISharedFields {
- item: {
- usernames: string[];
- emoji: string;
- };
-}
-
-interface IModalContent extends ISharedFields {
- message?: TMessageModel;
- onClose: () => void;
- theme: TSupportedThemes;
-}
-
-interface IReactionsModal extends ISharedFields {
- message?: TMessageModel;
- isVisible: boolean;
- onClose(): void;
-}
-
-const Item = React.memo(({ item, user, baseUrl, getCustomEmoji }: IItem) => {
- const { theme } = useTheme();
- const count = item.usernames.length;
- let usernames = item.usernames
- .slice(0, 3)
- .map((username: string) => (username === user?.username ? I18n.t('you') : username))
- .join(', ');
- if (count > 3) {
- usernames = `${usernames} ${I18n.t('and_more')} ${count - 3}`;
- } else {
- usernames = usernames.replace(/,(?=[^,]*$)/, ` ${I18n.t('and')}`);
- }
- return (
-
-
-
-
-
-
- {count === 1 ? I18n.t('1_person_reacted') : I18n.t('N_people_reacted', { n: count })}
-
- {usernames}
-
-
- );
-});
-
-const ModalContent = React.memo(({ message, onClose, ...props }: IModalContent) => {
- if (message && message.reactions) {
- return (
-
-
-
-
- {I18n.t('Reactions')}
-
-
- }
- keyExtractor={item => item.emoji}
- />
-
- );
- }
- return null;
-});
-
-const ReactionsModal = React.memo(
- ({ isVisible, onClose, ...props }: IReactionsModal) => {
- const { theme } = useTheme();
- return (
-
-
-
- );
- },
- (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible
-);
-
-ReactionsModal.displayName = 'ReactionsModal';
-ModalContent.displayName = 'ReactionsModalContent';
-Item.displayName = 'ReactionsModalItem';
-
-export default withTheme(ReactionsModal);
diff --git a/app/definitions/index.ts b/app/definitions/index.ts
index 4f7d7c3d3..cbe244639 100644
--- a/app/definitions/index.ts
+++ b/app/definitions/index.ts
@@ -28,6 +28,7 @@ export * from './ICredentials';
export * from './ISearch';
export * from './TUserStatus';
export * from './IProfile';
+export * from './IReaction';
export interface IBaseScreen, S extends string> {
navigation: StackNavigationProp;
diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx
index 0c79ddd41..f12177925 100644
--- a/app/views/RoomView/index.tsx
+++ b/app/views/RoomView/index.tsx
@@ -3,7 +3,6 @@ import { InteractionManager, Text, View } from 'react-native';
import { connect } from 'react-redux';
import parse from 'url-parse';
import moment from 'moment';
-import * as Haptics from 'expo-haptics';
import { Q } from '@nozbe/watermelondb';
import { dequal } from 'dequal';
import { EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context';
@@ -22,7 +21,7 @@ import EventEmitter from '../../lib/methods/helpers/events';
import I18n from '../../i18n';
import RoomHeader from '../../containers/RoomHeader';
import StatusBar from '../../containers/StatusBar';
-import ReactionsModal from '../../containers/ReactionsModal';
+import ReactionsList from '../../containers/ReactionsList';
import { LISTENER } from '../../containers/Toast';
import { getBadgeColor, isBlocked, makeThreadName } from '../../lib/methods/helpers/room';
import { isReadOnly } from '../../lib/methods/helpers/isReadOnly';
@@ -100,6 +99,7 @@ import {
hasPermission
} from '../../lib/methods/helpers';
import { Services } from '../../lib/services';
+import { withActionSheet, TActionSheetOptions } from '../../containers/ActionSheet';
type TStateAttrsUpdate = keyof IRoomViewState;
@@ -170,6 +170,7 @@ interface IRoomViewProps extends IBaseScreen {
transferLivechatGuestPermission?: string[]; // TODO: Check if its the correct type
viewCannedResponsesPermission?: string[]; // TODO: Check if its the correct type
livechatAllowManualOnHold?: boolean;
+ showActionSheet: (options: TActionSheetOptions) => void;
}
interface IRoomViewState {
@@ -853,12 +854,21 @@ class RoomView extends React.Component {
};
onReactionLongPress = (message: TAnyMessageModel) => {
- this.setState({ selectedMessage: message, reactionsModalVisible: true });
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- };
-
- onCloseReactionsModal = () => {
- this.setState({ selectedMessage: undefined, reactionsModalVisible: false });
+ this.setState({ selectedMessage: message });
+ const { showActionSheet, baseUrl, width } = this.props;
+ const { selectedMessage } = this.state;
+ showActionSheet({
+ children: (
+
+ ),
+ snaps: ['50%'],
+ enableContentPanningGesture: false
+ });
};
onEncryptedPress = () => {
@@ -1469,7 +1479,7 @@ class RoomView extends React.Component {
render() {
console.count(`${this.constructor.name}.render calls`);
- const { room, reactionsModalVisible, selectedMessage, loading, reacting, showingBlockingLoader } = this.state;
+ const { room, selectedMessage, loading, reacting, showingBlockingLoader } = this.state;
const { user, baseUrl, theme, navigation, Hide_System_Messages, width, height, serverVersion } = this.props;
const { rid, t } = room;
let sysMes;
@@ -1512,14 +1522,6 @@ class RoomView extends React.Component {
theme={theme}
/>
-
@@ -1545,4 +1547,4 @@ const mapStateToProps = (state: IApplicationState) => ({
livechatAllowManualOnHold: state.settings.Livechat_allow_manual_on_hold as boolean
});
-export default connect(mapStateToProps)(withDimensions(withTheme(withSafeAreaInsets(RoomView))));
+export default connect(mapStateToProps)(withDimensions(withTheme(withSafeAreaInsets(withActionSheet(RoomView)))));