[IMPROVE] Add `All` tab in Reactions List (#4409)
Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
parent
0974bbe6ff
commit
e08ad19530
|
@ -6,11 +6,13 @@ import RNBootSplash from 'react-native-bootsplash';
|
||||||
import { selectServerRequest } from '../app/actions/server';
|
import { selectServerRequest } from '../app/actions/server';
|
||||||
import { mockedStore as store } from '../app/reducers/mockedStore';
|
import { mockedStore as store } from '../app/reducers/mockedStore';
|
||||||
import database from '../app/lib/database';
|
import database from '../app/lib/database';
|
||||||
|
import { setUser } from '../app/actions/login';
|
||||||
|
|
||||||
RNBootSplash.hide();
|
RNBootSplash.hide();
|
||||||
|
|
||||||
const baseUrl = 'https://open.rocket.chat';
|
const baseUrl = 'https://open.rocket.chat';
|
||||||
store.dispatch(selectServerRequest(baseUrl));
|
store.dispatch(selectServerRequest(baseUrl));
|
||||||
|
store.dispatch(setUser({ id: 'abc', username: 'rocket.cat', name: 'Rocket Cat' }));
|
||||||
database.setActiveDB(baseUrl);
|
database.setActiveDB(baseUrl);
|
||||||
|
|
||||||
const StorybookUIRoot = getStorybookUI({});
|
const StorybookUIRoot = getStorybookUI({});
|
||||||
|
|
|
@ -30,6 +30,7 @@ const getStories = () => {
|
||||||
require("../app/containers/markdown/new/NewMarkdown.stories.tsx"),
|
require("../app/containers/markdown/new/NewMarkdown.stories.tsx"),
|
||||||
require("../app/containers/message/Components/CollapsibleQuote/CollapsibleQuote.stories.tsx"),
|
require("../app/containers/message/Components/CollapsibleQuote/CollapsibleQuote.stories.tsx"),
|
||||||
require("../app/containers/message/Message.stories.tsx"),
|
require("../app/containers/message/Message.stories.tsx"),
|
||||||
|
require("../app/containers/ReactionsList/ReactionsList.stories.tsx"),
|
||||||
require("../app/containers/RoomHeader/RoomHeader.stories.tsx"),
|
require("../app/containers/RoomHeader/RoomHeader.stories.tsx"),
|
||||||
require("../app/containers/RoomItem/RoomItem.stories.tsx"),
|
require("../app/containers/RoomItem/RoomItem.stories.tsx"),
|
||||||
require("../app/containers/SearchBox/SearchBox.stories.tsx"),
|
require("../app/containers/SearchBox/SearchBox.stories.tsx"),
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,145 +0,0 @@
|
||||||
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 (
|
|
||||||
<Pressable
|
|
||||||
key={tab.emoji}
|
|
||||||
onPress={() => {
|
|
||||||
goToPage?.(index);
|
|
||||||
}}
|
|
||||||
style={({ pressed }: { pressed: boolean }) => ({
|
|
||||||
opacity: pressed ? 0.7 : 1
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<View style={styles.tabBarItem}>
|
|
||||||
<Emoji
|
|
||||||
content={tab.emoji}
|
|
||||||
standardEmojiStyle={styles.standardEmojiStyle}
|
|
||||||
customEmojiStyle={styles.customEmojiStyle}
|
|
||||||
baseUrl={baseUrl}
|
|
||||||
getCustomEmoji={getCustomEmoji}
|
|
||||||
/>
|
|
||||||
<Text style={[styles.reactionCount, { color: colors.auxiliaryTintColor }]}>{tab.usernames.length}</Text>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ReactionsTabBar = ({ tabs, activeTab, goToPage, baseUrl, getCustomEmoji, width }: IReactionsTabBar) => {
|
|
||||||
const tabWidth = tabs && Math.max(width / tabs.length, MIN_TAB_WIDTH);
|
|
||||||
const { colors } = useTheme();
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<ScrollView horizontal={true} showsHorizontalScrollIndicator={false}>
|
|
||||||
{tabs?.map((tab, index) => {
|
|
||||||
const isActiveTab = activeTab === index;
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: tabWidth,
|
|
||||||
borderBottomWidth: isActiveTab ? 2 : 1,
|
|
||||||
borderColor: isActiveTab ? colors.tintActive : colors.separatorColor
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TabBarItem tab={tab} index={index} goToPage={goToPage} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const UsersList = ({ tabLabel }: { tabLabel: IReaction }) => {
|
|
||||||
const { colors } = useTheme();
|
|
||||||
const { emoji, usernames } = tabLabel;
|
|
||||||
return (
|
|
||||||
<FlatList
|
|
||||||
data={usernames}
|
|
||||||
ListHeaderComponent={() => (
|
|
||||||
<View style={styles.emojiName}>
|
|
||||||
<Text style={{ color: colors.auxiliaryTintColor }}>{emoji}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
renderItem={({ item }) => (
|
|
||||||
<View style={styles.userItemContainer}>
|
|
||||||
<Avatar text={item} size={36} />
|
|
||||||
<View style={styles.usernameContainer}>
|
|
||||||
<Text style={[styles.usernameText, { color: colors.titleText }]}>{item}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
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 (
|
|
||||||
<View style={styles.reactionsListContainer}>
|
|
||||||
<ScrollableTabView renderTabBar={() => <ReactionsTabBar baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} width={width} />}>
|
|
||||||
{sortedReactions?.map(reaction => (
|
|
||||||
<UsersList tabLabel={reaction} key={reaction.emoji} />
|
|
||||||
))}
|
|
||||||
</ScrollableTabView>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ReactionsList;
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Text, View, FlatList } from 'react-native';
|
||||||
|
|
||||||
|
import Emoji from '../message/Emoji';
|
||||||
|
import { useTheme } from '../../theme';
|
||||||
|
import { IReaction } from '../../definitions';
|
||||||
|
import { TGetCustomEmoji } from '../../definitions/IEmoji';
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
import styles from './styles';
|
||||||
|
import { useAppSelector } from '../../lib/hooks';
|
||||||
|
|
||||||
|
interface IAllReactionsListItemProps {
|
||||||
|
getCustomEmoji: TGetCustomEmoji;
|
||||||
|
item: IReaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAllTabProps {
|
||||||
|
getCustomEmoji: TGetCustomEmoji;
|
||||||
|
tabLabel: IReaction;
|
||||||
|
reactions?: IReaction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const AllReactionsListItem = ({ item, getCustomEmoji }: IAllReactionsListItemProps) => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const useRealName = useAppSelector(state => state.settings.UI_Use_Real_Name);
|
||||||
|
const server = useAppSelector(state => state.server.server);
|
||||||
|
const username = useAppSelector(state => state.login.user.username);
|
||||||
|
const count = item.usernames.length;
|
||||||
|
|
||||||
|
let displayNames;
|
||||||
|
if (useRealName && item.names) {
|
||||||
|
displayNames = item.names
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((name, index) => (item.usernames[index] === username ? I18n.t('you') : name))
|
||||||
|
.join(', ');
|
||||||
|
} else {
|
||||||
|
displayNames = item.usernames
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((otherUsername: string) => (username === otherUsername ? I18n.t('you') : otherUsername))
|
||||||
|
.join(', ');
|
||||||
|
}
|
||||||
|
if (count > 3) {
|
||||||
|
displayNames = `${displayNames} ${I18n.t('and_more')} ${count - 3}`;
|
||||||
|
} else {
|
||||||
|
displayNames = displayNames.replace(/,(?=[^,]*$)/, ` ${I18n.t('and')}`);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<View style={styles.listItemContainer}>
|
||||||
|
<Emoji
|
||||||
|
content={item.emoji}
|
||||||
|
standardEmojiStyle={styles.allTabStandardEmojiStyle}
|
||||||
|
customEmojiStyle={styles.allTabCustomEmojiStyle}
|
||||||
|
baseUrl={server}
|
||||||
|
getCustomEmoji={getCustomEmoji}
|
||||||
|
/>
|
||||||
|
<View style={styles.textContainer}>
|
||||||
|
<Text style={[styles.allListNPeopleReacted, { color: colors.bodyText }]}>
|
||||||
|
{count === 1 ? I18n.t('1_person_reacted') : I18n.t('N_people_reacted', { n: count })}
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.allListWhoReacted, { color: colors.auxiliaryText }]}>{displayNames}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AllTab = ({ reactions, getCustomEmoji }: IAllTabProps): React.ReactElement => (
|
||||||
|
<View style={styles.allTabContainer} testID='reactionsListAllTab'>
|
||||||
|
<FlatList
|
||||||
|
data={reactions}
|
||||||
|
contentContainerStyle={styles.listContainer}
|
||||||
|
renderItem={({ item }) => <AllReactionsListItem item={item} getCustomEmoji={getCustomEmoji} />}
|
||||||
|
keyExtractor={item => item.emoji}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AllTab;
|
|
@ -0,0 +1,71 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
|
||||||
|
import { TGetCustomEmoji, IEmoji } from '../../definitions';
|
||||||
|
import ReactionsList from '.';
|
||||||
|
import { mockedStore as store } from '../../reducers/mockedStore';
|
||||||
|
import { updateSettings } from '../../actions/settings';
|
||||||
|
|
||||||
|
const getCustomEmoji: TGetCustomEmoji = content => {
|
||||||
|
const customEmoji = {
|
||||||
|
marioparty: { name: content, extension: 'gif' },
|
||||||
|
react_rocket: { name: content, extension: 'png' },
|
||||||
|
nyan_rocket: { name: content, extension: 'png' }
|
||||||
|
}[content] as IEmoji;
|
||||||
|
return customEmoji;
|
||||||
|
};
|
||||||
|
|
||||||
|
const reactions = [
|
||||||
|
{
|
||||||
|
emoji: ':marioparty:',
|
||||||
|
_id: 'marioparty',
|
||||||
|
usernames: ['rocket.cat', 'diego.mello'],
|
||||||
|
names: ['Rocket Cat', 'Diego Mello']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emoji: ':react_rocket:',
|
||||||
|
_id: 'react_rocket',
|
||||||
|
usernames: ['rocket.cat', 'diego.mello'],
|
||||||
|
names: ['Rocket Cat', 'Diego Mello']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emoji: ':nyan_rocket:',
|
||||||
|
_id: 'nyan_rocket',
|
||||||
|
usernames: ['rocket.cat'],
|
||||||
|
names: ['Rocket Cat']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emoji: ':grinning:',
|
||||||
|
_id: 'grinning',
|
||||||
|
usernames: ['diego.mello'],
|
||||||
|
names: ['Diego Mello']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emoji: ':tada:',
|
||||||
|
_id: 'tada',
|
||||||
|
usernames: ['diego.mello'],
|
||||||
|
names: ['Diego Mello']
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ReactionsListStory = () => {
|
||||||
|
store.dispatch(updateSettings('UI_Use_Real_Name', false));
|
||||||
|
return (
|
||||||
|
<View style={{ paddingVertical: 10, flex: 1 }}>
|
||||||
|
<ReactionsList getCustomEmoji={getCustomEmoji} reactions={reactions} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReactionsListFullName = () => {
|
||||||
|
store.dispatch(updateSettings('UI_Use_Real_Name', true));
|
||||||
|
return (
|
||||||
|
<View style={{ paddingVertical: 10, flex: 1 }}>
|
||||||
|
<ReactionsList getCustomEmoji={getCustomEmoji} reactions={reactions} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'ReactionsList'
|
||||||
|
};
|
|
@ -0,0 +1,79 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { fireEvent, render, within } from '@testing-library/react-native';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import ReactionsList from '.';
|
||||||
|
import { mockedStore } from '../../reducers/mockedStore';
|
||||||
|
|
||||||
|
const getCustomEmoji = jest.fn();
|
||||||
|
const reactions = [
|
||||||
|
{
|
||||||
|
emoji: 'marioparty',
|
||||||
|
_id: 'marioparty',
|
||||||
|
usernames: ['rocket.cat', 'diego.mello'],
|
||||||
|
names: ['Rocket Cat', 'Diego Mello']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emoji: 'react_rocket',
|
||||||
|
_id: 'react_rocket',
|
||||||
|
usernames: ['rocket.cat', 'diego.mello'],
|
||||||
|
names: ['Rocket Cat', 'Diego Mello']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emoji: 'nyan_rocket',
|
||||||
|
_id: 'nyan_rocket',
|
||||||
|
usernames: ['rocket.cat'],
|
||||||
|
names: ['Rocket Cat']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emoji: 'grinning',
|
||||||
|
_id: 'grinning',
|
||||||
|
usernames: ['diego.mello'],
|
||||||
|
names: ['Diego Mello']
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const Render = () => (
|
||||||
|
<Provider store={mockedStore}>
|
||||||
|
<ReactionsList getCustomEmoji={getCustomEmoji} reactions={reactions} />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('ReactionsList', () => {
|
||||||
|
test('should render Reactions List', async () => {
|
||||||
|
const { findByTestId } = render(<Render />);
|
||||||
|
const ReactionsListView = await findByTestId('reactionsList');
|
||||||
|
expect(ReactionsListView).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render tab bar', async () => {
|
||||||
|
const { findByTestId } = render(<Render />);
|
||||||
|
const AllTab = await findByTestId('reactionsTabBar');
|
||||||
|
expect(AllTab).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render All tab', async () => {
|
||||||
|
const { findByTestId } = render(<Render />);
|
||||||
|
const AllTab = await findByTestId('reactionsListAllTab');
|
||||||
|
expect(AllTab).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('correct tab on clicking tab item', async () => {
|
||||||
|
const { findByTestId } = render(<Render />);
|
||||||
|
const tab = await findByTestId(`tabBarItem-${reactions[0].emoji}`);
|
||||||
|
fireEvent.press(tab);
|
||||||
|
const usersList = await findByTestId(`usersList-${reactions[0].emoji}`);
|
||||||
|
expect(usersList).toBeTruthy();
|
||||||
|
const emojiName = await within(usersList).getByTestId(`usersListEmojiName`);
|
||||||
|
expect(emojiName.props.children).toEqual(reactions[0].emoji);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render correct number of reactions', async () => {
|
||||||
|
const { findByTestId } = render(<Render />);
|
||||||
|
const tab = await findByTestId(`tabBarItem-${reactions[0].emoji}`);
|
||||||
|
fireEvent.press(tab);
|
||||||
|
const usersList = await findByTestId(`usersList-${reactions[0].emoji}`);
|
||||||
|
const allReactions = await within(usersList).getAllByTestId('userItem');
|
||||||
|
expect(allReactions).toHaveLength(reactions[0].usernames.length);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,89 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Text, Pressable, View, ScrollView } from 'react-native';
|
||||||
|
|
||||||
|
import Emoji from '../message/Emoji';
|
||||||
|
import { useTheme } from '../../theme';
|
||||||
|
import { IReaction } from '../../definitions';
|
||||||
|
import { TGetCustomEmoji } from '../../definitions/IEmoji';
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
import styles, { MIN_TAB_WIDTH } from './styles';
|
||||||
|
import { useDimensions, useOrientation } from '../../dimensions';
|
||||||
|
import { useAppSelector } from '../../lib/hooks';
|
||||||
|
|
||||||
|
interface ITabBarItem {
|
||||||
|
getCustomEmoji: TGetCustomEmoji;
|
||||||
|
tab: IReaction;
|
||||||
|
index: number;
|
||||||
|
goToPage?: (index: number) => void;
|
||||||
|
}
|
||||||
|
interface IReactionsTabBar {
|
||||||
|
getCustomEmoji: TGetCustomEmoji;
|
||||||
|
activeTab?: number;
|
||||||
|
tabs?: IReaction[];
|
||||||
|
goToPage?: (index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabBarItem = ({ tab, index, goToPage, getCustomEmoji }: ITabBarItem) => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const server = useAppSelector(state => state.server.server);
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={tab.emoji}
|
||||||
|
onPress={() => {
|
||||||
|
goToPage?.(index);
|
||||||
|
}}
|
||||||
|
style={({ pressed }: { pressed: boolean }) => ({
|
||||||
|
opacity: pressed ? 0.7 : 1
|
||||||
|
})}
|
||||||
|
testID={`tabBarItem-${tab.emoji}`}
|
||||||
|
>
|
||||||
|
<View style={styles.tabBarItem}>
|
||||||
|
{tab._id === 'All' ? (
|
||||||
|
<Text style={[styles.allTabItem, { color: colors.auxiliaryTintColor }]}>{I18n.t('All')}</Text>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Emoji
|
||||||
|
content={tab.emoji}
|
||||||
|
standardEmojiStyle={styles.standardEmojiStyle}
|
||||||
|
customEmojiStyle={styles.customEmojiStyle}
|
||||||
|
baseUrl={server}
|
||||||
|
getCustomEmoji={getCustomEmoji}
|
||||||
|
/>
|
||||||
|
<Text style={[styles.reactionCount, { color: colors.auxiliaryTintColor }]}>{tab.usernames.length}</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReactionsTabBar = ({ tabs, activeTab, goToPage, getCustomEmoji }: IReactionsTabBar): React.ReactElement => {
|
||||||
|
const { isLandscape } = useOrientation();
|
||||||
|
const { width } = useDimensions();
|
||||||
|
const reactionsListWidth = isLandscape ? width / 2 : width;
|
||||||
|
const tabWidth = tabs && Math.max(reactionsListWidth / tabs.length, MIN_TAB_WIDTH);
|
||||||
|
const { colors } = useTheme();
|
||||||
|
return (
|
||||||
|
<View testID='reactionsTabBar'>
|
||||||
|
<ScrollView horizontal={true} showsHorizontalScrollIndicator={false}>
|
||||||
|
{tabs?.map((tab, index) => {
|
||||||
|
const isActiveTab = activeTab === index;
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: tabWidth,
|
||||||
|
borderBottomWidth: isActiveTab ? 2 : 1,
|
||||||
|
borderColor: isActiveTab ? colors.tintActive : colors.separatorColor
|
||||||
|
}}
|
||||||
|
key={tab.emoji}
|
||||||
|
>
|
||||||
|
<TabBarItem tab={tab} index={index} goToPage={goToPage} getCustomEmoji={getCustomEmoji} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReactionsTabBar;
|
|
@ -0,0 +1,47 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Text, View, FlatList } from 'react-native';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import { useTheme } from '../../theme';
|
||||||
|
import { IReaction, IApplicationState } from '../../definitions';
|
||||||
|
import Avatar from '../Avatar';
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
|
const UsersList = ({ tabLabel }: { tabLabel: IReaction }): React.ReactElement => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const useRealName = useSelector((state: IApplicationState) => state.settings.UI_Use_Real_Name);
|
||||||
|
|
||||||
|
const { emoji, usernames, names } = tabLabel;
|
||||||
|
const users =
|
||||||
|
names?.length > 0
|
||||||
|
? usernames.map((username, index) => ({ username, name: names[index] }))
|
||||||
|
: usernames.map(username => ({ username, name: '' }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
data={users}
|
||||||
|
contentContainerStyle={styles.listContainer}
|
||||||
|
ListHeaderComponent={
|
||||||
|
<View style={styles.emojiNameContainer}>
|
||||||
|
<Text style={[styles.emojiName, { color: colors.auxiliaryText }]} testID='usersListEmojiName'>
|
||||||
|
{emoji}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<View style={styles.listItemContainer} testID='userItem'>
|
||||||
|
<Avatar text={item.username} size={36} />
|
||||||
|
<View style={styles.textContainer}>
|
||||||
|
<Text style={[styles.usernameText, { color: colors.bodyText }]} numberOfLines={1}>
|
||||||
|
{useRealName && item.name ? item.name : item.username}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
keyExtractor={item => item.username}
|
||||||
|
testID={`usersList-${emoji}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UsersList;
|
|
@ -0,0 +1,34 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
import ScrollableTabView from 'react-native-scrollable-tab-view';
|
||||||
|
|
||||||
|
import { TGetCustomEmoji } from '../../definitions/IEmoji';
|
||||||
|
import { IReaction } from '../../definitions';
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
import styles from './styles';
|
||||||
|
import AllTab from './AllTab';
|
||||||
|
import UsersList from './UsersList';
|
||||||
|
import ReactionsTabBar from './ReactionsTabBar';
|
||||||
|
|
||||||
|
interface IReactionsListProps {
|
||||||
|
getCustomEmoji: TGetCustomEmoji;
|
||||||
|
reactions?: IReaction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReactionsList = ({ reactions, getCustomEmoji }: 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);
|
||||||
|
const allTabLabel = { emoji: I18n.t('All'), usernames: [], names: [], _id: 'All' };
|
||||||
|
return (
|
||||||
|
<View style={styles.container} testID='reactionsList'>
|
||||||
|
<ScrollableTabView renderTabBar={() => <ReactionsTabBar getCustomEmoji={getCustomEmoji} />}>
|
||||||
|
<AllTab tabLabel={allTabLabel} reactions={sortedReactions} getCustomEmoji={getCustomEmoji} />
|
||||||
|
{sortedReactions?.map(reaction => (
|
||||||
|
<UsersList tabLabel={reaction} key={reaction.emoji} />
|
||||||
|
))}
|
||||||
|
</ScrollableTabView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReactionsList;
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
|
||||||
|
import sharedStyles from '../../views/Styles';
|
||||||
|
|
||||||
|
export const MIN_TAB_WIDTH = 70;
|
||||||
|
|
||||||
|
export default StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1
|
||||||
|
},
|
||||||
|
allTabContainer: {
|
||||||
|
flex: 1
|
||||||
|
},
|
||||||
|
tabBarItem: {
|
||||||
|
paddingBottom: 4,
|
||||||
|
height: 44,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'row'
|
||||||
|
},
|
||||||
|
listContainer: {
|
||||||
|
marginHorizontal: 12,
|
||||||
|
marginVertical: 8,
|
||||||
|
paddingBottom: 30
|
||||||
|
},
|
||||||
|
reactionCount: {
|
||||||
|
marginLeft: 4,
|
||||||
|
...sharedStyles.textSemibold
|
||||||
|
},
|
||||||
|
emojiNameContainer: {
|
||||||
|
marginVertical: 8
|
||||||
|
},
|
||||||
|
emojiName: {
|
||||||
|
fontSize: 14,
|
||||||
|
...sharedStyles.textMedium
|
||||||
|
},
|
||||||
|
listItemContainer: {
|
||||||
|
marginVertical: 6,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
textContainer: {
|
||||||
|
flex: 1,
|
||||||
|
marginLeft: 8,
|
||||||
|
justifyContent: 'center'
|
||||||
|
},
|
||||||
|
usernameText: {
|
||||||
|
fontSize: 16,
|
||||||
|
...sharedStyles.textSemibold
|
||||||
|
},
|
||||||
|
standardEmojiStyle: {
|
||||||
|
fontSize: 20,
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#fff'
|
||||||
|
},
|
||||||
|
customEmojiStyle: {
|
||||||
|
width: 24,
|
||||||
|
height: 24
|
||||||
|
},
|
||||||
|
allTabItem: {
|
||||||
|
fontSize: 16,
|
||||||
|
...sharedStyles.textSemibold
|
||||||
|
},
|
||||||
|
allTabStandardEmojiStyle: {
|
||||||
|
fontSize: 30,
|
||||||
|
width: 36,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#fff'
|
||||||
|
},
|
||||||
|
allTabCustomEmojiStyle: {
|
||||||
|
width: 36,
|
||||||
|
height: 36
|
||||||
|
},
|
||||||
|
allListItemContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
allListNPeopleReacted: {
|
||||||
|
fontSize: 14,
|
||||||
|
...sharedStyles.textMedium
|
||||||
|
},
|
||||||
|
allListWhoReacted: {
|
||||||
|
fontSize: 14,
|
||||||
|
...sharedStyles.textRegular
|
||||||
|
}
|
||||||
|
});
|
|
@ -114,7 +114,16 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} messageId={id} />;
|
return (
|
||||||
|
<Reply
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
attachment={file}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
getCustomEmoji={getCustomEmoji}
|
||||||
|
messageId={id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
return <>{attachmentsElements}</>;
|
return <>{attachmentsElements}</>;
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,4 +2,5 @@ export interface IReaction {
|
||||||
_id: string;
|
_id: string;
|
||||||
emoji: string;
|
emoji: string;
|
||||||
usernames: string[];
|
usernames: string[];
|
||||||
|
names: string[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,8 @@ export default (msg: any): IMessage | IThreadResult | null => {
|
||||||
msg.reactions = Object.keys(msg.reactions).map(key => ({
|
msg.reactions = Object.keys(msg.reactions).map(key => ({
|
||||||
_id: `${msg._id}${key}`,
|
_id: `${msg._id}${key}`,
|
||||||
emoji: key,
|
emoji: key,
|
||||||
usernames: msg.reactions ? msg.reactions[key].usernames : []
|
usernames: msg.reactions ? msg.reactions[key].usernames : [],
|
||||||
|
names: msg.reactions ? msg.reactions[key].names : []
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (msg.translations && Object.keys(msg.translations).length) {
|
if (msg.translations && Object.keys(msg.translations).length) {
|
||||||
|
|
|
@ -862,18 +862,11 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
|
||||||
|
|
||||||
onReactionLongPress = (message: TAnyMessageModel) => {
|
onReactionLongPress = (message: TAnyMessageModel) => {
|
||||||
this.setState({ selectedMessage: message });
|
this.setState({ selectedMessage: message });
|
||||||
const { showActionSheet, baseUrl, width } = this.props;
|
const { showActionSheet } = this.props;
|
||||||
const { selectedMessage } = this.state;
|
const { selectedMessage } = this.state;
|
||||||
this.messagebox?.current?.closeEmojiAndAction(showActionSheet, {
|
this.messagebox?.current?.closeEmojiAndAction(showActionSheet, {
|
||||||
children: (
|
children: <ReactionsList reactions={selectedMessage?.reactions} getCustomEmoji={this.getCustomEmoji} />,
|
||||||
<ReactionsList
|
snaps: ['50%', '80%'],
|
||||||
reactions={selectedMessage?.reactions}
|
|
||||||
baseUrl={baseUrl}
|
|
||||||
getCustomEmoji={this.getCustomEmoji}
|
|
||||||
width={width}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
snaps: ['50%'],
|
|
||||||
enableContentPanningGesture: false
|
enableContentPanningGesture: false
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -306,7 +306,7 @@ describe('Room screen', () => {
|
||||||
.withTimeout(60000);
|
.withTimeout(60000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show reaction picker on add reaction button pressed and have frequently used emoji, and dismiss review nag', async () => {
|
it('should show reaction picker on add reaction button pressed and have frequently used emoji', async () => {
|
||||||
await element(by.id('message-add-reaction')).tap();
|
await element(by.id('message-add-reaction')).tap();
|
||||||
await waitFor(element(by.id('reaction-picker')))
|
await waitFor(element(by.id('reaction-picker')))
|
||||||
.toExist()
|
.toExist()
|
||||||
|
@ -324,6 +324,19 @@ describe('Room screen', () => {
|
||||||
.withTimeout(60000);
|
.withTimeout(60000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should ask for review', async () => {
|
||||||
|
await dismissReviewNag(); // TODO: Create a proper test for this elsewhere.
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open/close reactions list', async () => {
|
||||||
|
await element(by.id('message-reaction-:grinning:')).longPress();
|
||||||
|
await waitFor(element(by.id('reactionsList')))
|
||||||
|
.toExist()
|
||||||
|
.withTimeout(4000);
|
||||||
|
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||||
|
await element(by.id('action-sheet-handle')).swipe('down', 'fast', 0.5);
|
||||||
|
});
|
||||||
|
|
||||||
it('should remove reaction', async () => {
|
it('should remove reaction', async () => {
|
||||||
await element(by.id('message-reaction-:grinning:')).tap();
|
await element(by.id('message-reaction-:grinning:')).tap();
|
||||||
await waitFor(element(by.id('message-reaction-:grinning:')))
|
await waitFor(element(by.id('message-reaction-:grinning:')))
|
||||||
|
@ -331,10 +344,6 @@ describe('Room screen', () => {
|
||||||
.withTimeout(60000);
|
.withTimeout(60000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ask for review', async () => {
|
|
||||||
await dismissReviewNag(); // TODO: Create a proper test for this elsewhere.
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should edit message', async () => {
|
it('should edit message', async () => {
|
||||||
await mockMessage('edit');
|
await mockMessage('edit');
|
||||||
await element(by[textMatcher](`${data.random}edit`))
|
await element(by[textMatcher](`${data.random}edit`))
|
||||||
|
|
Loading…
Reference in New Issue