Move User list to a component (#6783)

* Move user list to a component

* added tests to see that several situations render
This commit is contained in:
Javier Aguirre
2022-11-23 19:08:22 +01:00
committed by GitHub
parent 068549a285
commit 81dcfc817b
5 changed files with 935 additions and 3 deletions

View File

@@ -0,0 +1,817 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/channel_list_row should show no results 1`] = `
<RCTScrollView
ListEmptyComponent={[Function]}
ListFooterComponent={[Function]}
contentContainerStyle={
{
"flexGrow": 1,
}
}
data={
[
{
"data": [
{
"auth_service": "",
"create_at": 1111,
"delete_at": 0,
"email": "john@doe.com",
"first_name": "",
"id": "1",
"last_name": "",
"locale": "",
"nickname": "",
"notify_props": {
"channel": "true",
"comments": "never",
"desktop": "mention",
"desktop_sound": "true",
"email": "true",
"first_name": "true",
"mention_keys": "",
"push": "mention",
"push_status": "away",
},
"position": "",
"roles": "",
"update_at": 1111,
"username": "johndoe",
},
],
"id": "J",
},
]
}
extraData={false}
getItem={[Function]}
getItemCount={[Function]}
initialNumToRender={15}
keyExtractor={[Function]}
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={16}
onContentSizeChange={[Function]}
onEndReached={[Function]}
onLayout={[Function]}
onMomentumScrollBegin={[Function]}
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
removeClippedSubviews={true}
renderItem={[Function]}
scrollEventThrottle={60}
stickyHeaderIndices={[]}
style={
{
"backgroundColor": "#ffffff",
"flex": 1,
}
}
testID="UserListRow.section_list"
>
<View>
<View
onLayout={[Function]}
style={null}
>
<View
style={
{
"backgroundColor": "#ffffff",
}
}
>
<View
style={
{
"backgroundColor": "rgba(63,67,80,0.08)",
"height": 24,
"justifyContent": "center",
"paddingLeft": 16,
}
}
>
<Text
style={
{
"color": "#3f4350",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 12,
"fontWeight": "600",
"lineHeight": 16,
}
}
>
J
</Text>
</View>
</View>
</View>
<View
onLayout={[Function]}
style={null}
>
<View
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
>
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
>
<View
style={
{
"flex": 1,
"flexDirection": "row",
"height": 58,
"overflow": "hidden",
"paddingHorizontal": 20,
}
}
testID="create_direct_message.user_list.user_item.1"
>
<View
style={
[
{
"alignItems": "center",
"color": "#3f4350",
"flexDirection": "row",
},
{
"opacity": 1,
},
]
}
>
<View
style={
{
"borderRadius": 21.5,
"height": 42,
"width": 42,
}
}
testID="create_direct_message.user_list.user_item.1.profile_picture"
>
<Icon
name="account-outline"
size={24}
style={
{
"color": "rgba(63,67,80,0.48)",
}
}
/>
</View>
</View>
<View
style={
[
{
"flex": 1,
"flexDirection": "column",
"justifyContent": "center",
"paddingHorizontal": 10,
},
{
"opacity": 1,
},
]
}
>
<View
style={
{
"flexDirection": "row",
}
}
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
{
"color": "#3f4350",
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"height": 24,
"lineHeight": 24,
"maxWidth": "80%",
}
}
testID="create_direct_message.user_list.user_item.1.display_name"
>
johndoe
</Text>
</View>
</View>
<View
style={
{
"alignItems": "center",
"justifyContent": "center",
}
}
>
<Icon
color="rgba(63,67,80,0.1024)"
name="circle-outline"
size={28}
/>
</View>
</View>
</View>
</View>
</View>
<View
onLayout={[Function]}
style={null}
/>
<View
onLayout={[Function]}
>
<View
style={
{
"alignItems": "center",
"flex": 1,
"justifyContent": "center",
}
}
>
<ActivityIndicator
color="#1c58d9"
size="large"
/>
</View>
</View>
</View>
</RCTScrollView>
`;
exports[`components/channel_list_row should show results and tutorial 1`] = `
<RCTScrollView
ListEmptyComponent={[Function]}
ListFooterComponent={[Function]}
contentContainerStyle={
{
"flexGrow": 1,
}
}
data={
[
{
"data": [
{
"auth_service": "",
"create_at": 1111,
"delete_at": 0,
"email": "john@doe.com",
"first_name": "",
"id": "1",
"last_name": "",
"locale": "",
"nickname": "",
"notify_props": {
"channel": "true",
"comments": "never",
"desktop": "mention",
"desktop_sound": "true",
"email": "true",
"first_name": "true",
"mention_keys": "",
"push": "mention",
"push_status": "away",
},
"position": "",
"roles": "",
"update_at": 1111,
"username": "johndoe",
},
],
"id": "J",
},
]
}
extraData={false}
getItem={[Function]}
getItemCount={[Function]}
initialNumToRender={15}
keyExtractor={[Function]}
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={16}
onContentSizeChange={[Function]}
onEndReached={[Function]}
onLayout={[Function]}
onMomentumScrollBegin={[Function]}
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
removeClippedSubviews={true}
renderItem={[Function]}
scrollEventThrottle={60}
stickyHeaderIndices={[]}
style={
{
"backgroundColor": "#ffffff",
"flex": 1,
}
}
testID="UserListRow.section_list"
>
<View>
<View
onLayout={[Function]}
style={null}
>
<View
style={
{
"backgroundColor": "#ffffff",
}
}
>
<View
style={
{
"backgroundColor": "rgba(63,67,80,0.08)",
"height": 24,
"justifyContent": "center",
"paddingLeft": 16,
}
}
>
<Text
style={
{
"color": "#3f4350",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 12,
"fontWeight": "600",
"lineHeight": 16,
}
}
>
J
</Text>
</View>
</View>
</View>
<View
onLayout={[Function]}
style={null}
>
<View
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
>
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
>
<View
style={
{
"flex": 1,
"flexDirection": "row",
"height": 58,
"overflow": "hidden",
"paddingHorizontal": 20,
}
}
testID="create_direct_message.user_list.user_item.1"
>
<View
style={
[
{
"alignItems": "center",
"color": "#3f4350",
"flexDirection": "row",
},
{
"opacity": 1,
},
]
}
>
<View
style={
{
"borderRadius": 21.5,
"height": 42,
"width": 42,
}
}
testID="create_direct_message.user_list.user_item.1.profile_picture"
>
<Icon
name="account-outline"
size={24}
style={
{
"color": "rgba(63,67,80,0.48)",
}
}
/>
</View>
</View>
<View
style={
[
{
"flex": 1,
"flexDirection": "column",
"justifyContent": "center",
"paddingHorizontal": 10,
},
{
"opacity": 1,
},
]
}
>
<View
style={
{
"flexDirection": "row",
}
}
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
{
"color": "#3f4350",
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"height": 24,
"lineHeight": 24,
"maxWidth": "80%",
}
}
testID="create_direct_message.user_list.user_item.1.display_name"
>
johndoe
</Text>
</View>
</View>
<View
style={
{
"alignItems": "center",
"justifyContent": "center",
}
}
>
<Icon
color="rgba(63,67,80,0.1024)"
name="circle-outline"
size={28}
/>
</View>
</View>
</View>
</View>
</View>
<View
onLayout={[Function]}
style={null}
/>
<View
onLayout={[Function]}
>
<View
style={
{
"alignItems": "center",
"flex": 1,
"justifyContent": "center",
}
}
>
<ActivityIndicator
color="#1c58d9"
size="large"
/>
</View>
</View>
</View>
</RCTScrollView>
`;
exports[`components/channel_list_row should show results no tutorial 1`] = `
<RCTScrollView
ListEmptyComponent={[Function]}
ListFooterComponent={[Function]}
contentContainerStyle={
{
"flexGrow": 1,
}
}
data={
[
{
"data": [
{
"auth_service": "",
"create_at": 1111,
"delete_at": 0,
"email": "john@doe.com",
"first_name": "",
"id": "1",
"last_name": "",
"locale": "",
"nickname": "",
"notify_props": {
"channel": "true",
"comments": "never",
"desktop": "mention",
"desktop_sound": "true",
"email": "true",
"first_name": "true",
"mention_keys": "",
"push": "mention",
"push_status": "away",
},
"position": "",
"roles": "",
"update_at": 1111,
"username": "johndoe",
},
],
"id": "J",
},
]
}
extraData={false}
getItem={[Function]}
getItemCount={[Function]}
initialNumToRender={15}
keyExtractor={[Function]}
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={16}
onContentSizeChange={[Function]}
onEndReached={[Function]}
onLayout={[Function]}
onMomentumScrollBegin={[Function]}
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
removeClippedSubviews={true}
renderItem={[Function]}
scrollEventThrottle={60}
stickyHeaderIndices={[]}
style={
{
"backgroundColor": "#ffffff",
"flex": 1,
}
}
testID="UserListRow.section_list"
>
<View>
<View
onLayout={[Function]}
style={null}
>
<View
style={
{
"backgroundColor": "#ffffff",
}
}
>
<View
style={
{
"backgroundColor": "rgba(63,67,80,0.08)",
"height": 24,
"justifyContent": "center",
"paddingLeft": 16,
}
}
>
<Text
style={
{
"color": "#3f4350",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 12,
"fontWeight": "600",
"lineHeight": 16,
}
}
>
J
</Text>
</View>
</View>
</View>
<View
onLayout={[Function]}
style={null}
>
<View
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
>
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
>
<View
style={
{
"flex": 1,
"flexDirection": "row",
"height": 58,
"overflow": "hidden",
"paddingHorizontal": 20,
}
}
testID="create_direct_message.user_list.user_item.1"
>
<View
style={
[
{
"alignItems": "center",
"color": "#3f4350",
"flexDirection": "row",
},
{
"opacity": 1,
},
]
}
>
<View
style={
{
"borderRadius": 21.5,
"height": 42,
"width": 42,
}
}
testID="create_direct_message.user_list.user_item.1.profile_picture"
>
<Icon
name="account-outline"
size={24}
style={
{
"color": "rgba(63,67,80,0.48)",
}
}
/>
</View>
</View>
<View
style={
[
{
"flex": 1,
"flexDirection": "column",
"justifyContent": "center",
"paddingHorizontal": 10,
},
{
"opacity": 1,
},
]
}
>
<View
style={
{
"flexDirection": "row",
}
}
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
{
"color": "#3f4350",
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"height": 24,
"lineHeight": 24,
"maxWidth": "80%",
}
}
testID="create_direct_message.user_list.user_item.1.display_name"
>
johndoe
</Text>
</View>
</View>
<View
style={
{
"alignItems": "center",
"justifyContent": "center",
}
}
>
<Icon
color="rgba(63,67,80,0.1024)"
name="circle-outline"
size={28}
/>
</View>
</View>
</View>
</View>
</View>
<View
onLayout={[Function]}
style={null}
/>
<View
onLayout={[Function]}
>
<View
style={
{
"alignItems": "center",
"flex": 1,
"justifyContent": "center",
}
}
>
<ActivityIndicator
color="#1c58d9"
size="large"
/>
</View>
</View>
</View>
</RCTScrollView>
`;

View File

@@ -0,0 +1,116 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Database from '@nozbe/watermelondb/Database';
import React from 'react';
import {renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
import UserList from '.';
describe('components/channel_list_row', () => {
let database: Database;
const user: UserProfile = {
id: '1',
create_at: 1111,
update_at: 1111,
delete_at: 0,
username: 'johndoe',
auth_service: '',
email: 'john@doe.com',
nickname: '',
first_name: '',
last_name: '',
position: '',
roles: '',
locale: '',
notify_props: {
channel: 'true',
comments: 'never',
desktop: 'mention',
desktop_sound: 'true',
email: 'true',
first_name: 'true',
mention_keys: '',
push: 'mention',
push_status: 'away',
},
};
beforeAll(async () => {
const server = await TestHelper.setupServerDatabase();
database = server.database;
});
it('should show no results', () => {
const wrapper = renderWithEverything(
<UserList
profiles={[user]}
testID='UserListRow'
currentUserId={'1'}
teammateNameDisplay={'johndoe'}
handleSelectProfile={() => {
// noop
}}
fetchMore={() => {
// noop
}}
loading={true}
selectedIds={{}}
showNoResults={true}
tutorialWatched={true}
/>,
{database},
);
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('should show results no tutorial', () => {
const wrapper = renderWithEverything(
<UserList
profiles={[user]}
testID='UserListRow'
currentUserId={'1'}
teammateNameDisplay={'johndoe'}
handleSelectProfile={() => {
// noop
}}
fetchMore={() => {
// noop
}}
loading={true}
selectedIds={{}}
showNoResults={true}
tutorialWatched={true}
/>,
{database},
);
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('should show results and tutorial', () => {
const wrapper = renderWithEverything(
<UserList
profiles={[user]}
testID='UserListRow'
currentUserId={'1'}
teammateNameDisplay={'johndoe'}
handleSelectProfile={() => {
// noop
}}
fetchMore={() => {
// noop
}}
loading={true}
selectedIds={{}}
showNoResults={false}
tutorialWatched={false}
/>,
{database},
);
expect(wrapper.toJSON()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,280 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {FlatList, Keyboard, ListRenderItemInfo, Platform, SectionList, SectionListData, Text, View} from 'react-native';
import {storeProfile} from '@actions/local/user';
import Loading from '@components/loading';
import NoResultsWithTerm from '@components/no_results_with_term';
import UserListRow from '@components/user_list_row';
import {General, Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useKeyboardHeight} from '@hooks/device';
import {openAsBottomSheet} from '@screens/navigation';
import {
changeOpacity,
makeStyleSheetFromTheme,
} from '@utils/theme';
import {typography} from '@utils/typography';
const INITIAL_BATCH_TO_RENDER = 15;
const SCROLL_EVENT_THROTTLE = 60;
const keyboardDismissProp = Platform.select({
android: {
onScrollBeginDrag: Keyboard.dismiss,
},
ios: {
keyboardDismissMode: 'on-drag' as const,
},
});
const keyExtractor = (item: UserProfile) => {
return item.id;
};
const sectionKeyExtractor = (profile: UserProfile) => {
// Group items alphabetically by first letter of username
return profile.username[0].toUpperCase();
};
export function createProfilesSections(profiles: UserProfile[]) {
const sections: {[key: string]: UserProfile[]} = {};
const sectionKeys: string[] = [];
for (const profile of profiles) {
const sectionKey = sectionKeyExtractor(profile);
if (!sections[sectionKey]) {
sections[sectionKey] = [];
sectionKeys.push(sectionKey);
}
sections[sectionKey].push(profile);
}
sectionKeys.sort();
return sectionKeys.map((sectionKey) => {
return {
id: sectionKey,
data: sections[sectionKey],
};
});
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
list: {
backgroundColor: theme.centerChannelBg,
flex: 1,
...Platform.select({
android: {
marginBottom: 20,
},
}),
},
container: {
flexGrow: 1,
},
loadingContainer: {
flex: 1,
justifyContent: 'center' as const,
alignItems: 'center' as const,
},
noResultContainer: {
flexGrow: 1,
alignItems: 'center' as const,
justifyContent: 'center' as const,
},
sectionContainer: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
paddingLeft: 16,
justifyContent: 'center',
height: 24,
},
sectionWrapper: {
backgroundColor: theme.centerChannelBg,
},
sectionText: {
color: theme.centerChannelColor,
...typography('Body', 75, 'SemiBold'),
},
};
});
type Props = {
profiles: UserProfile[];
currentUserId: string;
teammateNameDisplay: string;
handleSelectProfile: (user: UserProfile) => void;
fetchMore: () => void;
loading: boolean;
showNoResults: boolean;
selectedIds: {[id: string]: UserProfile};
testID?: string;
term?: string;
tutorialWatched: boolean;
}
export default function UserList({
profiles,
selectedIds,
currentUserId,
teammateNameDisplay,
handleSelectProfile,
fetchMore,
loading,
showNoResults,
term,
testID,
tutorialWatched,
}: Props) {
const intl = useIntl();
const theme = useTheme();
const serverUrl = useServerUrl();
const style = getStyleFromTheme(theme);
const keyboardHeight = useKeyboardHeight();
const noResutsStyle = useMemo(() => [
style.noResultContainer,
{paddingBottom: keyboardHeight},
], [style, keyboardHeight]);
const data = useMemo(() => {
if (term) {
return profiles;
}
return createProfilesSections(profiles);
}, [term, profiles]);
const openUserProfile = useCallback(async (profile: UserProfile) => {
const {user} = await storeProfile(serverUrl, profile);
if (user) {
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const closeButtonId = 'close-user-profile';
const props = {
closeButtonId,
userId: user.id,
location: Screens.USER_PROFILE,
};
Keyboard.dismiss();
openAsBottomSheet({screen, title, theme, closeButtonId, props});
}
}, []);
const renderItem = useCallback(({item, index, section}: ListRenderItemInfo<UserProfile> & {section?: SectionListData<UserProfile>}) => {
// The list will re-render when the selection changes because it's passed into the list as extraData
const selected = Boolean(selectedIds[item.id]);
const canAdd = Object.keys(selectedIds).length < General.MAX_USERS_IN_GM;
return (
<UserListRow
key={item.id}
highlight={section?.id === data?.[0].id && index === 0}
id={item.id}
isMyUser={currentUserId === item.id}
onPress={handleSelectProfile}
onLongPress={openUserProfile}
selectable={canAdd}
selected={selected}
testID='create_direct_message.user_list.user_item'
teammateNameDisplay={teammateNameDisplay}
tutorialWatched={tutorialWatched}
user={item}
/>
);
}, [selectedIds, currentUserId, handleSelectProfile, teammateNameDisplay, tutorialWatched, data]);
const renderLoading = useCallback(() => {
if (!loading) {
return null;
}
return (
<Loading
color={theme.buttonBg}
containerStyle={style.loadingContainer}
size='large'
/>
);
}, [loading, theme]);
const renderNoResults = useCallback(() => {
if (!showNoResults || !term) {
return null;
}
return (
<View style={noResutsStyle}>
<NoResultsWithTerm term={term}/>
</View>
);
}, [showNoResults && style, term, noResutsStyle]);
const renderSectionHeader = useCallback(({section}: {section: SectionListData<UserProfile>}) => {
return (
<View style={style.sectionWrapper}>
<View style={style.sectionContainer}>
<Text style={style.sectionText}>{section.id}</Text>
</View>
</View>
);
}, [style]);
const renderFlatList = (items: UserProfile[]) => {
return (
<FlatList
contentContainerStyle={style.container}
data={items}
extraData={selectedIds}
keyboardShouldPersistTaps='always'
{...keyboardDismissProp}
keyExtractor={keyExtractor}
initialNumToRender={INITIAL_BATCH_TO_RENDER}
ListEmptyComponent={renderNoResults}
ListFooterComponent={renderLoading}
maxToRenderPerBatch={INITIAL_BATCH_TO_RENDER + 1}
removeClippedSubviews={true}
renderItem={renderItem}
scrollEventThrottle={SCROLL_EVENT_THROTTLE}
style={style.list}
testID={`${testID}.flat_list`}
/>
);
};
const renderSectionList = (sections: Array<SectionListData<UserProfile>>) => {
return (
<SectionList
contentContainerStyle={style.container}
extraData={loading ? false : selectedIds}
keyboardShouldPersistTaps='always'
{...keyboardDismissProp}
keyExtractor={keyExtractor}
initialNumToRender={INITIAL_BATCH_TO_RENDER}
ListEmptyComponent={renderNoResults}
ListFooterComponent={renderLoading}
maxToRenderPerBatch={INITIAL_BATCH_TO_RENDER + 1}
removeClippedSubviews={true}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
scrollEventThrottle={SCROLL_EVENT_THROTTLE}
sections={sections}
style={style.list}
stickySectionHeadersEnabled={false}
testID={`${testID}.section_list`}
onEndReached={fetchMore}
/>
);
};
if (term) {
return renderFlatList(data as UserProfile[]);
}
return renderSectionList(data as Array<SectionListData<UserProfile>>);
}