// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import React, {useCallback, useMemo} from 'react'; import {defineMessages, type IntlShape, useIntl} from 'react-intl'; import {FlatList, Keyboard, type ListRenderItemInfo, Platform, SectionList, type 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} from '@constants'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {useKeyboardHeight} from '@hooks/device'; import {openUserProfileModal} from '@screens/navigation'; import { changeOpacity, makeStyleSheetFromTheme, } from '@utils/theme'; import {typography} from '@utils/typography'; import type UserModel from '@typings/database/models/servers/user'; import type {AvailableScreens} from '@typings/screens/navigation'; type UserProfileWithChannelAdmin = UserProfile & {scheme_admin?: boolean} type RenderItemType = ListRenderItemInfo & {section?: SectionListData} const INITIAL_BATCH_TO_RENDER = 15; const SCROLL_EVENT_THROTTLE = 60; const messages = defineMessages({ admins: { id: 'mobile.manage_members.section_title_admins', defaultMessage: 'CHANNEL ADMINS', }, members: { id: 'mobile.manage_members.section_title_members', defaultMessage: 'MEMBERS', }, }); 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(); }; const sectionRoleKeyExtractor = (cAdmin: boolean) => { // Group items by channel admin or channel member return cAdmin ? messages.admins : messages.members; }; export function createProfilesSections(intl: IntlShape, profiles: UserProfile[], members?: ChannelMembership[]) { if (!profiles.length) { return []; } const sections = new Map(); if (members?.length) { // when channel members are provided, build the sections by admins and members const membersDictionary = new Map(); const membersSections = new Map(); const {formatMessage} = intl; members.forEach((m) => membersDictionary.set(m.user_id, m)); profiles.forEach((p) => { const member = membersDictionary.get(p.id); if (member) { const sectionKey = sectionRoleKeyExtractor(member.scheme_admin!).id; const section = membersSections.get(sectionKey) || []; section.push({...p, scheme_admin: member.scheme_admin}); membersSections.set(sectionKey, section); } }); sections.set(formatMessage(messages.admins), membersSections.get(messages.admins.id) || []); sections.set(formatMessage(messages.members), membersSections.get(messages.members.id) || []); } else { // when channel members are not provided, build the sections alphabetically profiles.forEach((p) => { const sectionKey = sectionKeyExtractor(p); const sectionValue = sections.get(sectionKey) || []; const section = [...sectionValue, p]; sections.set(sectionKey, section); }); } const results = []; let index = 0; for (const [k, v] of sections) { if (v.length) { results.push({ first: index === 0, id: k, data: v, }); index++; } } return results; } function createProfiles(profiles: UserProfile[], members?: ChannelMembership[]): UserProfileWithChannelAdmin[] { if (!profiles.length) { return []; } const profileMap = new Map(); profiles.forEach((profile) => { profileMap.set(profile.id, profile); }); if (members?.length) { members.forEach((m) => { const profileFound = profileMap.get(m.user_id); if (profileFound) { profileFound.scheme_admin = m.scheme_admin; } }); } return Array.from(profileMap.values()); } const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { return { list: { backgroundColor: theme.centerChannelBg, flex: 1, }, 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[]; channelMembers?: ChannelMembership[]; handleSelectProfile: (user: UserProfile | UserModel) => void; fetchMore?: () => void; loading: boolean; manageMode?: boolean; showManageMode?: boolean; showNoResults: boolean; selectedIds: Set; testID?: string; term?: string; tutorialWatched: boolean; includeUserMargin?: boolean; location: AvailableScreens; customSection?: (profiles: UserProfile[]) => Array>; } export default function UserList({ profiles, channelMembers, selectedIds, handleSelectProfile, fetchMore, loading, manageMode = false, showManageMode = false, showNoResults, term, testID, tutorialWatched, includeUserMargin, location, customSection, }: 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 (profiles.length === 0 && !loading) { return []; } if (term) { return createProfiles(profiles, channelMembers); } if (customSection) { return customSection(profiles); } return createProfilesSections(intl, profiles, channelMembers); }, [channelMembers, customSection, intl, loading, profiles, term]); const openUserProfile = useCallback(async (profile: UserProfile | UserModel) => { let user: UserModel; if ('create_at' in profile) { const res = await storeProfile(serverUrl, profile); if (!res.user) { return; } user = res.user; } else { user = profile; } openUserProfileModal(intl, theme, { userId: user.id, location, }); }, [intl, location, serverUrl, theme]); const renderItem = useCallback(({item, index, section}: RenderItemType) => { // The list will re-render when the selection changes because it's passed into the list as extraData const selected = selectedIds.has(item.id); const canAdd = selectedIds.size < General.MAX_USERS_IN_GM; const isChAdmin = item.scheme_admin || false; return ( ); }, [selectedIds, manageMode, handleSelectProfile, openUserProfile, showManageMode, testID, tutorialWatched, includeUserMargin]); const renderLoading = useCallback(() => { if (!loading) { return null; } return ( ); }, [loading, style.loadingContainer, theme.buttonBg]); const renderNoResults = useCallback(() => { if (!showNoResults || !term) { return null; } return ( ); }, [showNoResults, term, noResutsStyle]); const renderSectionHeader = useCallback(({section}: {section: SectionListData}) => { return ( {section.id} ); }, [style]); const renderFlatList = (items: UserProfile[]) => { return ( ); }; const renderSectionList = (sections: Array>) => { return ( ); }; if (term) { return renderFlatList(data as UserProfileWithChannelAdmin[]); } return renderSectionList(data as Array>); }