import type { FC } from '../../lib/teact/teact'; import React, { useCallback, useEffect, useMemo, useRef, useState, memo, } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; import type { ApiMessage, ApiChat, ApiChatMember, ApiUser, ApiUserStatus, } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types'; import type { ISettings, ProfileState, ProfileTabType, SharedMediaType, } from '../../types'; import { NewChatMembersProgress, MediaViewerOrigin, AudioOrigin } from '../../types'; import { MEMBERS_SLICE, PROFILE_SENSITIVE_AREA, SHARED_MEDIA_SLICE, SLIDE_TRANSITION_DURATION, } from '../../config'; import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; import { getHasAdminRight, isChatAdmin, isChatChannel, isChatGroup, isUserBot, isUserId, isUserRightBanned, } from '../../global/helpers'; import { selectChatMessages, selectChat, selectCurrentMediaSearch, selectIsRightColumnShown, selectTheme, selectActiveDownloadIds, selectUser, } from '../../global/selectors'; import { captureEvents, SwipeDirection } from '../../util/captureEvents'; import { getSenderName } from '../left/search/helpers/getSenderName'; import useCacheBuster from '../../hooks/useCacheBuster'; import useProfileViewportIds from './hooks/useProfileViewportIds'; import useProfileState from './hooks/useProfileState'; import useTransitionFixes from './hooks/useTransitionFixes'; import useAsyncRendering from './hooks/useAsyncRendering'; import useLang from '../../hooks/useLang'; import { useIntersectionObserver } from '../../hooks/useIntersectionObserver'; import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; import Transition from '../ui/Transition'; import InfiniteScroll from '../ui/InfiniteScroll'; import TabList from '../ui/TabList'; import Spinner from '../ui/Spinner'; import ListItem from '../ui/ListItem'; import PrivateChatInfo from '../common/PrivateChatInfo'; import ProfileInfo from '../common/ProfileInfo'; import Document from '../common/Document'; import Audio from '../common/Audio'; import ChatExtra from '../common/ChatExtra'; import Media from '../common/Media'; import WebLink from '../common/WebLink'; import NothingFound from '../common/NothingFound'; import FloatingActionButton from '../ui/FloatingActionButton'; import DeleteMemberModal from './DeleteMemberModal'; import GroupChatInfo from '../common/GroupChatInfo'; import './Profile.scss'; type OwnProps = { chatId: string; topicId?: number; profileState: ProfileState; isMobile?: boolean; onProfileStateChange: (state: ProfileState) => void; }; type StateProps = { theme: ISettings['theme']; isChannel?: boolean; currentUserId?: string; resolvedUserId?: string; messagesById?: Record; foundIds?: number[]; mediaSearchType?: SharedMediaType; hasCommonChatsTab?: boolean; hasMembersTab?: boolean; areMembersHidden?: boolean; canAddMembers?: boolean; canDeleteMembers?: boolean; members?: ApiChatMember[]; adminMembersById?: Record; commonChatIds?: string[]; chatsById: Record; usersById: Record; userStatusesById: Record; isRightColumnShown: boolean; isRestricted?: boolean; lastSyncTime?: number; activeDownloadIds: number[]; isChatProtected?: boolean; }; const TABS = [ { type: 'media', title: 'SharedMediaTab2' }, { type: 'documents', title: 'SharedFilesTab2' }, { type: 'links', title: 'SharedLinksTab2' }, { type: 'audio', title: 'SharedMusicTab2' }, ]; const HIDDEN_RENDER_DELAY = 1000; const INTERSECTION_THROTTLE = 500; const Profile: FC = ({ chatId, topicId, profileState, onProfileStateChange, theme, isChannel, resolvedUserId, currentUserId, messagesById, foundIds, mediaSearchType, hasCommonChatsTab, hasMembersTab, areMembersHidden, canAddMembers, canDeleteMembers, commonChatIds, members, adminMembersById, usersById, userStatusesById, chatsById, isRightColumnShown, isRestricted, lastSyncTime, activeDownloadIds, isChatProtected, }) => { const { setLocalMediaSearchType, loadMoreMembers, loadCommonChats, openChat, searchMediaMessagesLocal, openMediaViewer, openAudioPlayer, focusMessage, loadProfilePhotos, setNewChatMembersDialogState, } = getActions(); // eslint-disable-next-line no-null/no-null const containerRef = useRef(null); // eslint-disable-next-line no-null/no-null const transitionRef = useRef(null); const lang = useLang(); const [activeTab, setActiveTab] = useState(0); const [deletingUserId, setDeletingUserId] = useState(); const tabs = useMemo(() => ([ ...(hasMembersTab ? [{ type: 'members', title: isChannel ? 'ChannelSubscribers' : 'GroupMembers', }] : []), ...TABS, // TODO The filter for voice messages currently does not work // in forum topics. Return it when it's fixed on the server side. ...(!topicId ? [{ type: 'voice', title: 'SharedVoiceTab2' }] : []), ...(hasCommonChatsTab ? [{ type: 'commonChats', title: 'SharedGroupsTab2' }] : []), ]), [hasCommonChatsTab, hasMembersTab, isChannel, topicId]); const renderingActiveTab = activeTab > tabs.length - 1 ? tabs.length - 1 : activeTab; const tabType = tabs[renderingActiveTab].type as ProfileTabType; const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds( loadMoreMembers, loadCommonChats, searchMediaMessagesLocal, tabType, mediaSearchType, members, commonChatIds, usersById, userStatusesById, chatsById, messagesById, foundIds, lastSyncTime, topicId, ); const isFirstTab = resultType === 'members' || (!hasMembersTab && resultType === 'media'); const activeKey = tabs.findIndex(({ type }) => type === resultType); const { handleScroll } = useProfileState(containerRef, resultType, profileState, onProfileStateChange); const { applyTransitionFix, releaseTransitionFix } = useTransitionFixes(containerRef); const [cacheBuster, resetCacheBuster] = useCacheBuster(); const { observe: observeIntersectionForMedia } = useIntersectionObserver({ rootRef: containerRef, throttleMs: INTERSECTION_THROTTLE, }); const handleTransitionStop = useCallback(() => { releaseTransitionFix(); resetCacheBuster(); }, [releaseTransitionFix, resetCacheBuster]); const handleNewMemberDialogOpen = useCallback(() => { setNewChatMembersDialogState({ newChatMembersProgress: NewChatMembersProgress.InProgress }); }, [setNewChatMembersDialogState]); // Update search type when switching tabs or forum topics useEffect(() => { setLocalMediaSearchType({ mediaType: tabType as SharedMediaType }); }, [setLocalMediaSearchType, tabType, topicId]); const profileId = resolvedUserId || chatId; useEffect(() => { if (lastSyncTime) { loadProfilePhotos({ profileId }); } }, [loadProfilePhotos, profileId, lastSyncTime]); const handleSelectMedia = useCallback((mediaId: number) => { openMediaViewer({ chatId: profileId, threadId: MAIN_THREAD_ID, mediaId, origin: MediaViewerOrigin.SharedMedia, }); }, [profileId, openMediaViewer]); const handlePlayAudio = useCallback((messageId: number) => { openAudioPlayer({ chatId: profileId, messageId }); }, [profileId, openAudioPlayer]); const handleMemberClick = useCallback((id: string) => { openChat({ id }); }, [openChat]); const handleMessageFocus = useCallback((messageId: number) => { focusMessage({ chatId: profileId, messageId }); }, [profileId, focusMessage]); const handleDeleteMembersModalClose = useCallback(() => { setDeletingUserId(undefined); }, []); useEffectWithPrevDeps(([prevHasMemberTabs]) => { if (activeTab === 0 || prevHasMemberTabs === hasMembersTab) { return; } const newActiveTab = activeTab + (hasMembersTab ? 1 : -1); setActiveTab(Math.min(newActiveTab, tabs.length - 1)); }, [hasMembersTab, activeTab, tabs]); useEffect(() => { if (!transitionRef.current || !IS_TOUCH_ENV) { return undefined; } return captureEvents(transitionRef.current, { selectorToPreventScroll: '.Profile', onSwipe: ((e, direction) => { if (direction === SwipeDirection.Left) { setActiveTab(Math.min(renderingActiveTab + 1, tabs.length - 1)); return true; } else if (direction === SwipeDirection.Right) { setActiveTab(Math.max(0, renderingActiveTab - 1)); return true; } return false; }), }); }, [renderingActiveTab, tabs.length]); let renderingDelay; // @optimization Used to unparallelize rendering of message list and profile media if (isFirstTab) { renderingDelay = !isRightColumnShown ? HIDDEN_RENDER_DELAY : 0; // @optimization Used to delay first render of secondary tabs while animating } else if (!viewportIds) { renderingDelay = SLIDE_TRANSITION_DURATION; } const canRenderContent = useAsyncRendering([chatId, topicId, resultType, renderingActiveTab], renderingDelay); function getMemberContextAction(memberId: string) { return memberId === currentUserId || !canDeleteMembers ? undefined : [{ title: lang('lng_context_remove_from_group'), icon: 'stop', handler: () => { setDeletingUserId(memberId); }, }]; } function renderContent() { if (!viewportIds || !canRenderContent || !messagesById) { const noSpinner = isFirstTab && !canRenderContent; const forceRenderHiddenMembers = Boolean(resultType === 'members' && areMembersHidden); return (
{!noSpinner && !forceRenderHiddenMembers && } {forceRenderHiddenMembers && }
); } if (!viewportIds.length) { let text: string; switch (resultType) { case 'members': text = areMembersHidden ? 'You have no access to group members list.' : 'No members found'; break; case 'commonChats': text = lang('NoGroupsInCommon'); break; case 'documents': text = lang('lng_media_file_empty'); break; case 'links': text = lang('lng_media_link_empty'); break; case 'audio': text = lang('lng_media_song_empty'); break; case 'voice': text = lang('lng_media_audio_empty'); break; default: text = lang('SharedMedia.EmptyTitle'); } return (
); } return (
{resultType === 'media' ? ( (viewportIds as number[])!.map((id) => messagesById[id] && ( )) ) : resultType === 'documents' ? ( (viewportIds as number[])!.map((id) => messagesById[id] && ( )) ) : resultType === 'links' ? ( (viewportIds as number[])!.map((id) => messagesById[id] && ( )) ) : resultType === 'audio' ? ( (viewportIds as number[])!.map((id) => messagesById[id] && (
); } return ( {!noProfileInfo && renderProfileInfo(chatId, resolvedUserId, isRightColumnShown && canRenderContent)} {!isRestricted && (
{renderContent()}
)} {canAddMembers && ( )} {canDeleteMembers && ( )}
); }; function renderProfileInfo(chatId: string, resolvedUserId: string | undefined, isReady: boolean) { return (
); } function buildInfiniteScrollItemSelector(resultType: string) { return [ // Used on first render `.shared-media-transition > div:only-child > .${resultType}-list > .scroll-item`, // Used after transition `.shared-media-transition > .Transition__slide--active > .${resultType}-list > .scroll-item`, ].join(', '); } export default memo(withGlobal( (global, { chatId, topicId, isMobile }): StateProps => { const chat = selectChat(global, chatId); const messagesById = selectChatMessages(global, chatId); const { currentType: mediaSearchType, resultsByType } = selectCurrentMediaSearch(global) || {}; const { foundIds } = (resultsByType && mediaSearchType && resultsByType[mediaSearchType]) || {}; const { byId: usersById, statusesById: userStatusesById } = global.users; const { byId: chatsById } = global.chats; const isGroup = chat && isChatGroup(chat); const isChannel = chat && isChatChannel(chat); const hasMembersTab = !topicId && (isGroup || (isChannel && isChatAdmin(chat!))); const members = chat?.fullInfo?.members; const adminMembersById = chat?.fullInfo?.adminMembersById; const areMembersHidden = hasMembersTab && chat && (chat.isForbidden || (chat.fullInfo && !chat.fullInfo.canViewMembers)); const canAddMembers = hasMembersTab && chat && (getHasAdminRight(chat, 'inviteUsers') || !isUserRightBanned(chat, 'inviteUsers') || chat.isCreator); const canDeleteMembers = hasMembersTab && chat && (getHasAdminRight(chat, 'banUsers') || chat.isCreator); const activeDownloadIds = selectActiveDownloadIds(global, chatId); let hasCommonChatsTab; let resolvedUserId; let user; if (isUserId(chatId)) { resolvedUserId = chatId; user = selectUser(global, resolvedUserId); hasCommonChatsTab = user && !user.isSelf && !isUserBot(user); } return { theme: selectTheme(global), isChannel, resolvedUserId, messagesById, foundIds, mediaSearchType, hasCommonChatsTab, hasMembersTab, areMembersHidden, canAddMembers, canDeleteMembers, currentUserId: global.currentUserId, isRightColumnShown: selectIsRightColumnShown(global, isMobile), isRestricted: chat?.isRestricted, lastSyncTime: global.lastSyncTime, activeDownloadIds, usersById, userStatusesById, chatsById, isChatProtected: chat?.isProtected, ...(hasMembersTab && members && { members, adminMembersById }), ...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }), }; }, )(Profile));